diff --git a/.agent-os/decision-policy.yaml b/.agent-os/decision-policy.yaml new file mode 100644 index 0000000..a5258cc --- /dev/null +++ b/.agent-os/decision-policy.yaml @@ -0,0 +1,3 @@ +decision_policy: + strictness: warn + downgrade_reason: "" diff --git a/.agent-os/specs.yaml b/.agent-os/specs.yaml index 046cdff..62e0257 100644 --- a/.agent-os/specs.yaml +++ b/.agent-os/specs.yaml @@ -1,7 +1,22 @@ -active_spec: SPEC-20260513-001 +active_spec: SPEC-20260711-001 specs: - id: SPEC-20260513-001 title: "KnowledgeOS Core Product Philosophy" status: active created_at: 2026-05-13T05:59:52.646718+00:00 path: .agent-os/specs/SPEC-20260513-001 + - id: SPEC-20260519-001 + title: "Capability effect verification" + status: active + created_at: 2026-05-19T01:20:48.972160+00:00 + path: .agent-os/specs/SPEC-20260519-001 + - id: SPEC-20260529-001 + title: "Decision Graph Module" + status: active + created_at: 2026-05-29T14:25:59.993564+00:00 + path: .agent-os/specs/SPEC-20260529-001 + - id: SPEC-20260711-001 + title: "Subagent runtime repair and catalog validation" + status: active + created_at: 2026-07-11T12:28:04.694154+00:00 + path: .agent-os/specs/SPEC-20260711-001 diff --git a/.agent-os/specs/SPEC-20260529-001/acceptance.md b/.agent-os/specs/SPEC-20260529-001/acceptance.md new file mode 100644 index 0000000..f0f0e82 --- /dev/null +++ b/.agent-os/specs/SPEC-20260529-001/acceptance.md @@ -0,0 +1,10 @@ +# Acceptance + +- `decision-event` records command-generated decision events and returns `DECISION_OK`. +- `decision-query` returns filtered parent/child decision events. +- `verify-decisions` rejects forged or orphaned decision events and emits `DECISION_VERIFY_OK`. +- `decision-policy strictness: warn` does not block linear work without decision events. +- `decision-policy strictness: enforce` blocks completion without valid decision evidence. +- `decision-policy strictness: off` requires `downgrade_reason`. +- `render-html --kind decision-map` creates a self-contained HTML sidecar with source hash metadata. +- Documentation explains Decision Graph as a module, not kernel. diff --git a/.agent-os/specs/SPEC-20260529-001/alignment.md b/.agent-os/specs/SPEC-20260529-001/alignment.md new file mode 100644 index 0000000..9eb87cb --- /dev/null +++ b/.agent-os/specs/SPEC-20260529-001/alignment.md @@ -0,0 +1,3 @@ +# Alignment + +No alignment has been run yet. diff --git a/.agent-os/specs/SPEC-20260529-001/change-log.ndjson b/.agent-os/specs/SPEC-20260529-001/change-log.ndjson new file mode 100644 index 0000000..52e3d12 --- /dev/null +++ b/.agent-os/specs/SPEC-20260529-001/change-log.ndjson @@ -0,0 +1,2 @@ +{"event_type": "create-spec", "set_active": true, "timestamp": "2026-05-29T14:25:59.994039+00:00", "title": "Decision Graph Module"} +{"timestamp":"2026-05-29T14:36:16Z","change":"Filled Decision Graph Module spec with public contract, acceptance, and non-goals."} diff --git a/.agent-os/specs/SPEC-20260529-001/non-goals.md b/.agent-os/specs/SPEC-20260529-001/non-goals.md new file mode 100644 index 0000000..e2f8241 --- /dev/null +++ b/.agent-os/specs/SPEC-20260529-001/non-goals.md @@ -0,0 +1,7 @@ +# Non-Goals + +- Do not store hidden chain-of-thought. +- Do not make Decision Graph mandatory for minimal projects. +- Do not replace `trace-step`, `phase-task`, `capability-event`, or `artifact-assert`. +- Do not auto-open HTML in v1. +- Do not build a full visual Workbench UI in this change. diff --git a/.agent-os/specs/SPEC-20260529-001/spec.md b/.agent-os/specs/SPEC-20260529-001/spec.md new file mode 100644 index 0000000..3c7d10e --- /dev/null +++ b/.agent-os/specs/SPEC-20260529-001/spec.md @@ -0,0 +1,26 @@ +# Spec: Decision Graph Module + +## Intent + +Add an optional Decision Graph module that records public, auditable natural-language decision summaries during KnowledgeOS runs. The module captures plan branches, route selections, inserted steps, abandoned branches, rollbacks, superseded paths, deferred work, human decisions, risk tradeoffs, and final decisions. + +The module must preserve KnowledgeOS product philosophy: + +```text +small kernel + pluggable modules + optional apps + project-level strictness + mandatory visible checkpoints +``` + +Decision Graph is not kernel. It is a module that can be observed by default and enforced only by project policy. + +## Public Contract + +- `decision-event` writes `.agent-os/runs//decision-events.ndjson` and emits `DECISION_OK`. +- `decision-query` filters decision events by run, task, kind, status, or parent id. +- `verify-decisions` checks command evidence, orphan parents, duplicate ids, invalid kinds/statuses, and unexplained abandoned/rollback/superseded branches. +- `render-html --kind decision-map` renders a static HTML sidecar from `decision-events.ndjson`. +- `.agent-os/decision-policy.yaml` defaults to `strictness: warn`. +- `complete-task` blocks only when decision verification status is failed, including `strictness: enforce` without valid decision evidence. + +## Source Of Truth + +The source of truth is NDJSON under `.agent-os/runs//decision-events.ndjson`. HTML is presentation only. diff --git a/.agent-os/specs/SPEC-20260711-001/acceptance.md b/.agent-os/specs/SPEC-20260711-001/acceptance.md new file mode 100644 index 0000000..c5fe8bd --- /dev/null +++ b/.agent-os/specs/SPEC-20260711-001/acceptance.md @@ -0,0 +1,6 @@ +# Acceptance Criteria + +- codex-default, codex-explorer, and codex-worker each complete three consecutive lifecycle smokes +- all 42 registered subagents complete at least one live invocation and role-contract check +- timeouts, orphaned agents, cleanup failures, and stub responses cannot count as success +- doctor, unit tests, guardrail scenarios, privacy scan, and GitHub release checks pass diff --git a/.agent-os/specs/SPEC-20260711-001/alignment.md b/.agent-os/specs/SPEC-20260711-001/alignment.md new file mode 100644 index 0000000..9eb87cb --- /dev/null +++ b/.agent-os/specs/SPEC-20260711-001/alignment.md @@ -0,0 +1,3 @@ +# Alignment + +No alignment has been run yet. diff --git a/.agent-os/specs/SPEC-20260711-001/change-log.ndjson b/.agent-os/specs/SPEC-20260711-001/change-log.ndjson new file mode 100644 index 0000000..7c8ced7 --- /dev/null +++ b/.agent-os/specs/SPEC-20260711-001/change-log.ndjson @@ -0,0 +1 @@ +{"event_type": "create-spec", "set_active": true, "timestamp": "2026-07-11T12:28:04.694773+00:00", "title": "Subagent runtime repair and catalog validation"} diff --git a/.agent-os/specs/SPEC-20260711-001/non-goals.md b/.agent-os/specs/SPEC-20260711-001/non-goals.md new file mode 100644 index 0000000..f92202b --- /dev/null +++ b/.agent-os/specs/SPEC-20260711-001/non-goals.md @@ -0,0 +1,4 @@ +# Non-Goals + +- Modify the proprietary Codex host runtime directly +- Use a registry or adapter-only check as evidence of live execution diff --git a/.agent-os/specs/SPEC-20260711-001/spec.md b/.agent-os/specs/SPEC-20260711-001/spec.md new file mode 100644 index 0000000..bcec177 --- /dev/null +++ b/.agent-os/specs/SPEC-20260711-001/spec.md @@ -0,0 +1,9 @@ +# Subagent runtime repair and catalog validation + +Spec ID: SPEC-20260711-001 +Status: active +Created At: 2026-07-11T12:28:04.694154+00:00 + +## Intent + +Reproduce and repair the codex-explorer lifecycle failure, then live-verify every registered Codex and Maestro subagent with auditable runtime evidence. diff --git a/.agent-os/startup-prompt.md b/.agent-os/startup-prompt.md index fbf1078..929760b 100644 --- a/.agent-os/startup-prompt.md +++ b/.agent-os/startup-prompt.md @@ -8,28 +8,39 @@ This prompt is only the session trigger. Durable rules live in `AGENTS.md`, `.ag Before substantial work: +0. At the start of every conversation, make a visible KnowledgeOS judgment and relay `KOS_DECISION` with project state (`managed` or `unmanaged`), work class (`simple`, `substantial`, or `blocked`), required flow (`answer-only`, `task`, `spec`, `thread-plan`, or `full lifecycle`), and reason. This is a public routing decision, not hidden reasoning. 1. Read `AGENTS.md`. -2. Read `.agent-os/workspace.yaml`, `.agent-os/project.yaml`, `.agent-os/tasks.yaml`, `.agent-os/specs.yaml`, `.agent-os/phase-policy.yaml`, `.agent-os/effect-policy.yaml`, `.agent-os/decisions.yaml`, `.agent-os/evals.yaml`, `.agent-os/fabric-link.yaml`, `.agent-os/read-policy.yaml`, `.agent-os/write-policy.yaml`, `.agent-os/dispatch-policy.yaml`, and `.agent-os/tool-registry.yaml`. +2. Read `.agent-os/workspace.yaml`, `.agent-os/project.yaml`, `.agent-os/tasks.yaml`, `.agent-os/specs.yaml`, `.agent-os/phase-policy.yaml`, `.agent-os/decision-policy.yaml`, `.agent-os/effect-policy.yaml`, `.agent-os/decisions.yaml`, `.agent-os/evals.yaml`, `.agent-os/fabric-link.yaml`, `.agent-os/read-policy.yaml`, `.agent-os/write-policy.yaml`, `.agent-os/dispatch-policy.yaml`, and `.agent-os/tool-registry.yaml`. 3. Run `./bin/knowledgeos doctor --project-root . --summary` and do not proceed if it fails. 4. If the user says `create spec`, `align spec`, `创建spec`, `对齐spec`, or equivalent, run `./bin/knowledgeos create-spec --project-root . --title ""` or `./bin/knowledgeos align-spec --project-root . --task-id <task-id>` before execution. -5. Select or confirm one task id from `.agent-os/tasks.yaml`. -6. Run `./bin/knowledgeos route-task --project-root . --task-id <task-id>`. -7. Run `./bin/knowledgeos dispatch-task --project-root . --task-id <task-id>` before invoking subagents, MCP tools, skills, workflows, or scripts. -8. Before planned mutation, run `./bin/knowledgeos check-route-write --project-root . --task-id <task-id> --path <planned-path>`. -9. Create run evidence with `./bin/knowledgeos run-task --project-root . --task-id <task-id>`. -10. Write/update the execution context with `./bin/knowledgeos context-pack --project-root . --task-id <task-id> --run-id <run-id>` and `./bin/knowledgeos plan-task --project-root . --task-id <task-id> --run-id <run-id> --summary "<summary>"`. -11. Pause at consultation checkpoints, state your recommended next move, name the tradeoff, and ask the human whether to proceed. -12. Record run-bound dispatch evidence with `./bin/knowledgeos dispatch-task --project-root . --task-id <task-id> --run-id <run-id>`. -13. Record public operational progress with `./bin/knowledgeos trace-step --project-root . --task-id <task-id> --run-id <run-id> --step <step> --note "<public trace>" --evidence "<command/file/user confirmation>"`, and relay the returned `TRACE_OK` marker. -14. Record public phase evidence with `./bin/knowledgeos phase-task --project-root . --task-id <task-id> --run-id <run-id> --phase <route|plan|review|dispatch|execute|report> --status completed --note "<public trace>" --evidence "<command/file/user confirmation>"`, and relay the returned `CHECKPOINT_OK` marker. -15. Record MCP, skill, subagent, orchestrator, or important script use with `./bin/knowledgeos capability-event --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose "<purpose>"`, and relay the returned `CAPABILITY_OK` marker. -16. Verify real side effects with `./bin/knowledgeos artifact-assert --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --path <artifact>` and relay the returned `EFFECT_OK` marker. -17. Run `./bin/knowledgeos eval-task --project-root . --task-id <task-id> --run-id <run-id>`; do not manually append eval status. -18. Run `./bin/knowledgeos verify-context --project-root . --task-id <task-id> --run-id <run-id>`, `./bin/knowledgeos verify-lifecycle --project-root . --task-id <task-id> --run-id <run-id>`, and `./bin/knowledgeos verify-effects --project-root . --task-id <task-id> --run-id <run-id>`; relay the returned `EFFECT_VERIFY_OK` marker before claiming effect verification success. -19. Use `./bin/knowledgeos complete-task --project-root . --task-id <task-id> --run-id <run-id> --summary "<summary>"`; it enforces spec/context/plan, lifecycle, capability visibility, effect verification, and required postflight. -20. If a shared-fabric postflight hook is configured, report `[SYNC_OK]` only after `complete-task` returns `sync_status: SYNC_OK`. -21. For reset requests, run `./bin/knowledgeos reset-project --project-root . --mode <soft|hard> --dry-run` before destructive action. -22. For old-project reorganization requests, run `./bin/knowledgeos migrate-legacy-project --project-root . --write-plan` before moving files. -23. For historical or superseded files that should be stored but not read by default, run `./bin/knowledgeos archive-legacy-project --project-root . --write-plan` before moving files into `archive/`. +5. If the user starts or continues a durable plan/spec conversation, run `./bin/knowledgeos thread-plan current --project-root .` or `./bin/knowledgeos thread-plan start --project-root . --title "<natural language goal>"`; append plain-language progress with `./bin/knowledgeos thread-plan append --project-root . --thread-id <thread-id> --kind <kind> --text "<plain note>"` and relay `THREAD_PLAN_OK`. +6. Select or confirm one task id from `.agent-os/tasks.yaml`. +7. Run `./bin/knowledgeos route-task --project-root . --task-id <task-id>`. +8. Run `./bin/knowledgeos dispatch-task --project-root . --task-id <task-id>` before invoking subagents, MCP tools, skills, workflows, or scripts, and relay the returned `AGENT_DISPATCH_PLAN` marker. +8a. If `dispatch-task` selects `codex-*` or `maestro-*` subagents, run `./bin/knowledgeos subagent-adapter --project-root . --id <subagent-id>` to resolve the Codex runtime call package. Actual delegation must use the Codex runtime subagent tool, then be recorded with `capability-event --kind subagent`. +8b. Preserve every spawned agent id. For substantive work, use one multi-minute `wait_agent` window rather than repeated short polls. If it expires, do not declare failure immediately: send one interrupt requesting current findings, wait once more, and close only after a terminal result. If a late result arrives, append a completed capability event with `recovered_from=timed_out` so `dispatch-report` can reconcile the gap. +8c. A bounded spawned subagent executes the parent-assigned scope directly. It must not create/reopen tasks or specs, run completion/postflight, or recursively dispatch subagents unless the parent explicitly delegated orchestration. The parent Agent owns KnowledgeOS lifecycle and capability evidence. +9. Before planned mutation, run `./bin/knowledgeos check-route-write --project-root . --task-id <task-id> --path <planned-path>`. +10. Create run evidence with `./bin/knowledgeos run-task --project-root . --task-id <task-id>`. +11. Write/update the execution context with `./bin/knowledgeos context-pack --project-root . --task-id <task-id> --run-id <run-id>` and `./bin/knowledgeos plan-task --project-root . --task-id <task-id> --run-id <run-id> --summary "<summary>"`. +12. Pause at consultation checkpoints, state your recommended next move, name the tradeoff, and ask the human whether to proceed. +13. Record run-bound dispatch evidence with `./bin/knowledgeos dispatch-task --project-root . --task-id <task-id> --run-id <run-id>`. +14. Record public operational progress with `./bin/knowledgeos trace-step --project-root . --task-id <task-id> --run-id <run-id> --step <step> --note "<public trace>" --evidence "<command/file/user confirmation>"`, and relay the returned `TRACE_OK` marker. +15. Record public phase evidence with `./bin/knowledgeos phase-task --project-root . --task-id <task-id> --run-id <run-id> --phase <route|plan|review|dispatch|execute|report> --status completed --note "<public trace>" --evidence "<command/file/user confirmation>"`, and relay the returned `CHECKPOINT_OK` marker. +16. Record MCP, skill, plugin/app, browser/Chrome/GitHub/security connector, subagent, orchestrator, shell, file_read, or important script use with `./bin/knowledgeos capability-event --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose "<purpose>"`, and relay the returned `CAPABILITY_OK` marker. +16a. If a planned `codex-*` or `maestro-*` subagent is not used or the runtime times out, record `capability-event --kind subagent --status skipped|timed_out|blocked|close_failed` with the reason before `dispatch-report`. +16b. For an explicit catalog validation task, run `./bin/knowledgeos verify-subagents --project-root . --task-id <task-id> --run-id <run-id>` and relay `SUBAGENT_CATALOG_OK` only when challenge-bound parent attestations cover the immutable run catalog; do not claim independent host verification. +17. Summarize actual capability dispatch with `./bin/knowledgeos dispatch-report --project-root . --task-id <task-id> --run-id <run-id>` and relay the returned `AGENT_DISPATCH_OK` marker. Treat it as a full capability report: agents invoked/skipped, MCP, skills, plugins/apps, browser/Chrome/GitHub/security connectors, scripts, shell, file reads, evidence files, and gaps. If no subagent was used, still report `agents=0` and explain why. +18. Record public decision changes with `./bin/knowledgeos decision-event --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --title "<title>" --summary "<summary>" --reason "<reason>" --evidence "<evidence>"`, and relay the returned `DECISION_OK` marker when plans branch, change, roll back, or abandon a route. +19. Verify real side effects with `./bin/knowledgeos artifact-assert --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --path <artifact>` and relay the returned `EFFECT_OK` marker. +20. Run `./bin/knowledgeos eval-task --project-root . --task-id <task-id> --run-id <run-id>`; do not manually append eval status. +21. Run `./bin/knowledgeos verify-context --project-root . --task-id <task-id> --run-id <run-id>`, `./bin/knowledgeos verify-lifecycle --project-root . --task-id <task-id> --run-id <run-id>`, `./bin/knowledgeos verify-effects --project-root . --task-id <task-id> --run-id <run-id>`, and `./bin/knowledgeos verify-decisions --project-root . --task-id <task-id> --run-id <run-id>`; relay `EFFECT_VERIFY_OK` and `DECISION_VERIFY_OK` before claiming verification success. +22. Use `./bin/knowledgeos complete-task --project-root . --task-id <task-id> --run-id <run-id> --summary "<summary>"`; it enforces spec/context/plan, lifecycle, capability visibility, visible dispatch reporting, effect verification, decision verification, and required postflight. +23. Include the `AGENT_DISPATCH_OK` marker and full capability summary returned by `dispatch-report` or `complete-task` in the final answer for substantial work. +24. For medium, high, or complex tasks, include the returned `FLOW_OK` Mermaid Mission Flow in the final answer; if needed, run `./bin/knowledgeos flow-summary --project-root . --run-id <run-id>`. +25. If a shared-fabric postflight hook is configured, report `[SYNC_OK]` only after `complete-task` returns `sync_status: SYNC_OK`. +26. For reset requests, run `./bin/knowledgeos reset-project --project-root . --mode <soft|hard> --dry-run` before destructive action. +27. For old-project reorganization requests, run `./bin/knowledgeos migrate-legacy-project --project-root . --write-plan` before moving files. +28. For historical or superseded files that should be stored but not read by default, run `./bin/knowledgeos archive-legacy-project --project-root . --write-plan` before moving files into `archive/`. -Never claim boot, route, dispatch, write safety, spec alignment, context pack, plan, trace, checkpoint, capability, effect, eval, completion, or sync success without command evidence. +Never skip the initial `KOS_DECISION`. Never claim boot, route, dispatch, write safety, spec alignment, thread plan, context pack, plan, trace, checkpoint, capability, agent dispatch, decision, effect, eval, completion, flow, or sync success without command evidence. diff --git a/.agent-os/tasks.yaml b/.agent-os/tasks.yaml index ea337dd..d877295 100644 --- a/.agent-os/tasks.yaml +++ b/.agent-os/tasks.yaml @@ -1195,3 +1195,431 @@ tasks: - "harness-audit detects dispatch-task entries that lack --run-id as lifecycle drift" - "harness-audit apply upgrades all stale workflow profiles and doctor passes" - "Project5.5 dry-run audit reports every stale router profile consistently" + - id: KOS-T065 + title: "Add composable HTML sidecar reports" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "html_report_surface" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/executable-control-plane.md" + - "docs/LIVE-REPORT.md" + acceptance: + - "render-html creates receipt and handoff HTML sidecars with source hash metadata" + - "render-html creates self-contained rich reports without remote assets" + - "render-html can compose fragment manifests into one HTML page" + - "doctor continues to trust canonical Markdown YAML and NDJSON evidence" + - id: KOS-T066 + title: "Add capability effect verification gates" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "verifier_gate_regression" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/executable-control-plane.md" + - "docs/LIVE-REPORT.md" + - "docs/route-bound-execution-guard.md" + - "docs/public-release-checklist.md" + - "docs/agent-guide.md" + - ".agent-os/effect-policy.yaml" + - ".agent-os/write-policy.yaml" + - ".agent-os/startup-prompt.md" + - "templates/project-control-plane/.agent-os/effect-policy.yaml" + - "templates/project-control-plane/.agent-os/write-policy.yaml" + - "templates/project-control-plane/.agent-os/startup-prompt.md" + - "templates/project-control-plane/AGENTS.md" + - ".agent-os/specs.yaml" + - ".agent-os/specs/SPEC-20260519-001/" + - ".agent-os/workflows/router.yaml" + - "docs/agentos-architecture.md" + - "docs/knowledgeos-operating-spec.md" + - "docs/quickstart.md" + - "docs/spec-context-plan.md" + - "docs/workflow-router.md" + - "templates/project-control-plane/.agent-os/workflows/router.yaml" + acceptance: + - "effect-policy controls observe warn enforce and off strictness modes" + - "artifact-assert records EFFECT_OK only after touching the real artifact" + - "verify-effects detects missing or forged effect assertions" + - "complete-task enforces or reports effect verification according to policy" + - "doctor and guardrail tests cover the new effect verifier contract" + - id: KOS-T067 + title: "Create HTML summary for capability effect verification changes" + type: "report_task" + status: cancelled + complexity: "low" + risk: "low" + outputs: + - "reports/drafts/capability-effect-verification-summary.md" + - "reports/drafts/capability-effect-verification-summary.html" + - "reports/drafts/capability-effect-verification-summary.manifest.json" + acceptance: + - "summary explains the change set, bug roots, verification evidence, and current status" + - "HTML is generated as a sidecar from Markdown and includes source hash metadata" + - id: KOS-T068 + title: "Create HTML summary for capability effect verification changes" + type: "route_bound_execution_guard" + status: cancelled + complexity: "low" + risk: "low" + outputs: + - ".agent-os/tasks.yaml" + - "reports/drafts/capability-effect-verification-summary.md" + - "reports/drafts/capability-effect-verification-summary.html" + - "reports/drafts/capability-effect-verification-summary.manifest.json" + acceptance: + - "supersede the unroutable report_task intake entry without deleting evidence" + - "summary explains the change set, bug roots, verification evidence, and current status" + - "HTML is generated as a sidecar from Markdown and includes source hash metadata" + - id: KOS-T069 + title: "Create docs HTML summary for capability effect verification changes" + type: "route_bound_execution_guard" + status: completed + complexity: "low" + risk: "low" + outputs: + - "docs/capability-effect-verification-summary.md" + - "docs/capability-effect-verification-summary.html" + - "docs/capability-effect-verification-summary.manifest.json" + acceptance: + - "summary explains the change set, bug roots, verification evidence, and current status" + - "HTML is generated as a sidecar from Markdown and includes source hash metadata" + - id: KOS-T070 + title: "Restore report_task route profile after HTML summary dry run" + type: "route_bound_execution_guard" + status: completed + complexity: "low" + risk: "router_config_regression" + outputs: + - ".agent-os/workflows/router.yaml" + acceptance: + - "doctor passes after cancelled report_task intake entries remain in task history" + - "report_task routes reports docs outputs and knowledge without weakening lifecycle gates" + - id: KOS-T071 + title: "Add explicit effect verification marker" + type: "route_bound_execution_guard" + status: completed + complexity: "low" + risk: "verifier_reporting_regression" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/executable-control-plane.md" + - "docs/agent-guide.md" + - "docs/route-bound-execution-guard.md" + - "docs/public-release-checklist.md" + - "docs/LIVE-REPORT.md" + - ".agent-os/startup-prompt.md" + - "templates/project-control-plane/.agent-os/startup-prompt.md" + - "templates/project-control-plane/AGENTS.md" + acceptance: + - "verify-effects plain output includes EFFECT_VERIFY_OK with status strictness assertion warning and error counts" + - "verify-effects --json includes effect_verify_marker and marker fields" + - "agent guides require relaying EFFECT_VERIFY_OK before claiming effect verification success" + - "tests and doctor pass" + - id: KOS-T072 + title: "Audit staged diff and robustness before GitHub sync" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "guardrail_regression" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/LIVE-REPORT.md" + - "docs/public-release-checklist.md" + acceptance: + - "staged and unstaged diff boundaries are reviewed before commit" + - "full tests and guardrail scenarios pass after any bug fix" + - "bug audit reports root cause reproduction fix and verification" + - id: KOS-T073 + title: "Update changelog and publish staged KnowledgeOS release" + type: "route_bound_execution_guard" + status: completed + complexity: "low" + risk: "release_sync" + outputs: + - "CHANGELOG.md" + acceptance: + - "CHANGELOG documents capability effect verification and HTML sidecar release notes" + - "staged release commit is pushed to GitHub" + - id: KOS-T074 + title: "Add local external-write policy overlay for Antigravity runtime migration" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "normal" + outputs: + - "knowledgeos/cli.py" + acceptance: + - "KnowledgeOS can classify explicitly whitelisted local runtime paths outside the project root without opening broad external write access; local-only policy stays under .knowledgeos-local/; doctor and tests pass." + - id: KOS-T075 + title: "Migrate local Gemini CLI workflow to Antigravity CLI with KnowledgeOS compatibility" + type: "migration_task" + status: completed + complexity: "medium" + risk: "normal" + outputs: + - ".knowledgeos-local/migrations/antigravity-cli-migration-20260527.md" + acceptance: + - "Antigravity CLI installs successfully, existing workflow extensions are reconciled or replaced, and KnowledgeOS boot/sync workflow remains usable." + - id: KOS-T076 + title: "Cleanly uninstall Antigravity CLI and Gemini CLI from local machine" + type: "migration_task" + status: completed + complexity: "medium" + risk: "normal" + outputs: + - ".knowledgeos-local/migrations/cli-uninstall-20260529.md" + acceptance: + - "gemini, agy, and agy-knowledgeos commands are absent; CLI-owned config/cache wrappers are removed or intentionally retained with rationale; verification evidence is recorded" + - id: KOS-T077 + title: "Add Decision Graph module" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "decision_trace_regression" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/decision-graph-module.md" + - "docs/executable-control-plane.md" + - "docs/knowledgeos-operating-spec.md" + - "docs/agent-guide.md" + - "templates/project-control-plane/AGENTS.md" + - "templates/project-control-plane/.agent-os/decision-policy.yaml" + - ".agent-os/decision-policy.yaml" + - ".agent-os/specs.yaml" + - ".agent-os/specs/" + - ".agent-os/tasks.yaml" + acceptance: + - "decision-event records DECISION_OK decision ledger entries with command evidence" + - "decision-query filters decision graph events by run status kind and parent" + - "verify-decisions validates command evidence orphan nodes and unresolved decision branches" + - "render-html decision-map generates self-contained HTML sidecar from decision ledger" + - "complete-task enforces decision verification only when decision-policy strictness is enforce" + - id: KOS-T078 + title: "Add readable completion mission flow" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "normal" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/executable-control-plane.md" + - "docs/knowledgeos-operating-spec.md" + - "docs/agent-guide.md" + - "templates/project-control-plane/AGENTS.md" + - "templates/project-control-plane/.agent-os/startup-prompt.md" + - ".agent-os/startup-prompt.md" + - "CHANGELOG.md" + - "docs/LIVE-REPORT.md" + - ".agent-os/tasks.yaml" + acceptance: + - "flow-summary emits a readable layered Mermaid mission flow with FLOW_OK" + - "render-html --kind mission-flow writes a self-contained HTML sidecar" + - "the feature remains presentation/reporting layer, not kernel" + - "doctor, targeted tests, full tests, scenarios, and smoke pass" + - id: KOS-T079 + title: "Add chat-level Thread Plan Ledger" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "normal" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/executable-control-plane.md" + - "docs/knowledgeos-operating-spec.md" + - "docs/agent-guide.md" + - "docs/thread-plan-ledger.md" + - "docs/public-release-checklist.md" + - "templates/project-control-plane/AGENTS.md" + - "templates/project-control-plane/.agent-os/startup-prompt.md" + - "templates/project-control-plane/.agent-os/write-policy.yaml" + - ".agent-os/startup-prompt.md" + - ".agent-os/write-policy.yaml" + - "CHANGELOG.md" + - "docs/LIVE-REPORT.md" + - ".agent-os/tasks.yaml" + acceptance: + - "thread-plan start creates a chat-level append-only ledger and current thread pointer" + - "thread-plan append, current, link-run, and render work with readable natural language output" + - "thread-plan does not block complete-task or lifecycle verification" + - "markdown, mermaid, and self-contained HTML outputs are generated from source ledger" + - "doctor, tests, guardrail scenarios, smoke, and diff check pass" + - id: KOS-T080 + title: "Audit and harden Thread Plan Ledger before GitHub publish" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "guardrail_regression" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/thread-plan-ledger.md" + - "docs/executable-control-plane.md" + - "docs/knowledgeos-operating-spec.md" + - "docs/agent-guide.md" + - "docs/public-release-checklist.md" + - "templates/project-control-plane/AGENTS.md" + - "templates/project-control-plane/.agent-os/startup-prompt.md" + - "templates/project-control-plane/.agent-os/write-policy.yaml" + - ".agent-os/startup-prompt.md" + - ".agent-os/write-policy.yaml" + - "CHANGELOG.md" + - "docs/LIVE-REPORT.md" + - ".agent-os/tasks.yaml" + acceptance: + - "bug audit reproduces any issue before fixing" + - "thread-plan and lifecycle tests pass" + - "public diff leak scan passes before push" + - "changes are pushed to GitHub branch" + - id: KOS-T081 + title: "Loosen HTML sidecar presentation constraints" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "report_contract_regression" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/executable-control-plane.md" + - "docs/knowledgeos-operating-spec.md" + - "docs/LIVE-REPORT.md" + acceptance: + - "render-html supports default minimal bare and fragment presentation modes" + - "all presentation modes preserve required source metadata and source-of-truth notice" + - "default mode remains backward compatible while minimal and bare avoid forced hero/sidebar layout" + - "tests and doctor pass" + - id: KOS-T082 + title: "Add explicit agent dispatch reporting" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "report_contract_regression" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/executable-control-plane.md" + - "docs/knowledgeos-operating-spec.md" + - "docs/LIVE-REPORT.md" + acceptance: + - "dispatch-task exposes AGENT_DISPATCH_PLAN in text and JSON output" + - "dispatch-report summarizes actual capability events with AGENT_DISPATCH_OK" + - "complete-task returns and receipts agent dispatch summary" + - "targeted tests, doctor, and diff checks pass" + - id: KOS-T083 + title: "Make agent dispatch reporting mandatory in prompts" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "prompt_contract_regression" + outputs: + - "knowledgeos/cli.py" + - "templates/project-control-plane/AGENTS.md" + - "docs/agent-guide.md" + - ".agent-os/startup-prompt.md" + - "docs/LIVE-REPORT.md" + - "tests/test_knowledgeos_cli.py" + acceptance: + - "startup prompt and project template require AGENT_DISPATCH_PLAN and AGENT_DISPATCH_OK reporting" + - "agent guide documents dispatch-report as final visible marker" + - "tests cover the prompt contract" + - id: KOS-T084 + title: "Upgrade dispatch report to full capability decision report" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "prompt_contract_regression" + outputs: + - "knowledgeos/cli.py" + - "templates/project-control-plane/AGENTS.md" + - ".agent-os/startup-prompt.md" + - "docs/agent-guide.md" + - "docs/LIVE-REPORT.md" + - "tests/test_knowledgeos_cli.py" + acceptance: + - "dispatch-report summarizes all capability kinds, skipped agents, evidence, and gaps" + - "startup and template prompts require every conversation to run KnowledgeOS judgment before success claims" + - "tests verify full capability report wording and global prompt contract" + - id: KOS-T085 + title: "Publish latest KnowledgeOS update to GitHub" + type: "route_bound_execution_guard" + status: completed + complexity: "medium" + risk: "release" + outputs: + - ".gitignore" + - "CHANGELOG.md" + - "docs/LIVE-REPORT.md" + - "knowledgeos/cli.py" + - "templates/project-control-plane/AGENTS.md" + - ".agent-os/startup-prompt.md" + - "docs/agent-guide.md" + - "docs/executable-control-plane.md" + - "docs/knowledgeos-operating-spec.md" + - "tests/test_knowledgeos_cli.py" + - ".agent-os/tasks.yaml" + acceptance: + - "local-only files are ignored before staging" + - "security and secret scans pass before commit" + - "latest changes are committed and pushed to GitHub" + - id: KOS-T086 + title: "Register runtime-native and Maestro adapter subagents" + type: "route_bound_execution_guard" + status: completed + complexity: "high" + risk: "capability_orchestration_regression" + outputs: + - ".agent-os/tool-registry.yaml" + - "templates/project-control-plane/.agent-os/tool-registry.yaml" + - "capability-layer/subagents/maestro/manifest.yaml" + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - ".agent-os/startup-prompt.md" + - "templates/project-control-plane/AGENTS.md" + - "docs/agent-guide.md" + - "docs/executable-control-plane.md" + - "docs/capability-orchestration.md" + - "docs/executable-control-plane.md" + - "CHANGELOG.md" + - "docs/LIVE-REPORT.md" + - ".agent-os/tasks.yaml" + acceptance: + - "global and template registries expose codex-default codex-explorer and codex-worker" + - "subagent-adapter emits SUBAGENT_ADAPTER_OK for maestro-architect" + - "dispatch-task limits Maestro candidates and exposes runtime callable metadata" + - "harness-audit can repair old projects missing runtime-native subagents" + - "tests doctor guardrail scenarios smoke and security scans pass" + - id: KOS-T087 + title: "Repair explorer runtime lifecycle and verify all subagents" + type: "route_bound_execution_guard" + status: completed + complexity: "complex" + risk: "runtime_lifecycle_regression" + outputs: + - "knowledgeos/cli.py" + - "tests/test_knowledgeos_cli.py" + - "docs/subagent-runtime-validation.md" + - "docs/subagent-runtime-validation.html" + - "docs/subagent-runtime-validation.fragment.html" + - "docs/subagent-runtime-validation.manifest.json" + - ".agent-os/startup-prompt.md" + - "templates/project-control-plane/.agent-os/startup-prompt.md" + - "templates/project-control-plane/AGENTS.md" + - "docs/agent-guide.md" + - "docs/capability-orchestration.md" + - "CHANGELOG.md" + - "docs/LIVE-REPORT.md" + acceptance: + - "codex-explorer failure is reproduced before any fix" + - "native subagents pass three consecutive live lifecycle smokes" + - "all registered Maestro roles pass an individual live role-contract smoke" + - "runtime gaps remain explicit and never count as completed agents" + - "full tests, doctor, guardrail scenarios, privacy scan, and staged diff review pass" diff --git a/.agent-os/tool-registry.yaml b/.agent-os/tool-registry.yaml index 9096833..c97f626 100644 --- a/.agent-os/tool-registry.yaml +++ b/.agent-os/tool-registry.yaml @@ -32,7 +32,6 @@ tools: execution_mode: ask source_path: external:maestro - - id: maestro-mcp kind: mcp status: enabled @@ -48,469 +47,664 @@ tools: kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,accessibility_specialist,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-accessibility-specialist human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/accessibility_specialist - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-accessibility-specialist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-analytics-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,analytics_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-analytics-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/analytics_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-analytics-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-api-designer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,api_designer,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-api-designer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/api_designer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-api-designer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-architect kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,architect,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-architect human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/architect - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-architect.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-cloud-architect kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,cloud_architect,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-cloud-architect human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/cloud_architect - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-cloud-architect.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-cobol-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,cobol_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-cobol-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/cobol_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-cobol-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-code-reviewer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,code_review capability_fit: maestro_specialist,code_reviewer,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-code-reviewer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/code_reviewer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-code-reviewer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-coder kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,coder,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-coder human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/coder - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-coder.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-compliance-reviewer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,security_audit,code_review capability_fit: maestro_specialist,compliance_reviewer,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-compliance-reviewer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/compliance_reviewer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-compliance-reviewer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-content-strategist kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,content_strategist,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-content-strategist human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/content_strategist - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-content-strategist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-copywriter kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,copywriter,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-copywriter human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/copywriter - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-copywriter.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-data-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,data_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-data-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/data_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-data-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-database-administrator kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,database_administrator,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-database-administrator human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/database_administrator - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-database-administrator.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-db2-dba kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,db2_dba,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-db2-dba human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/db2_dba - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-db2-dba.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-debugger kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,debugger,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-debugger human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/debugger - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-debugger.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-design-system-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,design_system_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-design-system-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/design_system_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-design-system-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-devops-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,devops_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-devops-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/devops_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-devops-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-hlasm-assembler-specialist kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,hlasm_assembler_specialist,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-hlasm-assembler-specialist human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/hlasm_assembler_specialist - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-hlasm-assembler-specialist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-i18n-specialist kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,i18n_specialist,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-i18n-specialist human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/i18n_specialist - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-i18n-specialist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-ibm-i-specialist kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,ibm_i_specialist,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-ibm-i-specialist human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/ibm_i_specialist - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-ibm-i-specialist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-integration-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,integration_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-integration-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/integration_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-integration-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-ml-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,ml_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-ml-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/ml_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-ml-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-mlops-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,mlops_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-mlops-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/mlops_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-mlops-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-mobile-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,mobile_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-mobile-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/mobile_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-mobile-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-observability-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,observability_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-observability-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/observability_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-observability-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-performance-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,performance_engineer,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-performance-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/performance_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-performance-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-platform-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,platform_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-platform-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/platform_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-platform-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-product-manager kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,product_manager,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-product-manager human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/product_manager - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-product-manager.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-prompt-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,prompt_engineer,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-prompt-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/prompt_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-prompt-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-refactor kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,refactor,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-refactor human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/refactor - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-refactor.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-release-manager kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,release_manager,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-release-manager human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/release_manager - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-release-manager.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-security-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,security_audit capability_fit: maestro_specialist,security_engineer,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-security-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/security_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-security-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-seo-specialist kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,seo_specialist,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-seo-specialist human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/seo_specialist - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-seo-specialist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-site-reliability-engineer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,site_reliability_engineer,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-site-reliability-engineer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/site_reliability_engineer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-site-reliability-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-solutions-architect kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,solutions_architect,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-solutions-architect human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/solutions_architect - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-solutions-architect.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-technical-writer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,technical_writer,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-technical-writer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/technical_writer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-technical-writer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-tester kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,testing_task capability_fit: maestro_specialist,tester,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-tester human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/tester - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-tester.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-ux-designer kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,ux_designer,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-ux-designer human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/ux_designer - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-ux-designer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: maestro-zos-sysprog kind: subagent status: recommended scope: maestro-specialist-agent - invocation: via_maestro_mcp_get_agent_then_spawn_agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task capability_fit: maestro_specialist,zos_sysprog,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-zos-sysprog human_gate: true execution_mode: ask - source_path: maestro-mcp:get_agent/zos_sysprog - notes: Maestro specialist agent served through MCP get_agent; dispatch through Codex spawn_agent when delegation is useful. + source_path: capability-layer/subagents/maestro/maestro-zos-sysprog.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. - id: agent-orchestrator kind: orchestrator @@ -632,3 +826,48 @@ tools: human_gate: false execution_mode: ask source_path: templates/governance-core/skills/generated/branch-builder + + - id: codex-default + kind: subagent + status: recommended + scope: codex-native-subagent + invocation: via_codex_runtime_spawn_agent + task_fit: capability_orchestration,knowledge_structuring,report_task,documentation_task + capability_fit: codex_native,default_agent,general_execution + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: default + human_gate: true + execution_mode: ask + source_path: codex-runtime:multi_agent_v1.spawn_agent + notes: Codex native runtime subagent. Dispatch plan generates intent; actual spawn happens through Codex runtime. + + - id: codex-explorer + kind: subagent + status: recommended + scope: codex-native-subagent + invocation: via_codex_runtime_spawn_agent + task_fit: research_task,documentation_task,analysis_task,security_audit,architecture_design + capability_fit: codex_native,exploration,read_only,research + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + human_gate: true + execution_mode: ask + source_path: codex-runtime:multi_agent_v1.spawn_agent + notes: Codex native runtime subagent. Dispatch plan generates intent; actual spawn happens through Codex runtime. + + - id: codex-worker + kind: subagent + status: recommended + scope: codex-native-subagent + invocation: via_codex_runtime_spawn_agent + task_fit: engineering_change,route_bound_execution_guard,executable_control_plane,workflow_routing,tool_registry + capability_fit: codex_native,implementation,read_write,tests + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + human_gate: true + execution_mode: ask + source_path: codex-runtime:multi_agent_v1.spawn_agent + notes: Codex native runtime subagent. Dispatch plan generates intent; actual spawn happens through Codex runtime. diff --git a/.agent-os/workflows/router.yaml b/.agent-os/workflows/router.yaml index b55edbc..bf278f3 100644 --- a/.agent-os/workflows/router.yaml +++ b/.agent-os/workflows/router.yaml @@ -47,6 +47,39 @@ workflows: notes: - Inventory local state without copying secrets into public docs. + migration_task: + route_order: + - doctor --project-root . + - route-task --project-root . --task-id <task-id> + - check-route-write --project-root . --task-id <task-id> --path <planned-path> + - run-task --project-root . --task-id <task-id> + - dispatch-task --project-root . --task-id <task-id> --run-id <run-id> + - context-pack --project-root . --task-id <task-id> --run-id <run-id> + - plan-task --project-root . --task-id <task-id> --run-id <run-id> --summary <summary> + - phase-task --project-root . --task-id <task-id> --run-id <run-id> --phase <phase> --status completed --note <public-trace> --evidence <evidence> + - eval-task --project-root . --task-id <task-id> --run-id <run-id> + - verify-context --project-root . --task-id <task-id> --run-id <run-id> + - verify-lifecycle --project-root . --task-id <task-id> --run-id <run-id> + - verify-effects --project-root . --task-id <task-id> --run-id <run-id> + - complete-task --project-root . --task-id <task-id> --run-id <run-id> --summary <summary> + eval_profile: migration_task + human_gate: explicit_approval + allow_external_controlled: true + allowed_outputs: + - .knowledgeos-local/ + - docs/ + - knowledgeos/ + - tests/ + - global-agent-fabric/ + - .agent-os/tool-registry.yaml + - .agent-os/dispatch-policy.yaml + - .agent-os/workflows/ + - .agent-os/write-policy.yaml + - README.md + notes: + - External runtime writes stay denied by default and must flow through the local external-write overlay. + - Use this route for reversible local migrations such as CLI install/import work, not for broad public template changes. + template_design: route_order: - doctor --root . --project-root templates/project-control-plane --template @@ -230,6 +263,7 @@ workflows: - .agent-os/workspace.yaml - .agent-os/tool-registry.yaml - .agent-os/startup-prompt.md + - .agent-os/decision-policy.yaml - .agent-os/effect-policy.yaml - .agent-os/phase-policy.yaml - .agent-os/read-policy.yaml diff --git a/.agent-os/write-policy.yaml b/.agent-os/write-policy.yaml index 1820a0d..8b2ad39 100644 --- a/.agent-os/write-policy.yaml +++ b/.agent-os/write-policy.yaml @@ -20,6 +20,7 @@ write_policy: - examples/** - templates/** - .agent-os/** + - .knowledgeos-local/** - knowledge/** - src/** - tests/** @@ -40,6 +41,7 @@ write_policy: - .agent-os/dispatch-policy.yaml - .agent-os/tool-registry.yaml - .agent-os/workflows/router.yaml + - .agent-os/decision-policy.yaml - .agent-os/effect-policy.yaml - .agent-os/phase-policy.yaml - .agent-os/fabric-link.yaml @@ -52,8 +54,15 @@ write_policy: - .agent-os/runs/**/command-events.ndjson - .agent-os/runs/**/step-events.ndjson - .agent-os/runs/**/capability-events.ndjson + - .agent-os/runs/**/decision-events.ndjson - .agent-os/runs/**/effect-assertions.ndjson - .agent-os/runs/**/postflight.md + - .agent-os/threads/current.json + - .agent-os/threads/**/thread.json + - .agent-os/threads/**/thread-plan.ndjson + - .agent-os/threads/**/thread-plan.md + - .agent-os/threads/**/thread-map.html + - .agent-os/threads/**/command-events.ndjson - templates/governance-core/** - templates/capability-layer/** @@ -70,3 +79,4 @@ write_policy: - examples/** - templates/** - .agent-os/** + - .knowledgeos-local/** diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3d7bb..111579d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,28 @@ KnowledgeOS is still a working prototype, so versions below describe capability ### Added +- `verify-subagents` now validates challenge-bound parent attestations against an immutable run catalog snapshot and emits `SUBAGENT_CATALOG_OK` only when marker, cleanup, role-contract, catalog-integrity, and native stability checks pass. +- A public Markdown and HTML subagent validation report records the 42-role runtime matrix without exposing local paths or runtime agent ids. +- Full Capability Dispatch Report: `dispatch-report` now treats `AGENT_DISPATCH_OK` as a complete mounted-capability report, not only a subagent summary. +- `capability-event` can record plugin/app, browser, Chrome, GitHub, security connector, MCP, skill, subagent, orchestrator, script, shell, and file-read capability use. +- `KOS_DECISION` prompt contract: every conversation should begin with a visible KnowledgeOS routing judgment covering project state, work class, required flow, and reason. +- `dispatch-report.md` now includes used/skipped counts by capability kind, skipped/not-needed reasons, dispatch plan evidence, evidence file paths, and gaps. +- Codex native runtime subagents: `codex-default`, `codex-explorer`, and `codex-worker` are registered globally and in new project templates. +- Maestro adapter-backed subagents: `maestro-*` role specs now live under `capability-layer/subagents/maestro/` and resolve to Codex `multi_agent_v1.spawn_agent` call packages. +- `subagent-adapter` emits `SUBAGENT_ADAPTER_OK` with runtime tool, runtime agent type, role prompt, and suggested capability-event evidence. +- `runtime-adapters` now reports registered Codex runtime subagents separately from CLI/builtin adapters. +- `dispatch-report` now treats `timed_out`, `blocked`, and `close_failed` subagent records as runtime gaps instead of successful agent invocations. +- HTML sidecar presentation modes: `default`, `minimal`, `bare`, and `fragment`, keeping evidence metadata mandatory while making visual layout project-selectable. +- Decision Graph module for public, auditable decision summaries without expanding the kernel. +- `decision-event` records plan branches, route selections, inserted steps, abandoned branches, rollbacks, deferred work, human decisions, risk tradeoffs, and final decisions into `decision-events.ndjson`, returning `DECISION_OK`. +- `decision-query` and `verify-decisions` make decision trees queryable and command-evidence checked. +- `render-html --kind decision-map` generates a presentation sidecar from the decision ledger while keeping NDJSON as the source of truth. +- `.agent-os/decision-policy.yaml` lets projects choose decision-graph strictness: `warn`, `enforce`, or `off` with a downgrade reason. +- `flow-summary` emits a readable, layered Mermaid Mission Flow with `FLOW_OK` for medium-or-larger task completion reports. +- `render-html --kind mission-flow` generates a self-contained Mission Flow HTML sidecar. +- `complete-task` now prepares `mission-flow.md` and returns `flow_mermaid` for medium, high, or complex tasks so agents can include a clear end-of-task flow diagram. +- Thread Plan Ledger module for chat-window-level, append-only natural-language planning across multiple tasks and runs. +- `thread-plan start/current/append/link-run/render` creates readable `Plan A / Plan B` and `Phase A / Phase B` planning maps, returning `THREAD_PLAN_OK`. - `render-html` for HTML presentation sidecars without replacing Markdown, YAML, or NDJSON as canonical evidence. - Composable HTML report manifests and fragments for receipt, handoff, rich-report, and stitched report outputs. - `.agent-os/effect-policy.yaml` for project-level capability effect verification strictness. @@ -25,6 +47,21 @@ KnowledgeOS is still a working prototype, so versions below describe capability ### Fixed +- Substantive subagent work is no longer treated as failed solely because a short marker-smoke wait elapsed; startup guidance now preserves agent ids, uses a multi-minute wait, performs one interrupt/recovery cycle, and reconciles late results. +- Adapter role prompts now enforce a bounded-subagent runtime boundary so specialist agents do not restart the full KnowledgeOS lifecycle or recursively delegate unless explicitly appointed as orchestrators. +- `dispatch-report` no longer labels safety-policy `blocked` events as runtime failures, separates resolved late-result gaps from active gaps, and reports unique agent ids separately from raw capability-event counts. +- Catalog verification no longer allows duplicate events for one role to compensate for another missing role. +- Catalog verification now rejects catalog drift, malformed marker evidence, missing adapter challenges, and attempts to lower the native stability floor below three; output explicitly states the parent-attested trust boundary. +- Runtime-gap reconciliation now requires a later `completed` event of the same capability kind, id, and purpose, so failed or unrelated events cannot erase a timeout. +- Adapter challenges are now single-use, standard runtime roles remain mandatory even if disabled before the first snapshot, snapshot metadata fails closed, and timeout recovery is one-to-one through `--recovers-event-id`. +- Snapshot metadata now has a complete integrity hash and validated timestamp/evidence model; failed or cancelled subagents are runtime gaps, never successful invocations. +- Prompt templates and generated startup prompts now require `AGENT_DISPATCH_PLAN`, `AGENT_DISPATCH_OK`, and full capability summaries for substantial work. +- `dispatch-task` limits subagent candidates to a small runtime-callable set instead of flooding plans with every Maestro role. +- `harness-audit --apply` can repair old project registries that are missing the three Codex native runtime subagents. +- Runtime subagent smoke failures can now be closed honestly through `capability-event --kind subagent --status timed_out` plus `AGENT_DISPATCH_OK` gap reporting. +- `dispatch-report` now reports `agents=0` with explicit skipped-agent reasons instead of making no-subagent runs look empty. +- `thread-plan render --format html` no longer duplicates the Thread Plan heading inside the report body; the HTML shell keeps the page title and the Markdown fragment keeps the plan title. +- `complete-task` now runs decision verification and blocks forged or structurally invalid decision evidence when project policy is enforced. - `artifact-assert` now rejects nonexistent `capability_event_id` links instead of allowing forged capability-to-effect evidence. - `verify-effects` now rejects existing effect assertions that reference missing capability events. - `run-task` now allocates suffixed run ids when the same task is run more than once within the same second. @@ -32,6 +69,11 @@ KnowledgeOS is still a working prototype, so versions below describe capability - `verify-lifecycle` now blocks completion when dispatch evidence or required capability-stage evidence is missing. - Root and template startup prompts now mention `TRACE_OK`, `CHECKPOINT_OK`, and `CAPABILITY_OK` as separate public evidence channels. +### Verified + +- All 42 registered runtime-callable roles completed strict live role-contract validation; the three native Codex roles each completed three serial smoke rounds. +- The prior explorer timeout was reproduced as a workload-duration issue: explorer and default both exceeded the same fixed wait on substantive review, then returned valid recoverable results. + ## 0.5.0 - System Hardening And Kernel Repair ### Added diff --git a/docs/LIVE-REPORT.md b/docs/LIVE-REPORT.md index 643e237..484671b 100644 --- a/docs/LIVE-REPORT.md +++ b/docs/LIVE-REPORT.md @@ -2,6 +2,68 @@ This file is the real-time construction log for the local KnowledgeOS build. +## 2026-07-11 - Milestone Update: Verified Subagent Runtime Catalog + +Status: implemented and live-validated in KOS-T087 + +Problem: + +- Minimal adapter checks could not prove that every registered role actually ran. +- Repeated capability events could inflate visible agent counts without proving unique catalog coverage. +- A fixed 120-second wait made substantive explorer work look failed even when a valid result arrived later. +- Safety-policy `blocked` events were mixed with timeout and cleanup failures under `runtime_gaps`. + +Changes: + +- Added `verify-subagents` with immutable catalog snapshots, adapter challenges, unique-role/nonces, cleanup, role-contract, and native stability checks. +- Added timeout recovery guidance to startup prompts and agent documentation. +- Added a bounded-subagent role-prompt contract that prevents accidental recursive lifecycle and delegation inside specialist calls. +- Added resolved runtime-gap reconciliation to `dispatch-report`. +- Added single-use adapter challenges, mandatory standard-catalog baselines, fail-closed snapshot metadata, and one-to-one timeout recovery event links. +- Added complete snapshot-integrity validation and explicit failed/cancelled runtime-gap classification. +- Changed dispatch agent counts to unique agent ids while retaining raw event counts separately. +- Added `docs/subagent-runtime-validation.md` and its composable HTML sidecar. + +Verification: + +- `SUBAGENT_CATALOG_OK roles=42/42 native_min=3 invalid=0`. +- Three native Codex roles completed three serial smoke rounds each. +- All 39 Maestro adapter roles completed one live role-contract smoke each. +- Forty-eight catalog invocations completed with strict evidence; three additional diagnostic invocations isolated timeout behavior. +- The substantive explorer review eventually completed and closed; a default control also exceeded 120 seconds and recovered after one interrupt, proving the issue was not explorer-specific. +- A frozen-diff Maestro reviewer reproduced the recursive-dispatch trigger, confirming that parent-level lifecycle rules needed an explicit subagent boundary. +- Active runtime gaps after reconciliation: zero. +- The verifier explicitly reports `evidence_model=parent_attested_runtime` and `host_runtime_verified=false`; it does not overclaim independent access to Codex host tool logs. + +## 2026-07-08 - Milestone Update: Runtime Subagents And Maestro Adapter + +Status: implemented in current branch + +Problem: + +- `dispatch-task` could recommend Maestro specialist roles, but those roles were mostly registry declarations rather than runtime-callable adapter packages. +- New project templates did not expose the three basic Codex runtime subagents. +- Old projects could keep stale registries with no `codex-default`, `codex-explorer`, or `codex-worker`. +- Large Maestro catalogs could make dispatch plans noisy by listing too many candidates. + +Changes: + +- Added global and template registry entries for `codex-default`, `codex-explorer`, and `codex-worker`. +- Converted visible `maestro-*` subagents into adapter-backed entries with `runtime_tool: multi_agent_v1.spawn_agent`. +- Added `capability-layer/subagents/maestro/` role specs and manifest. +- Added `subagent-adapter`, which emits `SUBAGENT_ADAPTER_OK` and returns the runtime call package without spawning from the shell. +- Updated `dispatch-task` to include runtime-callable agent metadata and to limit subagent recommendations to a small ranked candidate set. +- Updated `runtime-adapters` to report registered Codex runtime subagents. +- Extended `dispatch-report` so runtime subagent statuses such as `timed_out`, `blocked`, and `close_failed` become explicit `runtime_gaps` instead of being counted as successful agents. +- Updated `harness-audit --apply` to repair old project registries missing native Codex runtime subagents. +- Updated startup and agent docs to require actual host-runtime subagent calls to be followed by `capability-event --kind subagent` and `dispatch-report`. + +Verification: + +- Targeted unit tests cover registry visibility, dispatch candidate limiting, `subagent-adapter`, runtime adapter reporting, template initialization, and harness repair. +- A regression test covers the Dewey-style timeout path: spawned subagent evidence can be recorded as `timed_out`, then surfaced through `AGENT_DISPATCH_OK` runtime gaps. +- Full validation is recorded in the active KnowledgeOS run for `KOS-T086`. + ## 2026-05-09 - Milestone: Safe Skeleton Status: in progress @@ -658,3 +720,185 @@ Validation: - Reproduced the bogus capability link acceptance in a temporary project, then verified the same command fails with `capability event not found`. - Added targeted regression coverage for bogus capability links, forged effect ledgers, and same-second run id collisions. - Re-ran harness audit, guardrail scenarios, unit tests, smoke, and diff checks after the fix. + +## 2026-05-29 - Module Note: Decision Graph + +Status: implemented locally as an optional module, not a kernel expansion. + +Bug / product gap reproduced: + +- Long research and agent sessions could record what happened through trace steps and lifecycle checkpoints, but they had no first-class public record of why a plan branched, why a route was abandoned, or why a rollback happened. +- Without a decision ledger, the human-readable plan could drift from the executed path, especially when a task inserted new checks or changed route midway. +- Completion could not distinguish between ordinary linear progress and important decision changes that should be auditable. + +Fix: + +- Added `decision-event` to write `.agent-os/runs/<RUN_ID>/decision-events.ndjson` and return `DECISION_OK`. +- Added `decision-query` for filtering decision events by run, task, kind, status, or parent id. +- Added `verify-decisions` to detect forged events, orphan nodes, invalid kinds/statuses, and unexplained abandoned, rollback, or superseded branches. +- Added `.agent-os/decision-policy.yaml` with default `strictness: warn`; `strictness: enforce` makes `complete-task` block on decision verification failure. +- Added `render-html --kind decision-map` to generate a readable HTML sidecar from the decision ledger while keeping NDJSON as source of truth. +- Updated agent guide, startup prompts, operating spec, executable control-plane docs, release checklist, project template, and tests. + +Validation: + +- Reproduced missing CLI behavior before implementation: `decision-event` was an invalid command and completion did not enforce decision verification. +- Added targeted tests for event creation, query, orphan detection, strict policy completion blocking, default warn policy, startup prompt contract, doctor policy validation, and decision-map HTML output. +- Verified `python3 -B -m py_compile knowledgeos/cli.py`. +- Verified `python3 -B -m unittest discover -s tests -v`: 87 tests passed. +- Verified `./examples/scenarios/run_guardrail_scenarios.sh`: 30 checkpoints passed. +- Verified `./bin/knowledgeos doctor --root . --project-root . --summary`: 1789 checks passed. +- Verified `git diff --check`. + +## 2026-05-29 - Reporting Note: Mission Flow + +Status: implemented locally as a presentation/reporting layer, not kernel evidence. + +Product gap reproduced: + +- Medium and complex tasks could finish with strong command evidence but still require the user to mentally assemble what happened from raw markers such as `CHECKPOINT_OK`, `CAPABILITY_OK`, `EFFECT_OK`, `DECISION_OK`, and `[SYNC_OK]`. +- The user-facing closeout needed a readable flow diagram with plain labels, not a dense dump of internal lifecycle terms. + +Fix: + +- Added `flow-summary`, which writes `.agent-os/runs/<RUN_ID>/mission-flow.md` and emits `FLOW_OK`. +- Added `render-html --kind mission-flow`, which creates a self-contained HTML sidecar with colored cards and source metadata. +- Updated `complete-task` so medium, high, and complex tasks return `flow_marker`, `flow_summary_marker`, `flow_mermaid`, and `flow_source`. +- Kept Mission Flow out of kernel enforcement. It summarizes existing evidence lanes for humans and does not replace Markdown, YAML, or NDJSON evidence. +- Updated executable docs, operating spec, agent guide, startup prompts, templates, changelog, and tests. + +Validation: + +- Added targeted tests for `flow-summary`, mission-flow HTML sidecars, and `complete-task` flow output. +- Verified the diagram uses readable labels such as `Goal`, `Health Check`, `Task & Plan`, `Safe Writes`, `Work Done`, `Tools Used`, `Proof`, `Decisions`, and `Finish`. + +## 2026-05-29 - Module Note: Thread Plan Ledger + +Status: implemented locally as a chat-level planning module, not a lifecycle checkpoint. + +Product gap reproduced: + +- Run-level Mission Flow summarizes one completed task but does not preserve the whole chat-window plan as the conversation grows. +- Long research/product conversations need a natural-language version map: Plan A / Plan B, Phase A / Phase B, current route, inserted steps, linked runs, and deferred branches. +- The desired view should be readable by humans and should not add another completion gate. + +Fix: + +- Added `thread-plan start`, `thread-plan current`, `thread-plan append`, `thread-plan link-run`, and `thread-plan render`. +- Added `.agent-os/threads/<THREAD_ID>/thread-plan.ndjson` as the append-only source of truth, with Markdown and HTML sidecars for review. +- Added `THREAD_PLAN_OK` command output for successful thread-plan operations. +- Kept Thread Plan Ledger out of `complete-task`, `verify-lifecycle`, and checkpoint enforcement. +- Updated executable docs, operating spec, agent guide, startup prompts, templates, release checklist, changelog, and tests. + +Validation: + +- Added targeted tests for start/current/append/link-run/render and append-only ledger behavior. +- Verified Markdown includes `Plan A / Plan B`, `Phase A / Phase B`, current working line, and linked run evidence. +- Verified HTML sidecar is self-contained and marks HTML as presentation, not source of truth. + +## 2026-05-30 - Bugfix Note: Thread Plan HTML Heading + +Status: fixed and regression-tested. + +Bug reproduced: + +- `thread-plan render --format html` produced three `<h1>` headings for one thread-plan page. +- Root cause: the HTML document shell already renders a page title, the Thread Plan renderer manually prepended another `<h1>`, and the Markdown fragment converted its own top-level title into a third `<h1>`. + +Fix: + +- Removed the extra hand-written Thread Plan body heading from `render_thread_plan_html`. +- Kept the canonical Markdown title and the outer HTML shell title intact. + +Validation: + +- Reproduced the issue in a temporary project before the fix: `h1_count=3`. +- Re-ran the same reproduction after the fix: `h1_count=2`. +- Added a regression assertion to the Thread Plan Ledger test. + +## 2026-06-08 - HTML Sidecar Presentation Modes + +Status: implemented locally as a lighter presentation contract. + +Product gap reproduced: + +- `render-html --presentation minimal` failed with `unrecognized arguments`, so projects had no supported way to keep KnowledgeOS evidence metadata while opting out of the default hero/sidebar/card layout. +- The original HTML sidecar contract mixed two concerns: required provenance metadata and optional visual styling. + +Fix: + +- Added `render-html --presentation default|minimal|bare|fragment`. +- Kept `default` backward compatible with the existing `knowledgeos-default` card layout. +- Added `minimal` and `bare` shells that preserve source path, source SHA-256, generated time, kind/run metadata, and the source-of-truth notice without forcing hero/sidebar layout. +- Added `fragment` mode for composable report pieces that write only `.fragment.html` and `.manifest.json`. +- Updated manifests with `presentation` and `html_required_metadata`. +- Documented that KnowledgeOS enforces provenance, traceability, and safety, not visual design. + +Validation: + +- Reproduced the missing CLI option before implementation. +- Added a targeted regression test for all four presentation modes. +- Verified `python3 -B -m py_compile knowledgeos/cli.py`. + +## 2026-06-09 - Explicit Agent Dispatch Reporting + +Status: implemented locally as a visible dispatch summary layer. + +Product gap reproduced: + +- `dispatch-task` returned a structured plan but did not emit an obvious `AGENT_DISPATCH_PLAN` marker for user-facing summaries. +- `capability-event` recorded individual MCP, skill, subagent, orchestrator, script, shell, and file-read events, but there was no single run-level report like `[SYNC_OK]`. +- `complete-task` could finish without returning a visible agent/subagent dispatch summary. + +Fix: + +- Added `AGENT_DISPATCH_PLAN` fields to `dispatch-task` JSON and plain output. +- Added `dispatch-report`, which reads `capability-events.ndjson`, writes `dispatch-report.md`, and emits `AGENT_DISPATCH_OK`. +- Updated `complete-task` receipts and JSON output with agent count, capability count, dispatch marker, and report path. + +Validation: + +- Added targeted tests for dispatch plan markers, dispatch-report output, and complete-task dispatch summaries. +- Reproduced all three missing behaviors before implementation. +- Verified the targeted tests pass after implementation. + +## 2026-06-09 - Dispatch Reporting Prompt Contract Sync + +Status: fixed locally as a prompt/template propagation patch. + +Bug reproduced: + +- A downstream agent completed a KnowledgeOS task and reported `[SYNC_OK]`, but did not show `AGENT_DISPATCH_PLAN` or `AGENT_DISPATCH_OK` in the final visible summary. +- Runtime support already existed in `dispatch-task`, `dispatch-report`, and `complete-task`. +- Root cause: project template and guide text did not consistently require agents to relay the dispatch markers, so stale project prompts could silently omit them. + +Fix: + +- Updated the project `AGENTS.md` template to require `AGENT_DISPATCH_PLAN`, `dispatch-report`, and final `AGENT_DISPATCH_OK` reporting. +- Updated the live startup prompt and agent guide with the same visible dispatch contract. +- Added a static prompt-contract regression test so future template drift is caught. + +Validation: + +- Targeted prompt contract tests cover generated prompts and static template/docs/startup files. + +## 2026-06-11 - Full Capability Dispatch Report And KOS Decision Contract + +Status: implemented locally as a stricter visible reporting contract. + +Product gap reproduced: + +- `AGENT_DISPATCH_OK` was too subagent-centric, so runs with `agents=0` could look empty even when shell, file reads, GitHub, browser, security, MCP, skills, or scripts were used. +- Project prompts required dispatch reporting for substantial work but did not require every conversation to start with an explicit KnowledgeOS routing decision. + +Fix: + +- Upgraded `dispatch-report` into a Full Capability Dispatch Report. +- Added used/skipped counts by capability kind, skipped/not-needed records, dispatch plan evidence, evidence file paths, and gaps. +- Extended `capability-event` kinds to cover plugin/app, browser, Chrome, GitHub, and security connectors. +- Updated startup prompt, project template, generated guide, and agent guide to require `KOS_DECISION` at conversation start. +- Clarified that `AGENT_DISPATCH_OK` must summarize all mounted/external capabilities, not only subagents. + +Validation: + +- Added regression coverage for GitHub/browser capability reporting, skipped required subagent reporting, full capability wording, and `KOS_DECISION` prompt contract. diff --git a/docs/agent-guide.md b/docs/agent-guide.md index e91a0fb..51536bc 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -2,6 +2,11 @@ Use this checklist before substantial work in a KnowledgeOS-controlled project. +0. Start every conversation with a visible KnowledgeOS routing judgment. + - Relay `KOS_DECISION` with project state, work class, required flow, and reason. + - Use `answer-only` for simple non-mutating replies, but still state the decision. + - Use `task`, `spec`, `thread-plan`, or `full lifecycle` when work is substantial. + 1. Read the project entry contract. - `AGENTS.md` @@ -11,6 +16,7 @@ Use this checklist before substantial work in a KnowledgeOS-controlled project. - `.agent-os/tasks.yaml` - `.agent-os/specs.yaml` - `.agent-os/phase-policy.yaml` + - `.agent-os/decision-policy.yaml` - `.agent-os/effect-policy.yaml` - `.agent-os/decisions.yaml` - `.agent-os/evals.yaml` @@ -25,10 +31,13 @@ Use this checklist before substantial work in a KnowledgeOS-controlled project. - `knowledgeos check-write --project-root . --path <planned-path>` - `knowledgeos create-spec --project-root . --title "<title>"` when the user asks to create a spec. - `knowledgeos align-spec --project-root . --task-id <task-id>` when the user asks to align with a spec. + - `knowledgeos thread-plan current --project-root .` when continuing a long-lived planning conversation. + - `knowledgeos thread-plan start --project-root . --title "<natural language goal>"` when the user starts a durable plan/spec conversation. 4. Start work through a run envelope. - `knowledgeos route-task --project-root . --task-id <task-id>` - - `knowledgeos dispatch-task --project-root . --task-id <task-id>` + - `knowledgeos dispatch-task --project-root . --task-id <task-id>` and relay `AGENT_DISPATCH_PLAN` + - `knowledgeos subagent-adapter --project-root . --id <subagent-id>` before using a planned `codex-*` or `maestro-*` subagent. - `knowledgeos run-task --project-root . --task-id <task-id>` - `knowledgeos dispatch-task --project-root . --task-id <task-id> --run-id <run-id>` - `knowledgeos context-pack --project-root . --task-id <task-id> --run-id <run-id>` @@ -46,16 +55,34 @@ Use this checklist before substantial work in a KnowledgeOS-controlled project. - Relay the returned `TRACE_OK` marker to the user. - Use `knowledgeos phase-task --project-root . --task-id <task-id> --run-id <run-id> --phase <phase> --status completed --note "<public note>" --evidence "<command or file evidence>"`. - Relay the returned `CHECKPOINT_OK` marker to the user. - - Use `knowledgeos capability-event --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose "<purpose>"` for MCP, skill, subagent, orchestrator, or important script calls. + - Use `knowledgeos capability-event --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose "<purpose>"` for MCP, skill, plugin/app, browser/Chrome/GitHub/security connector, subagent, orchestrator, shell, file_read, or important script calls. - Relay the returned `CAPABILITY_OK` marker to the user. + - When `dispatch-task` recommends `codex-*` or `maestro-*`, the KnowledgeOS CLI only resolves and records the adapter intent. Actual subagent delegation must use the host Codex runtime tool, followed by `capability-event --kind subagent`. + - Preserve every spawned agent id. Substantive review or implementation work should use one multi-minute wait rather than repeated short polling. If the wait expires, request current findings once, wait once more, and close only after a terminal result. A late completion should be appended as `status=completed` with `recovered_from=timed_out`. + - A bounded spawned subagent should execute its assigned scope directly and return to the parent. It must not restart the full KnowledgeOS lifecycle, create/reopen tasks or specs, run completion/postflight, or recursively dispatch subagents unless orchestration was explicitly delegated. The parent owns lifecycle and capability evidence. + - If a planned subagent is not used, record it as `--status skipped` with a public reason before `dispatch-report`. + - If a spawned subagent times out, blocks, or cannot be closed cleanly, record the exact status. `dispatch-report` treats timeout/cleanup failures as runtime gaps, keeps policy blocks visible without mislabeling them as runtime failures, and reconciles an explicitly recovered late result. + - For whole-catalog validation, use `knowledgeos verify-subagents --project-root . --task-id <task-id> --run-id <run-id>`. It requires challenge-bound parent attestations for the immutable run catalog and at least three native-runtime successes; duplicate events cannot fill missing roles. Do not claim independent host verification. + - Use `knowledgeos dispatch-report --project-root . --task-id <task-id> --run-id <run-id>` to summarize actual capability dispatch. + - Relay the returned `AGENT_DISPATCH_OK` marker to the user and include the full capability summary in the final answer for substantial work. + - Treat `AGENT_DISPATCH_OK` as the complete capability report: agents invoked/skipped, MCP, skills, plugins/apps, browser/Chrome/GitHub/security connectors, scripts, shell, file reads, evidence files, and gaps. + - Use `knowledgeos decision-event --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --title "<title>" --summary "<summary>" --reason "<reason>" --evidence "<evidence>"` when a plan branches, changes, rolls back, abandons a route, or records a major human decision. + - Relay the returned `DECISION_OK` marker to the user. + - Use `knowledgeos thread-plan append --project-root . --thread-id <thread-id> --kind <plan|phase|branch|decision|progress|change|summary> --text "<plain note>"` when the chat-level plan changes or advances. + - Relay the returned `THREAD_PLAN_OK` marker when you update the long-lived plan. + - Use `knowledgeos thread-plan link-run --project-root . --thread-id <thread-id> --task-id <task-id> --run-id <run-id>` to connect a run to the chat-level plan. - Use `knowledgeos artifact-assert --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --path <artifact>` to prove real artifact side effects. - Relay the returned `EFFECT_OK` marker to the user. - Use `knowledgeos eval-task --project-root . --task-id <task-id> --run-id <run-id>`; do not hand-write `Status: passed`. - Use `knowledgeos verify-context --project-root . --task-id <task-id> --run-id <run-id>`. - Use `knowledgeos verify-lifecycle --project-root . --task-id <task-id> --run-id <run-id>`. - Use `knowledgeos verify-effects --project-root . --task-id <task-id> --run-id <run-id>` and relay the returned `EFFECT_VERIFY_OK` marker before claiming effect verification success. - - Use `knowledgeos complete-task --project-root . --task-id <task-id> --run-id <run-id> --summary "<summary>"`. - - Do not directly edit `.agent-os/specs.yaml`, `.agent-os/specs/**`, `.agent-os/runs/RUN-*/context-pack.md`, `.agent-os/runs/RUN-*/spec-snapshot.md`, `.agent-os/runs/RUN-*/plan.md`, `.agent-os/runs/RUN-*/eval.md`, `.agent-os/runs/RUN-*/phases.ndjson`, `.agent-os/runs/RUN-*/step-events.ndjson`, `.agent-os/runs/RUN-*/capability-events.ndjson`, `.agent-os/runs/RUN-*/effect-assertions.ndjson`, `.agent-os/runs/RUN-*/command-events.ndjson`, or `.agent-os/runs/RUN-*/receipt.md`. + - Use `knowledgeos verify-decisions --project-root . --task-id <task-id> --run-id <run-id>` and relay the returned `DECISION_VERIFY_OK` marker before claiming decision verification success. + - Use `knowledgeos complete-task --project-root . --task-id <task-id> --run-id <run-id> --summary "<summary>"`; it also returns visible dispatch reporting fields when capability events exist. + - For medium, high, or complex tasks, include the `FLOW_OK` Mermaid Mission Flow returned by `complete-task`; if needed, run `knowledgeos flow-summary --project-root . --run-id <run-id>` and relay `FLOW_OK`. + - Keep the Mission Flow user-facing and readable: prefer labels like `Goal`, `Health Check`, `Task & Plan`, `Safe Writes`, `Work Done`, `Tools Used`, `Proof`, `Decisions`, and `Finish`. + - Do not directly edit `.agent-os/specs.yaml`, `.agent-os/specs/**`, `.agent-os/threads/**`, `.agent-os/runs/RUN-*/context-pack.md`, `.agent-os/runs/RUN-*/spec-snapshot.md`, `.agent-os/runs/RUN-*/plan.md`, `.agent-os/runs/RUN-*/eval.md`, `.agent-os/runs/RUN-*/phases.ndjson`, `.agent-os/runs/RUN-*/step-events.ndjson`, `.agent-os/runs/RUN-*/capability-events.ndjson`, `.agent-os/runs/RUN-*/decision-events.ndjson`, `.agent-os/runs/RUN-*/effect-assertions.ndjson`, `.agent-os/runs/RUN-*/command-events.ndjson`, or `.agent-os/runs/RUN-*/receipt.md`. 7. Finish with the configured sync contract when a shared-fabric kernel module is active. + - Report `AGENT_DISPATCH_OK` only after `dispatch-report` or `complete-task` returns it. - Report `SYNC_OK` only after the postflight command succeeds. diff --git a/docs/capability-orchestration.md b/docs/capability-orchestration.md index 44e28b5..c96bfa3 100644 --- a/docs/capability-orchestration.md +++ b/docs/capability-orchestration.md @@ -111,22 +111,82 @@ An orchestrator and a subagent are related but not identical: KnowledgeOS can register an external orchestrator adapter without copying its runtime into the project. Optional external adapters can remain visible in the registry, but they should not be treated as active subagent catalogs unless their individual agents are discoverable and registered. -The active local specialist-agent catalog is currently **Maestro Orchestrate** (`josstei/maestro-orchestrate`). It exposes 39 specialist agents and a Codex MCP server. Locally, Codex already has `mcp_servers.maestro` configured; KnowledgeOS registers that interface as `maestro-mcp` and registers each specialist as a `maestro-*` `subagent` entry. +KnowledgeOS now exposes two runtime-backed subagent families: -Maestro's Codex runtime resolves agent methodology through MCP: +- `codex-default`, `codex-explorer`, and `codex-worker`: native Codex runtime subagents backed by `multi_agent_v1.spawn_agent`; +- `maestro-*`: Maestro role templates adapted onto the same Codex runtime instead of depending on a live Maestro MCP process. + +The active local specialist-agent catalog is represented under `capability-layer/subagents/maestro/`. It contains 39 role specs such as `maestro-architect`, `maestro-coder`, `maestro-security-engineer`, `maestro-code-reviewer`, and `maestro-tester`. + +The boundary is explicit: ```text -maestro-mcp --> get_runtime_context --> get_agent(["architect", "coder", "security-engineer", ...]) --> spawn_agent(...) when delegation is useful +dispatch-task +-> subagent-adapter +-> Codex runtime spawn_agent +-> capability-event --kind subagent +-> dispatch-report ``` -That means the MCP server retrieves the agent methodology and runtime context; the actual subagent execution still goes through Codex delegation (`spawn_agent`) or the host runtime's equivalent. KnowledgeOS records both surfaces: +KnowledgeOS does not spawn subagents from the shell. It registers intent, resolves the runtime call package, requires public evidence, and verifies the report. The host Codex runtime performs actual delegation. + +KnowledgeOS records these surfaces: - `maestro`: active orchestrator layer; -- `maestro-mcp`: MCP interface for runtime context, skill content, plan validation, and agent methodology; -- `maestro-*`: 39 visible specialist subagent entries, including `maestro-architect`, `maestro-coder`, `maestro-security-engineer`, `maestro-code-reviewer`, and `maestro-tester`. +- `maestro-mcp`: optional MCP interface for runtime context, skill content, plan validation, and agent methodology; +- `codex-*`: three native Codex runtime subagents; +- `maestro-*`: 39 adapter-backed specialist subagent entries. + +`dispatch-task` limits subagent recommendations to a small candidate set, usually at most three. This avoids noisy plans that list every specialist role. + +Use `subagent-adapter` to resolve the callable package: + +```bash +./bin/knowledgeos subagent-adapter \ + --project-root /path/to/project \ + --id maestro-architect \ + --task-id T001 \ + --run-id RUN-... \ + --purpose "Review architecture risks before implementation." +``` + +The command emits `SUBAGENT_ADAPTER_OK` with `runtime_tool`, `runtime_agent_type`, role prompt, and a suggested `capability-event`. It is still read-only with respect to actual agent execution. + +Keep the agent id returned by `spawn_agent`. A short marker smoke and a substantive review have different latency profiles: use one multi-minute wait for real work rather than repeated short polls. If the wait expires, request current findings once, wait once more, and close only after a terminal result. When a late result arrives, append a completed event with `recovered_from=timed_out`; `dispatch-report` then separates resolved history from active runtime gaps. + +Adapter role prompts also carry a subagent runtime boundary. A bounded specialist works directly on the parent-assigned scope and returns findings; it does not restart the project lifecycle or recursively dispatch another agent unless the parent explicitly delegated orchestration. This prevents parent-level KnowledgeOS rules from creating an accidental delegation tree inside every specialist call. + +If the host runtime accepts a subagent but the agent later times out, blocks, or cannot be closed cleanly, record that truth directly: + +```bash +./bin/knowledgeos capability-event \ + --project-root /path/to/project \ + --task-id T001 \ + --run-id RUN-... \ + --kind subagent \ + --id codex-explorer \ + --status timed_out \ + --purpose "Runtime smoke did not return before timeout." \ + --evidence "spawn_agent returned an id; wait_agent timed out" +``` + +`dispatch-report` will surface that as a runtime gap instead of counting it as a successful agent. + +For an explicit whole-catalog validation, strict evidence can be checked without trusting raw event counts: + +```bash +./bin/knowledgeos verify-subagents \ + --project-root /path/to/project \ + --task-id T001 \ + --run-id RUN-... \ + --native-min-successes 3 +``` + +The command emits `SUBAGENT_CATALOG_OK` only when every role in the run-level catalog snapshot has a unique successful nonce with `cleanup=completed`, `role_contract=passed`, and a matching adapter challenge. Repeated evidence for one role cannot compensate for another missing role, catalog drift fails verification, and the native stability floor cannot be lowered below three. + +Each adapter challenge is single-use. This is a parent-attested runtime proof: KnowledgeOS does not have direct access to the Codex host's private tool-call log, so output also reports `host_runtime_verified: false` instead of claiming an independent host signature. + +When a late completion resolves a timeout, record the exact timeout capability-event id with `capability-event --recovers-event-id <CAP-...>`. Recovery is one-to-one; matching text alone cannot clear a runtime gap. ComposioHQ Agent Orchestrator remains an optional external worktree/PR orchestration adapter. It is not the active source of the 39 specialist agents. @@ -177,6 +237,9 @@ This is intentionally a product feature: KnowledgeOS should make agents more tho Capability orchestration tests verify that: - `dispatch-task` returns a dispatch-ready plan for `KOS-T009`; +- Codex native subagents and Maestro adapter-backed subagents are visible in new project templates; +- `subagent-adapter` resolves `maestro-architect` into a Codex runtime call package; +- dispatch limits subagent candidates instead of flooding plans with every Maestro role; - Branch Builder is prioritized before orchestration; - orchestration appears before lower-level capability use; - consultation checkpoints include execution and completion; diff --git a/docs/decision-graph-module.md b/docs/decision-graph-module.md new file mode 100644 index 0000000..0105537 --- /dev/null +++ b/docs/decision-graph-module.md @@ -0,0 +1,98 @@ +# KnowledgeOS Decision Graph Module + +Decision Graph is an optional KnowledgeOS module for public, auditable decision summaries. It records how a task plan branches, changes, rolls back, abandons a route, or reaches a final decision. + +It is not a hidden chain-of-thought store. It saves concise natural-language decisions that a human can read later. + +## Why It Exists + +Research and long-running agent work are rarely pure automation. A task may start with one plan, insert a new check, abandon a later branch, or roll back after evidence changes. Linear traces say what happened; Decision Graph records why the path changed. + +## Evidence Lane + +```text +File: .agent-os/runs/<RUN_ID>/decision-events.ndjson +Command: decision-event +Marker: DECISION_OK +Verifier: verify-decisions +Verify Marker: DECISION_VERIFY_OK +HTML: render-html --kind decision-map +``` + +Decision Graph complements the existing lanes: + +```text +trace-step what happened +phase-task which lifecycle checkpoint passed +capability-event which tool or agent capability was used +artifact-assert which real side effect landed +decision-event why the route changed or was selected +``` + +## Commands + +Record a public decision: + +```bash +knowledgeos decision-event \ + --project-root . \ + --task-id T001 \ + --run-id RUN-... \ + --kind branch_selected \ + --status selected \ + --title "Use targeted rerun" \ + --summary "Select the smaller validation path." \ + --reason "It proves the changed artifact without repeating expensive work." \ + --evidence "plan review" +``` + +Query the graph: + +```bash +knowledgeos decision-query --project-root . --run-id RUN-... --parent-id DEC-... +``` + +Verify command-generated decision evidence: + +```bash +knowledgeos verify-decisions --project-root . --task-id T001 --run-id RUN-... +``` + +Render a human-readable sidecar: + +```bash +knowledgeos render-html --project-root . --run-id RUN-... --kind decision-map +``` + +## Policy + +Project policy lives at `.agent-os/decision-policy.yaml`: + +```yaml +decision_policy: + strictness: warn + downgrade_reason: "" +``` + +Supported strictness values: + +- `warn`: default; missing decision events warn but do not block completion. +- `enforce`: completion fails when decision evidence is missing or invalid. +- `off`: allowed only with a `downgrade_reason`. + +## When To Use + +Use `decision-event` when one of these happens: + +- a plan branches; +- a route is selected among alternatives; +- a new step is inserted mid-task; +- a branch is abandoned, deferred, superseded, or rolled back; +- the human makes a meaningful decision; +- a risk tradeoff changes execution. + +Do not use it for ordinary linear progress. Use `trace-step` for that. + +## Module Boundary + +Decision Graph is a module, not kernel. The kernel remains small: route, write guard, lifecycle, eval, effect verification, and completion. Decision Graph can be enforced by project policy when a research, strict, or release project needs stronger auditability. diff --git a/docs/executable-control-plane.md b/docs/executable-control-plane.md index 4e3af06..54e8bb7 100644 --- a/docs/executable-control-plane.md +++ b/docs/executable-control-plane.md @@ -64,7 +64,8 @@ Checks include: - write-policy coverage; - capability guardrails; - workflow route coverage; -- lifecycle consistency for `run-task -> context-pack -> plan-task -> phase-task -> eval-task -> verify-context -> verify-lifecycle -> verify-effects -> complete-task`. +- lifecycle consistency for `run-task -> context-pack -> plan-task -> phase-task -> eval-task -> verify-context -> verify-lifecycle -> verify-effects -> complete-task`; +- decision policy validation when `.agent-os/decision-policy.yaml` is present. ### `init-project` @@ -148,6 +149,48 @@ Align the active or selected spec with the current task before execution. The command writes `alignment.md` and returns `aligned` or `needs_review`. Agents should stop for human triage when alignment needs review. +### `thread-plan` + +Manage a chat-level, append-only natural-language plan. Use this when a conversation starts a durable plan/spec or when later rounds should build on an earlier planning path. + +```bash +./bin/knowledgeos thread-plan start \ + --project-root /path/to/project \ + --title "长期维护鸟类声景基金申请计划" \ + --spec-id SPEC-... +``` + +Append progress, branch, phase, decision, change, or summary notes: + +```bash +./bin/knowledgeos thread-plan append \ + --project-root /path/to/project \ + --thread-id THREAD-... \ + --kind phase \ + --text "Phase A:把聊天级计划记录清楚;Phase B:再把多个任务串起来。" +``` + +Link a run back to the long-lived conversation: + +```bash +./bin/knowledgeos thread-plan link-run \ + --project-root /path/to/project \ + --thread-id THREAD-... \ + --task-id T001 \ + --run-id RUN-... +``` + +Render the plan as Markdown, Mermaid, or HTML: + +```bash +./bin/knowledgeos thread-plan render \ + --project-root /path/to/project \ + --thread-id THREAD-... \ + --format html +``` + +Successful output begins with `THREAD_PLAN_OK`. This module does not gate completion. It is a readable planning map across a chat window, while `plan-task` remains the run-level execution plan. + ### `check-write` Classify a planned write against `.agent-os/write-policy.yaml`. @@ -321,7 +364,7 @@ Record an observable capability call without executing it. --purpose "Coordinate specialist review before execution." ``` -Allowed kinds are `mcp`, `skill`, `subagent`, `orchestrator`, `script`, `shell`, and `file_read`. +Allowed kinds are `app`, `browser`, `chrome`, `file_read`, `github`, `mcp`, `plugin`, `script`, `security`, `shell`, `skill`, `subagent`, and `orchestrator`. The command writes `.agent-os/runs/<RUN_ID>/capability-events.ndjson` and matching command evidence. Successful plain-text output begins with: @@ -331,6 +374,42 @@ CAPABILITY_OK kind=<kind> id=<capability-id> purpose=<short purpose> JSON output also includes a stable `capability_event_id`. Use that id when an effect assertion proves the real side effect produced by the capability. If `artifact-assert` is called with `--capability-event-id`, the id must already exist in the run's `capability-events.ndjson`; bogus links are rejected. +### `decision-event` + +Record a public Decision Graph event when a plan branches, a route is selected, a step is inserted, or a branch is abandoned, rolled back, superseded, deferred, or finalized. + +```bash +./bin/knowledgeos decision-event \ + --project-root /path/to/project \ + --task-id T001 \ + --run-id RUN-... \ + --kind branch_selected \ + --status selected \ + --title "Use targeted rerun" \ + --summary "Select the smaller validation path." \ + --reason "It proves the changed artifact without repeating expensive work." \ + --evidence "plan review" +``` + +The command writes `.agent-os/runs/<RUN_ID>/decision-events.ndjson` and matching command evidence. Plain-text output begins with: + +```text +DECISION_OK kind=<kind> status=<status> title=<short title> +``` + +Use `decision-event` for public decision summaries, not hidden chain-of-thought. Ordinary linear progress should stay in `trace-step`. + +### `decision-query` + +Query Decision Graph events by run, task, kind, status, or parent id. + +```bash +./bin/knowledgeos decision-query \ + --project-root /path/to/project \ + --run-id RUN-... \ + --parent-id DEC-... +``` + ### `artifact-assert` Verify a real artifact side effect before recording `EFFECT_OK`. @@ -366,6 +445,49 @@ Failed assertions return non-zero and do not write a passing effect record. Asse --run-id RUN-... ``` +Successful dispatch output includes `AGENT_DISPATCH_PLAN`. The dispatch summary now reports declared agents and runtime-callable agents separately, so a plan can distinguish "registered" from "actually callable through Codex runtime". + +### `subagent-adapter` + +Resolve a registered subagent into a Codex runtime call package. + +```bash +./bin/knowledgeos subagent-adapter \ + --project-root /path/to/project \ + --id maestro-architect \ + --task-id T001 \ + --run-id RUN-... \ + --purpose "Review architecture risks." +``` + +The command emits: + +```text +SUBAGENT_ADAPTER_OK id=maestro-architect runtime_agent_type=explorer +``` + +It returns `runtime_tool: multi_agent_v1.spawn_agent`, the runtime agent type, the role prompt, and a suggested `capability-event`. It does not execute the subagent itself; actual delegation belongs to the host Codex runtime. After delegation, record the real use with `capability-event --kind subagent`. + +### `verify-subagents` + +Verify that strict, challenge-bound parent attestations cover the immutable run catalog instead of trusting current registry entries or raw event counts. + +```bash +./bin/knowledgeos verify-subagents \ + --project-root /path/to/project \ + --task-id T001 \ + --run-id RUN-... \ + --native-min-successes 3 +``` + +A successful run emits: + +```text +SUBAGENT_CATALOG_OK roles=42/42 native_min=3 invalid=0 +``` + +Each covered role needs a unique smoke nonce, matching registered role id, `cleanup=completed`, `role_contract=passed`, and a single-use challenge returned by `subagent-adapter`. Duplicate events cannot replace a missing role, the standard native/Maestro catalog cannot be removed before snapshot, catalog drift is rejected, and native Codex roles cannot use a minimum below three. The result reports `evidence_model: parent_attested_runtime` and `host_runtime_verified: false` because the CLI cannot independently inspect Codex host tool logs. + ### `verify-lifecycle` Verify that a run has all phases required by `.agent-os/phase-policy.yaml`. @@ -404,9 +526,74 @@ EFFECT_VERIFY_OK status=<passed|warning|failed|disabled> strictness=<level> asse JSON output includes `effect_verify_marker: EFFECT_VERIFY_OK` and the full `marker` string. Agents must relay this marker before claiming effect verification success. +### `verify-decisions` + +Verify that Decision Graph evidence is command-generated and structurally valid. + +```bash +./bin/knowledgeos verify-decisions \ + --project-root /path/to/project \ + --task-id T001 \ + --run-id RUN-... +``` + +The project policy lives in `.agent-os/decision-policy.yaml`. `strictness: warn` is the default and does not block linear work with no decision events. `strictness: enforce` blocks completion when decision evidence is missing, forged, orphaned, or invalid. `strictness: off` requires a `downgrade_reason`. + +Plain output includes: + +```text +DECISION_VERIFY_OK status=<passed|warning|failed|disabled> strictness=<level> decisions=<n> warnings=<n> errors=<n> +``` + +### `render-html --kind decision-map` + +Render `.agent-os/runs/<RUN_ID>/decision-events.ndjson` into a human-readable static decision map. + +```bash +./bin/knowledgeos render-html \ + --project-root /path/to/project \ + --run-id RUN-... \ + --kind decision-map +``` + +The HTML includes source path, source SHA-256, run id, generated time, and the notice `HTML is presentation, not source of truth.` + +### `flow-summary` + +Print a friendly, layered Mermaid mission flow for a run. This is designed for human end-of-task reporting, not machine enforcement. + +```bash +./bin/knowledgeos flow-summary \ + --project-root /path/to/project \ + --run-id RUN-... +``` + +Plain output begins with: + +```text +FLOW_OK run=<run-id> stages=<n> synced=<yes|no> +``` + +The diagram uses simple labels: `Goal`, `Health Check`, `Task & Plan`, `Safe Writes`, `Work Done`, `Tools Used`, `Proof`, `Decisions`, and `Finish`. It intentionally avoids exposing internal jargon as the main user-facing surface. + +For medium, high, or complex tasks, `complete-task` also writes `.agent-os/runs/<RUN_ID>/mission-flow.md` and returns `flow_marker`, `flow_summary_marker`, and `flow_mermaid` so the agent can include the readable flow in its final answer. + +### `render-html --kind mission-flow` + +Render `.agent-os/runs/<RUN_ID>/mission-flow.md` into a self-contained HTML sidecar with colored cards and source metadata. + +```bash +./bin/knowledgeos render-html \ + --project-root /path/to/project \ + --run-id RUN-... \ + --kind mission-flow +``` + +The HTML is presentation only. Markdown, YAML, and NDJSON remain the source of truth. + ### `complete-task` -Close a task only after `eval-task` passed, declared outputs exist, context verification passes, lifecycle verification passes, effect verification passes or explicitly downgrades, and required postflight succeeds or records an explicit pending reason. +Close a task only after `eval-task` passed, declared outputs exist, context verification passes, lifecycle verification passes, effect verification passes or explicitly downgrades, decision verification passes or warns according to policy, and required postflight succeeds or records an explicit pending reason. ```bash ./bin/knowledgeos complete-task \ @@ -418,10 +605,45 @@ Close a task only after `eval-task` passed, declared outputs exist, context veri Manual `Status: passed` text is not enough unless an explicit override is used. -Before postflight, `complete-task` runs `verify-effects`. It blocks on failed effect verification and records effect status, the `EFFECT_VERIFY_OK` marker, warnings, or explicit strictness downgrades in the receipt. +Before postflight, `complete-task` runs `verify-effects` and `verify-decisions`. It blocks on failed verification and records status, visible markers, warnings, or explicit strictness downgrades in the receipt. + +For medium, high, or complex tasks, `complete-task` also prepares a readable Mission Flow summary and returns `FLOW_OK` fields. Agents should include that Mermaid flow in the final user-facing answer unless the user explicitly asks for a terse response. + +`complete-task` also writes a dispatch report from the recorded `capability-event` ledger and returns `AGENT_DISPATCH_OK` fields. This makes all mounted capabilities visible in the final response without trusting hidden runtime claims, including agents invoked or skipped, MCP, skills, plugins/apps, browser/Chrome/GitHub/security connectors, scripts, shell, and file reads. If `.agent-os/fabric-link.yaml` sets `postflight_required: true`, `complete-task` runs the configured shared-fabric `after-task.sh` and only reports `sync_status: SYNC_OK` when the hook emits `[SYNC_OK]`. Use `--allow-pending-postflight "<reason>"` only as an explicit, receipt-recorded escape hatch. +### `dispatch-report` + +Summarize actual capability events recorded for a run: + +```bash +./bin/knowledgeos dispatch-report \ + --project-root /path/to/project \ + --task-id T001 \ + --run-id RUN-... +``` + +The command writes: + +```text +.agent-os/runs/<RUN_ID>/dispatch-report.md +``` + +It emits `AGENT_DISPATCH_OK agents=<n> capabilities=<n> run=<RUN_ID>`. + +The generated report is a **Full Capability Dispatch Report**. It includes: + +- used and skipped counts by capability kind; +- agents invoked and agents skipped; +- MCP, skills, plugins/apps, browser/Chrome/GitHub/security connectors, scripts, shell, and file reads; +- skipped/not-needed reasons; +- dispatch plan evidence; +- capability and command ledger paths; +- gaps, such as required stages without a capability event or skip reason. + +Use `dispatch-task` for the planned route (`AGENT_DISPATCH_PLAN`) and `dispatch-report` for what was actually registered through `capability-event`. + ### `render-html` Render canonical Markdown evidence into static, composable HTML sidecars for human review. @@ -441,11 +663,39 @@ Render canonical Markdown evidence into static, composable HTML sidecars for hum --project-root /path/to/project \ --input reports/drafts/x.md \ --kind rich-report \ - --output reports/drafts/x.html + --output reports/drafts/x.html \ + --presentation minimal + +./bin/knowledgeos render-html \ + --project-root /path/to/project \ + --run-id RUN-... \ + --kind decision-map + +./bin/knowledgeos render-html \ + --project-root /path/to/project \ + --run-id RUN-... \ + --kind mission-flow ``` HTML is presentation only. Markdown, YAML, and NDJSON remain the source of truth. Generated pages include source path, source SHA-256, generated time, run id when available, and the notice `HTML is presentation, not source of truth.` +The OS only enforces the evidence metadata contract. It does not require a single visual layout. Presentation modes: + +- `default`: the original KnowledgeOS card layout with hero, panel, and colored report shell; +- `minimal`: readable document body with evidence metadata footer, without hero/sidebar layout; +- `bare`: very small HTML shell and metadata footer for project-owned styling; +- `fragment`: reusable fragment plus manifest only, without writing a full document shell. + +Projects may set an optional default: + +```yaml +reporting: + html_sidecars: true + html_source_of_truth: false + html_presentation_default: minimal + html_required_metadata: true +``` + Each render writes: ```text @@ -454,6 +704,8 @@ Each render writes: <output>.manifest.json ``` +When `--presentation fragment` is used, only `<output>.fragment.html` and `<output>.manifest.json` are written. The manifest records an empty `output` field because no full HTML document was generated. + Use the fragment and manifest for composition: ```bash diff --git a/docs/knowledgeos-operating-spec.md b/docs/knowledgeos-operating-spec.md index b816f34..bc458b9 100644 --- a/docs/knowledgeos-operating-spec.md +++ b/docs/knowledgeos-operating-spec.md @@ -44,7 +44,11 @@ Modules are invoked by task intent, route policy, or project profile. Current and planned modules include: - `spec`: durable user intent, context pack, plan, and drift checks; +- `thread-plan`: chat-level natural-language plan history across multiple tasks and runs; - `capability`: dispatch policy, tool registry, and capability events; +- `decision-graph`: public decision summaries, plan branches, abandoned routes, rollbacks, and decision-map HTML; +- `mission-flow`: readable completion flow summaries and HTML sidecars for medium or larger tasks; +- `html-sidecar`: presentation-only report rendering where provenance and safety are enforced but visual layout is project-selectable; - `archive`: cold storage for old or superseded project content; - `migration`: old-project reorganization plans; - `harness`: cross-project audit and repair; @@ -61,6 +65,7 @@ The Workbench should read: - `.agent-os/tasks.yaml`; - `.agent-os/runs/`; - `.agent-os/specs/`; +- `.agent-os/threads/`; - receipts and handoffs; - evidence lanes. @@ -83,14 +88,14 @@ Recommended profiles: - `minimal`: kernel gates only; - `standard`: kernel plus context, plan, checkpoint, and capability visibility; - `strict`: standard plus stronger spec and release checks; -- `research`: strict spec/context tracking and artifact discipline; +- `research`: strict spec/context tracking, artifact discipline, and decision graph visibility; - `release`: strict verification, security review, and postflight requirements. Checkpoint reporting should remain mandatory for substantial managed work even when optional modules are disabled. ## Evidence Lanes -KnowledgeOS separates evidence into four lanes. +KnowledgeOS separates evidence into auditable lanes. ### Public Operational Trace @@ -136,6 +141,45 @@ This lane records visible use of: Required dispatch stages must be recorded or explicitly skipped with a public reason. +### Agent Dispatch Report + +```text +File: .agent-os/runs/<RUN_ID>/dispatch-report.md +Command: dispatch-report +Markers: AGENT_DISPATCH_PLAN / AGENT_DISPATCH_OK +``` + +`dispatch-task` exposes the planned agent, subagent, orchestrator, and capability routing with `AGENT_DISPATCH_PLAN`. + +`dispatch-report` summarizes the actual `capability-event` ledger with `AGENT_DISPATCH_OK` as a Full Capability Dispatch Report. It covers all mounted or external capabilities, including agents invoked/skipped, MCP, skills, plugins/apps, browser/Chrome/GitHub/security connectors, scripts, shell, file reads, evidence files, and gaps. It is a visibility report, not a replacement for the capability ledger. + +### Decision Graph + +```text +File: .agent-os/runs/<RUN_ID>/decision-events.ndjson +Command: decision-event +Marker: DECISION_OK +Verifier: verify-decisions +Verify Marker: DECISION_VERIFY_OK +``` + +This module lane records public, human-readable decision summaries: plan branches, selected routes, inserted steps, abandoned branches, rollbacks, superseded paths, deferred work, human decisions, risk tradeoffs, and final decisions. It is not hidden chain-of-thought. + +Project policy lives at `.agent-os/decision-policy.yaml`. Default strictness is `warn`; `enforce` blocks completion on invalid or missing decision evidence; `off` requires a downgrade reason. + +### Thread Plan Ledger + +```text +File: .agent-os/threads/<THREAD_ID>/thread-plan.ndjson +Command: thread-plan +Marker: THREAD_PLAN_OK +HTML: thread-plan render --format html +``` + +This module lane records a long-lived chat-window plan in natural language. It can show `Plan A / Plan B`, `Phase A / Phase B`, current progress, abandoned or delayed routes, and linked task runs. It is append-only and human-readable. + +Thread Plan Ledger is not a checkpoint and does not block completion. It complements `plan-task`: `plan-task` is the run-level execution plan; `thread-plan` is the multi-turn conversation map. + ### Completion And Sync ```text @@ -146,6 +190,39 @@ Marker: SYNC_OK This lane proves the task closed through the completion gate and postflight contract. +### Completion Mission Flow + +```text +File: .agent-os/runs/<RUN_ID>/mission-flow.md +Command: flow-summary +Marker: FLOW_OK +HTML: render-html --kind mission-flow +``` + +This presentation lane gives humans a readable, layered flow at the end of medium, high, or complex tasks. It uses plain labels such as `Goal`, `Health Check`, `Task & Plan`, `Safe Writes`, `Work Done`, `Tools Used`, `Proof`, `Decisions`, and `Finish`. + +Mission Flow is not kernel evidence. It summarizes existing evidence lanes and should remain visually clear instead of exposing raw internal terminology. + +### HTML Sidecars + +```text +Command: render-html +Modes: default / minimal / bare / fragment +``` + +HTML sidecars are presentation artifacts. The source of truth remains Markdown, YAML, and NDJSON. + +KnowledgeOS enforces only the evidence metadata contract: + +- source path; +- source SHA-256; +- generated time; +- kind and run/thread/spec association when available; +- `HTML is presentation, not source of truth.`; +- no remote scripts, remote fonts, or CDN dependencies by default. + +KnowledgeOS does not enforce a single visual language. Projects and apps may choose the `default`, `minimal`, `bare`, or `fragment` presentation mode as long as the evidence metadata contract remains intact. + ## Full Task Chain A substantial managed task should follow this chain: @@ -156,6 +233,7 @@ User Intent -> Doctor Gate -> Task Intake -> Spec Alignment +-> Thread Plan -> Route Guard -> Dispatch Plan -> Write Guard @@ -164,14 +242,16 @@ User Intent -> Plan Task -> Execution -> Capability Visibility +-> Decision Graph -> Phase Checkpoints -> Eval -> Verify -> Complete -> Sync +-> Mission Flow ``` -`trace-step` explains the operational path. `phase-task` proves the lifecycle checkpoints. `capability-event` proves tool and agent visibility. `complete-task` proves closure and sync. +`trace-step` explains the operational path. `phase-task` proves the lifecycle checkpoints. `capability-event` proves tool and agent visibility. `decision-event` explains public decision changes. `thread-plan` preserves the long-lived natural-language plan across the chat window. `complete-task` proves closure and sync. `flow-summary` turns one run into a readable end-of-task map for humans. ## Doctor And Repair Tools diff --git a/docs/migration-boundary.md b/docs/migration-boundary.md index 78fdc2c..ca9d101 100644 --- a/docs/migration-boundary.md +++ b/docs/migration-boundary.md @@ -20,6 +20,20 @@ The local track lives under `.knowledgeos-local/` and can reference the current This track is for migration planning only. It should not be published as part of the clean KnowledgeOS release. +For runtime migrations that must touch exact machine-local targets outside the repo, keep the allowlist in `.knowledgeos-local/write-policy.local.yaml`. + +That file is a local overlay, not part of the public control plane. It may contain sections such as: + +- `external_controlled` +- `external_forbidden_without_human_gate` +- `external_require_receipt_for` + +External writes remain denied by default. They become legal only when all three conditions are true: + +1. the path is listed in the local overlay; +2. the active task route explicitly sets `allow_external_controlled: true`; +3. normal KnowledgeOS route, run, eval, and lifecycle evidence still complete. + ## Reorganization Boundary `migrate-legacy-project` is the safe entry point for old project folders. It creates a reviewable plan first and only moves confidently classified top-level entries with `--apply`. diff --git a/docs/public-release-checklist.md b/docs/public-release-checklist.md index c9e7fdc..493cc08 100644 --- a/docs/public-release-checklist.md +++ b/docs/public-release-checklist.md @@ -12,17 +12,25 @@ Before publishing KnowledgeOS, verify: - Any future app is described as a workbench that consumes receipts/wiki/graph outputs; - `archive/**` is documented as cold storage, not default context. - `create-spec` and `align-spec` work for durable user intent; +- `thread-plan start/current/append/link-run/render` work for chat-level natural-language planning; +- `thread-plan render --format html` creates a self-contained sidecar without becoming a completion gate; - `context-pack`, `plan-task`, and `verify-context` work; - `complete-task` refuses missing plan/context evidence and spec drift; - `create-task` works and new work does not require abusing `reopen-task`; - `phase-task` and `verify-lifecycle` work; - `phase-task` returns a visible `CHECKPOINT_OK` marker; - `capability-event` records capability visibility and returns `CAPABILITY_OK`; +- `decision-event` records public decision graph events and returns `DECISION_OK`; +- `decision-query` can filter run decisions by parent, kind, and status; +- `verify-decisions` returns a visible `DECISION_VERIFY_OK` marker and rejects forged or orphaned decisions; +- `flow-summary` returns a visible `FLOW_OK` marker and a readable Mermaid Mission Flow; +- `render-html --kind mission-flow` creates a self-contained HTML sidecar from `mission-flow.md`; - `artifact-assert` verifies real side effects and returns `EFFECT_OK`; - `artifact-assert` rejects bogus `--capability-event-id` links; - `verify-effects` returns a visible `EFFECT_VERIFY_OK` marker with status, strictness, assertion count, warning count, and error count; - `verify-effects` refuses missing or forged effect evidence when policy is `enforce`; - `verify-lifecycle` refuses missing dispatch or required capability evidence; - `complete-task` refuses missing phases; -- `complete-task` runs effect verification and records warnings or downgrade reasons; +- `complete-task` runs effect and decision verification and records warnings or downgrade reasons; +- `complete-task` returns Mission Flow fields for medium, high, or complex tasks; - `complete-task` runs required postflight or records an explicit pending reason. diff --git a/docs/subagent-runtime-validation.fragment.html b/docs/subagent-runtime-validation.fragment.html new file mode 100644 index 0000000..c4439fa --- /dev/null +++ b/docs/subagent-runtime-validation.fragment.html @@ -0,0 +1,166 @@ +<article class="kos-report-fragment" data-kos-fragment="true" data-kind="rich-report" data-source-sha256="4407ea2bf73ee5ca9a51368e864c07ead2bd35757689a9bb9ef1eb10ee9fde56"> + <div class="kos-eyebrow">rich-report</div> + <h1 id="rich-report-4407ea2bf73e-subagent-runtime-validation">Subagent Runtime Validation</h1> +<p>Date: 2026-07-11</p> +<p>Task: KOS-T087</p> +<p>Spec: SPEC-20260711-001</p> +<h2 id="rich-report-4407ea2bf73e-outcome">Outcome</h2> +<p>KnowledgeOS now has command-evidenced validation for the complete runtime-callable subagent catalog:</p> +<ul class="kos-list"> +<li>`SUBAGENT_CATALOG_OK roles=42/42 native_min=3 invalid=0`</li> +<li>3 native Codex roles completed three serial smoke rounds each.</li> +<li>39 Maestro adapter roles each completed one live role-contract smoke.</li> +<li>48 catalog invocations completed with a unique nonce, matching role id, `cleanup=completed`, and `role_contract=passed`.</li> +<li>Three additional diagnostic invocations isolated the substantive-task timeout behavior.</li> +<li>No active runtime gap remained after late-result reconciliation.</li> +</ul> +<p>This validates invocation, role-prompt loading, result delivery, and cleanup through parent-recorded runtime attestations. It is not a deep benchmark of every specialist domain or an independent host-runtime audit.</p> +<h2 id="rich-report-4407ea2bf73e-root-cause">Root Cause</h2> +<p>The earlier `codex-explorer` symptom was reproducible only for substantive work, not for minimal runtime smokes:</p> +<p>1. `codex-default`, `codex-explorer`, and `codex-worker` each completed three minimal serial smokes.</p> +<p>2. A real read-only code-review task exceeded a fixed 120-second wait on both `codex-explorer` and `codex-default`.</p> +<p>3. The explorer returned a valid late result without intervention.</p> +<p>4. The default agent returned valid current findings after one interrupt request.</p> +<p>5. Both agents closed successfully.</p> +<p>6. A later Maestro code-review run inherited parent-level lifecycle behavior and attempted nested delegation instead of directly reviewing the diff.</p> +<p>The defect was therefore not an explorer adapter mapping failure. The operational failure combined a fixed short wait, lost recovery context, and missing boundaries between parent orchestration and bounded subagent execution.</p> +<h2 id="rich-report-4407ea2bf73e-repair">Repair</h2> +<h3 id="rich-report-4407ea2bf73e-strict-catalog-verifier">Strict catalog verifier</h3> +<p>`verify-subagents` reads the registered runtime-callable catalog and the run capability ledger. A role counts only when its evidence contains:</p> +<pre><code>SUBAGENT_SMOKE_OK id=<registered-id> nonce=<unique-nonce> +cleanup=completed +role_contract=passed +adapter_challenge=<challenge from subagent-adapter></code></pre> +<p>The verifier:</p> +<ul class="kos-list"> +<li>counts unique role ids rather than raw events;</li> +<li>locks the target catalog in a run-level snapshot before validation;</li> +<li>validates snapshot schema, task/run binding, UTC timestamp, evidence model, catalog hash, and full snapshot integrity;</li> +<li>counts unique nonces for native stability;</li> +<li>rejects unknown roles and malformed strict evidence;</li> +<li>rejects catalog drift and challenge-free attestations;</li> +<li>consumes each adapter challenge once, so one adapter issuance cannot prove multiple executions;</li> +<li>enforces a native minimum floor of three rather than allowing callers to weaken it;</li> +<li>does not count `timed_out`, `blocked`, `skipped`, or cleanup failures;</li> +<li>emits `SUBAGENT_CATALOG_OK` only after full coverage.</li> +</ul> +<h3 id="rich-report-4407ea2bf73e-runtime-gap-reconciliation">Runtime-gap reconciliation</h3> +<p>`dispatch-report` now:</p> +<ul class="kos-list"> +<li>keeps safety-policy `blocked` events visible without calling them runtime failures;</li> +<li>treats timeout, interruption, orphaning, errors, and close failures as runtime gaps;</li> +<li>treats explicit `failed` or cancelled subagent events as runtime gaps rather than successful invocations;</li> +<li>recognizes a later completed event with `recovered_from=timed_out` as a resolved gap;</li> +<li>binds each recovery to one exact timeout with `--recovers-event-id`, preventing one completion from clearing multiple failures;</li> +<li>reports unique agent ids separately from raw capability-event counts.</li> +</ul> +<h3 id="rich-report-4407ea2bf73e-host-runtime-usage-contract">Host runtime usage contract</h3> +<p>Startup prompts and agent guidance now require:</p> +<ul class="kos-list"> +<li>preserving every spawned agent id;</li> +<li>one multi-minute wait for substantive work instead of repeated short polls;</li> +<li>one interrupt requesting current findings after a timeout;</li> +<li>one recovery wait;</li> +<li>closing only after a terminal result;</li> +<li>recording late completion as recovered evidence.</li> +</ul> +<h3 id="rich-report-4407ea2bf73e-bounded-subagent-runtime-boundary">Bounded subagent runtime boundary</h3> +<p>Every adapter role prompt now states that a spawned specialist:</p> +<ul class="kos-list"> +<li>works directly on the parent-assigned scope;</li> +<li>returns findings to the parent;</li> +<li>does not create or reopen KnowledgeOS tasks/specs;</li> +<li>does not run completion or postflight;</li> +<li>does not recursively dispatch another subagent unless orchestration was explicitly delegated.</li> +</ul> +<p>This keeps the parent Agent responsible for lifecycle and capability evidence and prevents accidental recursive delegation.</p> +<p>KnowledgeOS still does not spawn agents from the shell. The Codex host performs delegation; KnowledgeOS resolves roles and verifies public evidence.</p> +<p>The evidence model is explicitly `parent_attested_runtime`. `host_runtime_verified=false` means the CLI cannot independently inspect Codex's private tool-call log. The adapter challenge, immutable run catalog snapshot, result marker, cleanup record, and role-contract record make the parent attestation auditable and resistant to accidental or stale evidence, but they are not a cryptographic host signature.</p> +<h2 id="rich-report-4407ea2bf73e-verified-catalog">Verified Catalog</h2> +<h3 id="rich-report-4407ea2bf73e-default-backed-1">Default-backed (1)</h3> +<ul class="kos-list"> +<li>`codex-default`</li> +</ul> +<h3 id="rich-report-4407ea2bf73e-explorer-backed-17">Explorer-backed (17)</h3> +<ul class="kos-list"> +<li>`codex-explorer`</li> +<li>`maestro-accessibility-specialist`</li> +<li>`maestro-api-designer`</li> +<li>`maestro-architect`</li> +<li>`maestro-cloud-architect`</li> +<li>`maestro-code-reviewer`</li> +<li>`maestro-compliance-reviewer`</li> +<li>`maestro-content-strategist`</li> +<li>`maestro-database-administrator`</li> +<li>`maestro-db2-dba`</li> +<li>`maestro-debugger`</li> +<li>`maestro-performance-engineer`</li> +<li>`maestro-security-engineer`</li> +<li>`maestro-seo-specialist`</li> +<li>`maestro-site-reliability-engineer`</li> +<li>`maestro-solutions-architect`</li> +<li>`maestro-zos-sysprog`</li> +</ul> +<h3 id="rich-report-4407ea2bf73e-worker-backed-24">Worker-backed (24)</h3> +<ul class="kos-list"> +<li>`codex-worker`</li> +<li>`maestro-analytics-engineer`</li> +<li>`maestro-cobol-engineer`</li> +<li>`maestro-coder`</li> +<li>`maestro-copywriter`</li> +<li>`maestro-data-engineer`</li> +<li>`maestro-design-system-engineer`</li> +<li>`maestro-devops-engineer`</li> +<li>`maestro-hlasm-assembler-specialist`</li> +<li>`maestro-i18n-specialist`</li> +<li>`maestro-ibm-i-specialist`</li> +<li>`maestro-integration-engineer`</li> +<li>`maestro-ml-engineer`</li> +<li>`maestro-mlops-engineer`</li> +<li>`maestro-mobile-engineer`</li> +<li>`maestro-observability-engineer`</li> +<li>`maestro-platform-engineer`</li> +<li>`maestro-product-manager`</li> +<li>`maestro-prompt-engineer`</li> +<li>`maestro-refactor`</li> +<li>`maestro-release-manager`</li> +<li>`maestro-technical-writer`</li> +<li>`maestro-tester`</li> +<li>`maestro-ux-designer`</li> +</ul> +<h2 id="rich-report-4407ea2bf73e-verification-commands">Verification Commands</h2> +<pre><code>./bin/knowledgeos verify-subagents \ + --project-root . \ + --task-id KOS-T087 \ + --run-id RUN-20260711-202846-KOS-T087 \ + --native-min-successes 3 + +python3 -B -m py_compile knowledgeos/cli.py +python3 -B -m unittest discover -s tests -v +./examples/scenarios/run_guardrail_scenarios.sh +./bin/knowledgeos doctor --project-root . --summary +git diff --check</code></pre> +<h2 id="rich-report-4407ea2bf73e-evidence-boundary">Evidence Boundary</h2> +<p>Canonical evidence remains in Markdown, YAML, and NDJSON. The HTML report is a presentation sidecar and is not the source of truth. Public artifacts intentionally omit local absolute paths, runtime agent ids, and credentials.</p> + <details class="kos-fragment-meta"> + <summary>Source metadata</summary> + <dl class="kos-meta"><div><dt>Kind</dt><dd>rich-report</dd></div> +<div><dt>Run ID</dt><dd>not run-bound</dd></div> +<div><dt>Source</dt><dd>docs/subagent-runtime-validation.md</dd></div> +<div><dt>Source SHA-256</dt><dd>4407ea2bf73ee5ca9a51368e864c07ead2bd35757689a9bb9ef1eb10ee9fde56</dd></div> +<div><dt>Generated</dt><dd>2026-07-11T13:40:44.817518+00:00</dd></div></dl> + <p class="kos-notice">HTML is presentation, not source of truth.</p> + <nav class="kos-nav" aria-label="Report sections"><ul class="kos-list"><li><a href="#rich-report-4407ea2bf73e-subagent-runtime-validation">Subagent Runtime Validation</a></li> +<li><a href="#rich-report-4407ea2bf73e-outcome">Outcome</a></li> +<li><a href="#rich-report-4407ea2bf73e-root-cause">Root Cause</a></li> +<li><a href="#rich-report-4407ea2bf73e-repair">Repair</a></li> +<li><a href="#rich-report-4407ea2bf73e-strict-catalog-verifier">Strict catalog verifier</a></li> +<li><a href="#rich-report-4407ea2bf73e-runtime-gap-reconciliation">Runtime-gap reconciliation</a></li> +<li><a href="#rich-report-4407ea2bf73e-host-runtime-usage-contract">Host runtime usage contract</a></li> +<li><a href="#rich-report-4407ea2bf73e-bounded-subagent-runtime-boundary">Bounded subagent runtime boundary</a></li> +<li><a href="#rich-report-4407ea2bf73e-verified-catalog">Verified Catalog</a></li> +<li><a href="#rich-report-4407ea2bf73e-default-backed-1">Default-backed (1)</a></li> +<li><a href="#rich-report-4407ea2bf73e-explorer-backed-17">Explorer-backed (17)</a></li> +<li><a href="#rich-report-4407ea2bf73e-worker-backed-24">Worker-backed (24)</a></li></ul></nav> + </details> +</article> \ No newline at end of file diff --git a/docs/subagent-runtime-validation.html b/docs/subagent-runtime-validation.html new file mode 100644 index 0000000..98b5b1d --- /dev/null +++ b/docs/subagent-runtime-validation.html @@ -0,0 +1,281 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="knowledgeos-source-sha256" content="4407ea2bf73ee5ca9a51368e864c07ead2bd35757689a9bb9ef1eb10ee9fde56"> + <meta name="knowledgeos-source-path" content="docs/subagent-runtime-validation.md"> + <title>Subagent Runtime Validation + + + +
+
+
Composable sidecar report
+

Subagent Runtime Validation

+

A self-contained KnowledgeOS HTML sidecar generated from canonical Markdown, YAML, or NDJSON evidence. Use it for human review; keep source files as the contract.

+
+
+
+
rich-report
+

Subagent Runtime Validation

+

Date: 2026-07-11

+

Task: KOS-T087

+

Spec: SPEC-20260711-001

+

Outcome

+

KnowledgeOS now has command-evidenced validation for the complete runtime-callable subagent catalog:

+
    +
  • `SUBAGENT_CATALOG_OK roles=42/42 native_min=3 invalid=0`
  • +
  • 3 native Codex roles completed three serial smoke rounds each.
  • +
  • 39 Maestro adapter roles each completed one live role-contract smoke.
  • +
  • 48 catalog invocations completed with a unique nonce, matching role id, `cleanup=completed`, and `role_contract=passed`.
  • +
  • Three additional diagnostic invocations isolated the substantive-task timeout behavior.
  • +
  • No active runtime gap remained after late-result reconciliation.
  • +
+

This validates invocation, role-prompt loading, result delivery, and cleanup through parent-recorded runtime attestations. It is not a deep benchmark of every specialist domain or an independent host-runtime audit.

+

Root Cause

+

The earlier `codex-explorer` symptom was reproducible only for substantive work, not for minimal runtime smokes:

+

1. `codex-default`, `codex-explorer`, and `codex-worker` each completed three minimal serial smokes.

+

2. A real read-only code-review task exceeded a fixed 120-second wait on both `codex-explorer` and `codex-default`.

+

3. The explorer returned a valid late result without intervention.

+

4. The default agent returned valid current findings after one interrupt request.

+

5. Both agents closed successfully.

+

6. A later Maestro code-review run inherited parent-level lifecycle behavior and attempted nested delegation instead of directly reviewing the diff.

+

The defect was therefore not an explorer adapter mapping failure. The operational failure combined a fixed short wait, lost recovery context, and missing boundaries between parent orchestration and bounded subagent execution.

+

Repair

+

Strict catalog verifier

+

`verify-subagents` reads the registered runtime-callable catalog and the run capability ledger. A role counts only when its evidence contains:

+
SUBAGENT_SMOKE_OK id=<registered-id> nonce=<unique-nonce>
+cleanup=completed
+role_contract=passed
+adapter_challenge=<challenge from subagent-adapter>
+

The verifier:

+
    +
  • counts unique role ids rather than raw events;
  • +
  • locks the target catalog in a run-level snapshot before validation;
  • +
  • validates snapshot schema, task/run binding, UTC timestamp, evidence model, catalog hash, and full snapshot integrity;
  • +
  • counts unique nonces for native stability;
  • +
  • rejects unknown roles and malformed strict evidence;
  • +
  • rejects catalog drift and challenge-free attestations;
  • +
  • consumes each adapter challenge once, so one adapter issuance cannot prove multiple executions;
  • +
  • enforces a native minimum floor of three rather than allowing callers to weaken it;
  • +
  • does not count `timed_out`, `blocked`, `skipped`, or cleanup failures;
  • +
  • emits `SUBAGENT_CATALOG_OK` only after full coverage.
  • +
+

Runtime-gap reconciliation

+

`dispatch-report` now:

+
    +
  • keeps safety-policy `blocked` events visible without calling them runtime failures;
  • +
  • treats timeout, interruption, orphaning, errors, and close failures as runtime gaps;
  • +
  • treats explicit `failed` or cancelled subagent events as runtime gaps rather than successful invocations;
  • +
  • recognizes a later completed event with `recovered_from=timed_out` as a resolved gap;
  • +
  • binds each recovery to one exact timeout with `--recovers-event-id`, preventing one completion from clearing multiple failures;
  • +
  • reports unique agent ids separately from raw capability-event counts.
  • +
+

Host runtime usage contract

+

Startup prompts and agent guidance now require:

+
    +
  • preserving every spawned agent id;
  • +
  • one multi-minute wait for substantive work instead of repeated short polls;
  • +
  • one interrupt requesting current findings after a timeout;
  • +
  • one recovery wait;
  • +
  • closing only after a terminal result;
  • +
  • recording late completion as recovered evidence.
  • +
+

Bounded subagent runtime boundary

+

Every adapter role prompt now states that a spawned specialist:

+
    +
  • works directly on the parent-assigned scope;
  • +
  • returns findings to the parent;
  • +
  • does not create or reopen KnowledgeOS tasks/specs;
  • +
  • does not run completion or postflight;
  • +
  • does not recursively dispatch another subagent unless orchestration was explicitly delegated.
  • +
+

This keeps the parent Agent responsible for lifecycle and capability evidence and prevents accidental recursive delegation.

+

KnowledgeOS still does not spawn agents from the shell. The Codex host performs delegation; KnowledgeOS resolves roles and verifies public evidence.

+

The evidence model is explicitly `parent_attested_runtime`. `host_runtime_verified=false` means the CLI cannot independently inspect Codex's private tool-call log. The adapter challenge, immutable run catalog snapshot, result marker, cleanup record, and role-contract record make the parent attestation auditable and resistant to accidental or stale evidence, but they are not a cryptographic host signature.

+

Verified Catalog

+

Default-backed (1)

+
    +
  • `codex-default`
  • +
+

Explorer-backed (17)

+
    +
  • `codex-explorer`
  • +
  • `maestro-accessibility-specialist`
  • +
  • `maestro-api-designer`
  • +
  • `maestro-architect`
  • +
  • `maestro-cloud-architect`
  • +
  • `maestro-code-reviewer`
  • +
  • `maestro-compliance-reviewer`
  • +
  • `maestro-content-strategist`
  • +
  • `maestro-database-administrator`
  • +
  • `maestro-db2-dba`
  • +
  • `maestro-debugger`
  • +
  • `maestro-performance-engineer`
  • +
  • `maestro-security-engineer`
  • +
  • `maestro-seo-specialist`
  • +
  • `maestro-site-reliability-engineer`
  • +
  • `maestro-solutions-architect`
  • +
  • `maestro-zos-sysprog`
  • +
+

Worker-backed (24)

+
    +
  • `codex-worker`
  • +
  • `maestro-analytics-engineer`
  • +
  • `maestro-cobol-engineer`
  • +
  • `maestro-coder`
  • +
  • `maestro-copywriter`
  • +
  • `maestro-data-engineer`
  • +
  • `maestro-design-system-engineer`
  • +
  • `maestro-devops-engineer`
  • +
  • `maestro-hlasm-assembler-specialist`
  • +
  • `maestro-i18n-specialist`
  • +
  • `maestro-ibm-i-specialist`
  • +
  • `maestro-integration-engineer`
  • +
  • `maestro-ml-engineer`
  • +
  • `maestro-mlops-engineer`
  • +
  • `maestro-mobile-engineer`
  • +
  • `maestro-observability-engineer`
  • +
  • `maestro-platform-engineer`
  • +
  • `maestro-product-manager`
  • +
  • `maestro-prompt-engineer`
  • +
  • `maestro-refactor`
  • +
  • `maestro-release-manager`
  • +
  • `maestro-technical-writer`
  • +
  • `maestro-tester`
  • +
  • `maestro-ux-designer`
  • +
+

Verification Commands

+
./bin/knowledgeos verify-subagents \
+  --project-root . \
+  --task-id KOS-T087 \
+  --run-id RUN-20260711-202846-KOS-T087 \
+  --native-min-successes 3
+
+python3 -B -m py_compile knowledgeos/cli.py
+python3 -B -m unittest discover -s tests -v
+./examples/scenarios/run_guardrail_scenarios.sh
+./bin/knowledgeos doctor --project-root . --summary
+git diff --check
+

Evidence Boundary

+

Canonical evidence remains in Markdown, YAML, and NDJSON. The HTML report is a presentation sidecar and is not the source of truth. Public artifacts intentionally omit local absolute paths, runtime agent ids, and credentials.

+
+ Source metadata +
Kind
rich-report
+
Run ID
not run-bound
+
Source
docs/subagent-runtime-validation.md
+
Source SHA-256
4407ea2bf73ee5ca9a51368e864c07ead2bd35757689a9bb9ef1eb10ee9fde56
+
Generated
2026-07-11T13:40:44.817518+00:00
+

HTML is presentation, not source of truth.

+ +
+
+ +
+
HTML is presentation, not source of truth.
+
+ + diff --git a/docs/subagent-runtime-validation.manifest.json b/docs/subagent-runtime-validation.manifest.json new file mode 100644 index 0000000..87eef10 --- /dev/null +++ b/docs/subagent-runtime-validation.manifest.json @@ -0,0 +1,88 @@ +{ + "schema_version": "knowledgeos.html-report.v1", + "kind": "rich-report", + "title": "Subagent Runtime Validation", + "theme": "knowledgeos-default", + "presentation": "default", + "run_id": "", + "source": "docs/subagent-runtime-validation.md", + "source_sha256": "4407ea2bf73ee5ca9a51368e864c07ead2bd35757689a9bb9ef1eb10ee9fde56", + "output": "docs/subagent-runtime-validation.html", + "fragment": "docs/subagent-runtime-validation.fragment.html", + "sections": [ + { + "id": "rich-report-4407ea2bf73e-subagent-runtime-validation", + "title": "Subagent Runtime Validation", + "level": 1 + }, + { + "id": "rich-report-4407ea2bf73e-outcome", + "title": "Outcome", + "level": 2 + }, + { + "id": "rich-report-4407ea2bf73e-root-cause", + "title": "Root Cause", + "level": 2 + }, + { + "id": "rich-report-4407ea2bf73e-repair", + "title": "Repair", + "level": 2 + }, + { + "id": "rich-report-4407ea2bf73e-strict-catalog-verifier", + "title": "Strict catalog verifier", + "level": 3 + }, + { + "id": "rich-report-4407ea2bf73e-runtime-gap-reconciliation", + "title": "Runtime-gap reconciliation", + "level": 3 + }, + { + "id": "rich-report-4407ea2bf73e-host-runtime-usage-contract", + "title": "Host runtime usage contract", + "level": 3 + }, + { + "id": "rich-report-4407ea2bf73e-bounded-subagent-runtime-boundary", + "title": "Bounded subagent runtime boundary", + "level": 3 + }, + { + "id": "rich-report-4407ea2bf73e-verified-catalog", + "title": "Verified Catalog", + "level": 2 + }, + { + "id": "rich-report-4407ea2bf73e-default-backed-1", + "title": "Default-backed (1)", + "level": 3 + }, + { + "id": "rich-report-4407ea2bf73e-explorer-backed-17", + "title": "Explorer-backed (17)", + "level": 3 + }, + { + "id": "rich-report-4407ea2bf73e-worker-backed-24", + "title": "Worker-backed (24)", + "level": 3 + }, + { + "id": "rich-report-4407ea2bf73e-verification-commands", + "title": "Verification Commands", + "level": 2 + }, + { + "id": "rich-report-4407ea2bf73e-evidence-boundary", + "title": "Evidence Boundary", + "level": 2 + } + ], + "generated_at": "2026-07-11T13:40:44.817518+00:00", + "html_source_of_truth": false, + "html_required_metadata": true, + "notice": "HTML is presentation, not source of truth." +} diff --git a/docs/subagent-runtime-validation.md b/docs/subagent-runtime-validation.md new file mode 100644 index 0000000..1616401 --- /dev/null +++ b/docs/subagent-runtime-validation.md @@ -0,0 +1,168 @@ +# Subagent Runtime Validation + +Date: 2026-07-11 +Task: KOS-T087 +Spec: SPEC-20260711-001 + +## Outcome + +KnowledgeOS now has command-evidenced validation for the complete runtime-callable subagent catalog: + +- `SUBAGENT_CATALOG_OK roles=42/42 native_min=3 invalid=0` +- 3 native Codex roles completed three serial smoke rounds each. +- 39 Maestro adapter roles each completed one live role-contract smoke. +- 48 catalog invocations completed with a unique nonce, matching role id, `cleanup=completed`, and `role_contract=passed`. +- Three additional diagnostic invocations isolated the substantive-task timeout behavior. +- No active runtime gap remained after late-result reconciliation. + +This validates invocation, role-prompt loading, result delivery, and cleanup through parent-recorded runtime attestations. It is not a deep benchmark of every specialist domain or an independent host-runtime audit. + +## Root Cause + +The earlier `codex-explorer` symptom was reproducible only for substantive work, not for minimal runtime smokes: + +1. `codex-default`, `codex-explorer`, and `codex-worker` each completed three minimal serial smokes. +2. A real read-only code-review task exceeded a fixed 120-second wait on both `codex-explorer` and `codex-default`. +3. The explorer returned a valid late result without intervention. +4. The default agent returned valid current findings after one interrupt request. +5. Both agents closed successfully. +6. A later Maestro code-review run inherited parent-level lifecycle behavior and attempted nested delegation instead of directly reviewing the diff. + +The defect was therefore not an explorer adapter mapping failure. The operational failure combined a fixed short wait, lost recovery context, and missing boundaries between parent orchestration and bounded subagent execution. + +## Repair + +### Strict catalog verifier + +`verify-subagents` reads the registered runtime-callable catalog and the run capability ledger. A role counts only when its evidence contains: + +```text +SUBAGENT_SMOKE_OK id= nonce= +cleanup=completed +role_contract=passed +adapter_challenge= +``` + +The verifier: + +- counts unique role ids rather than raw events; +- locks the target catalog in a run-level snapshot before validation; +- validates snapshot schema, task/run binding, UTC timestamp, evidence model, catalog hash, and full snapshot integrity; +- counts unique nonces for native stability; +- rejects unknown roles and malformed strict evidence; +- rejects catalog drift and challenge-free attestations; +- consumes each adapter challenge once, so one adapter issuance cannot prove multiple executions; +- enforces a native minimum floor of three rather than allowing callers to weaken it; +- does not count `timed_out`, `blocked`, `skipped`, or cleanup failures; +- emits `SUBAGENT_CATALOG_OK` only after full coverage. + +### Runtime-gap reconciliation + +`dispatch-report` now: + +- keeps safety-policy `blocked` events visible without calling them runtime failures; +- treats timeout, interruption, orphaning, errors, and close failures as runtime gaps; +- treats explicit `failed` or cancelled subagent events as runtime gaps rather than successful invocations; +- recognizes a later completed event with `recovered_from=timed_out` as a resolved gap; +- binds each recovery to one exact timeout with `--recovers-event-id`, preventing one completion from clearing multiple failures; +- reports unique agent ids separately from raw capability-event counts. + +### Host runtime usage contract + +Startup prompts and agent guidance now require: + +- preserving every spawned agent id; +- one multi-minute wait for substantive work instead of repeated short polls; +- one interrupt requesting current findings after a timeout; +- one recovery wait; +- closing only after a terminal result; +- recording late completion as recovered evidence. + +### Bounded subagent runtime boundary + +Every adapter role prompt now states that a spawned specialist: + +- works directly on the parent-assigned scope; +- returns findings to the parent; +- does not create or reopen KnowledgeOS tasks/specs; +- does not run completion or postflight; +- does not recursively dispatch another subagent unless orchestration was explicitly delegated. + +This keeps the parent Agent responsible for lifecycle and capability evidence and prevents accidental recursive delegation. + +KnowledgeOS still does not spawn agents from the shell. The Codex host performs delegation; KnowledgeOS resolves roles and verifies public evidence. + +The evidence model is explicitly `parent_attested_runtime`. `host_runtime_verified=false` means the CLI cannot independently inspect Codex's private tool-call log. The adapter challenge, immutable run catalog snapshot, result marker, cleanup record, and role-contract record make the parent attestation auditable and resistant to accidental or stale evidence, but they are not a cryptographic host signature. + +## Verified Catalog + +### Default-backed (1) + +- `codex-default` + +### Explorer-backed (17) + +- `codex-explorer` +- `maestro-accessibility-specialist` +- `maestro-api-designer` +- `maestro-architect` +- `maestro-cloud-architect` +- `maestro-code-reviewer` +- `maestro-compliance-reviewer` +- `maestro-content-strategist` +- `maestro-database-administrator` +- `maestro-db2-dba` +- `maestro-debugger` +- `maestro-performance-engineer` +- `maestro-security-engineer` +- `maestro-seo-specialist` +- `maestro-site-reliability-engineer` +- `maestro-solutions-architect` +- `maestro-zos-sysprog` + +### Worker-backed (24) + +- `codex-worker` +- `maestro-analytics-engineer` +- `maestro-cobol-engineer` +- `maestro-coder` +- `maestro-copywriter` +- `maestro-data-engineer` +- `maestro-design-system-engineer` +- `maestro-devops-engineer` +- `maestro-hlasm-assembler-specialist` +- `maestro-i18n-specialist` +- `maestro-ibm-i-specialist` +- `maestro-integration-engineer` +- `maestro-ml-engineer` +- `maestro-mlops-engineer` +- `maestro-mobile-engineer` +- `maestro-observability-engineer` +- `maestro-platform-engineer` +- `maestro-product-manager` +- `maestro-prompt-engineer` +- `maestro-refactor` +- `maestro-release-manager` +- `maestro-technical-writer` +- `maestro-tester` +- `maestro-ux-designer` + +## Verification Commands + +```bash +./bin/knowledgeos verify-subagents \ + --project-root . \ + --task-id KOS-T087 \ + --run-id RUN-20260711-202846-KOS-T087 \ + --native-min-successes 3 + +python3 -B -m py_compile knowledgeos/cli.py +python3 -B -m unittest discover -s tests -v +./examples/scenarios/run_guardrail_scenarios.sh +./bin/knowledgeos doctor --project-root . --summary +git diff --check +``` + +## Evidence Boundary + +Canonical evidence remains in Markdown, YAML, and NDJSON. The HTML report is a presentation sidecar and is not the source of truth. Public artifacts intentionally omit local absolute paths, runtime agent ids, and credentials. diff --git a/docs/thread-plan-ledger.md b/docs/thread-plan-ledger.md new file mode 100644 index 0000000..f0e27a0 --- /dev/null +++ b/docs/thread-plan-ledger.md @@ -0,0 +1,80 @@ +# KnowledgeOS Thread Plan Ledger + +Thread Plan Ledger is a chat-level planning module. It keeps a natural-language, append-only map of a long conversation: the original plan, later branches, route changes, inserted phases, linked runs, and current working line. + +It is not a checkpoint gate. It does not replace `plan-task`, `decision-event`, lifecycle evidence, or completion receipts. + +## Why It Exists + +Some research and product conversations last across many tasks. A run-level Mission Flow shows one completed task, but it does not show how the whole conversation evolved. Thread Plan Ledger gives that longer story a durable, readable home. + +Use it when the user says they want to create, align, or follow a durable plan/spec, or when a conversation starts to branch over multiple rounds. + +## Evidence Files + +```text +.agent-os/threads//thread-plan.ndjson +.agent-os/threads//thread-plan.md +.agent-os/threads//thread-map.html +.agent-os/threads/current.json +``` + +The NDJSON ledger is the source of truth. Markdown and HTML are sidecars for human review. + +## Commands + +Start a chat-level plan: + +```bash +knowledgeos thread-plan start \ + --project-root . \ + --title "长期维护鸟类声景基金申请计划" \ + --spec-id SPEC-... +``` + +Append a natural-language note: + +```bash +knowledgeos thread-plan append \ + --project-root . \ + --thread-id THREAD-... \ + --kind phase \ + --text "Phase A:先稳定计划记录;Phase B:再串联多个任务。" +``` + +Link a run to the conversation: + +```bash +knowledgeos thread-plan link-run \ + --project-root . \ + --thread-id THREAD-... \ + --task-id T001 \ + --run-id RUN-... +``` + +Render the plan: + +```bash +knowledgeos thread-plan render --project-root . --thread-id THREAD-... --format markdown +knowledgeos thread-plan render --project-root . --thread-id THREAD-... --format mermaid +knowledgeos thread-plan render --project-root . --thread-id THREAD-... --format html +``` + +All successful commands return `THREAD_PLAN_OK`. + +## Writing Style + +Keep entries readable. Prefer: + +- `Plan A / Plan B`; +- `Phase A / Phase B`; +- `当前选择`; +- `为什么改路`; +- `已经完成到哪里`; +- `下一步是什么`. + +Do not paste hidden chain-of-thought. Record concise public planning notes that another human can read later. + +## Module Boundary + +Thread Plan Ledger is a module, not kernel. It should not block `complete-task`, `verify-lifecycle`, or `SYNC_OK`. It can be rendered in a Workbench or HTML page as a versioned planning map, similar to a lightweight visual worktree for the conversation. diff --git a/knowledgeos/cli.py b/knowledgeos/cli.py index 140ede3..4cd59a8 100644 --- a/knowledgeos/cli.py +++ b/knowledgeos/cli.py @@ -45,6 +45,8 @@ "docs/spec-context-plan.md", "docs/reset-and-migration.md", "docs/archive-policy.md", + "docs/decision-graph-module.md", + "docs/thread-plan-ledger.md", "templates/governance-core/README.md", "templates/governance-core/STRUCTURE-CHECK.md", "templates/governance-core/rules/global.md", @@ -68,6 +70,7 @@ "templates/governance-core/sync/import-state.json", "templates/capability-layer/README.md", "templates/capability-layer/STRUCTURE-CHECK.md", + "templates/capability-layer/subagents/maestro/manifest.yaml", "templates/project-control-plane/AGENTS.md", "examples/scenarios/README.md", "examples/scenarios/distracted-agent-guardrails.md", @@ -78,6 +81,7 @@ "templates/project-control-plane/.agent-os/tasks.yaml", "templates/project-control-plane/.agent-os/specs.yaml", "templates/project-control-plane/.agent-os/phase-policy.yaml", + "templates/project-control-plane/.agent-os/decision-policy.yaml", "templates/project-control-plane/.agent-os/effect-policy.yaml", "templates/project-control-plane/.agent-os/read-policy.yaml", "templates/project-control-plane/.agent-os/write-policy.yaml", @@ -136,6 +140,11 @@ "forbidden_without_human_gate", "require_receipt_for", } +LOCAL_EXTERNAL_WRITE_POLICY_SECTIONS = { + "external_controlled", + "external_forbidden_without_human_gate", + "external_require_receipt_for", +} READ_POLICY_SECTIONS = { "default_context", "cold_storage", @@ -145,8 +154,63 @@ EXPECTED_PHASE_KEYS = ["route", "plan", "review", "dispatch", "execute", "report"] PHASE_STATUSES = {"completed", "skipped"} -CAPABILITY_EVENT_KINDS = {"mcp", "skill", "subagent", "orchestrator", "script", "shell", "file_read"} +CAPABILITY_EVENT_KINDS = { + "app", + "browser", + "chrome", + "file_read", + "github", + "mcp", + "plugin", + "script", + "security", + "shell", + "skill", + "subagent", + "orchestrator", +} +AGENT_CAPABILITY_KINDS = {"orchestrator", "subagent"} +SKIPPED_CAPABILITY_STATUSES = {"skipped", "skip", "not_needed", "not-needed", "timed_out", "timed-out", "timeout", "blocked", "close_failed", "close-failed", "interrupted", "orphaned", "errored", "failed", "cancelled", "canceled"} +RUNTIME_GAP_CAPABILITY_STATUSES = {"timed_out", "timed-out", "timeout", "close_failed", "close-failed", "interrupted", "orphaned", "errored", "failed", "cancelled", "canceled"} +SUBAGENT_SMOKE_MARKER = "SUBAGENT_SMOKE_OK" +SUBAGENT_CATALOG_MARKER = "SUBAGENT_CATALOG_OK" +DEFAULT_NATIVE_SUBAGENT_SUCCESSES = 3 +SUBAGENT_RUNTIME_BOUNDARY = ( + "Runtime boundary: You are a bounded delegated subagent, not the parent orchestrator. " + "Work directly on the assigned scope and return findings to the parent. Do not create or reopen KnowledgeOS tasks/specs, " + "run completion/postflight, or recursively spawn subagents unless the parent explicitly delegates orchestration. " + "The parent owns lifecycle and capability evidence." +) EFFECT_STRICTNESS_LEVELS = {"observe", "warn", "enforce", "off"} +DECISION_STRICTNESS_LEVELS = {"warn", "enforce", "off"} +DECISION_EVENT_KINDS = { + "plan_node", + "branch_opened", + "branch_selected", + "step_inserted", + "branch_abandoned", + "rollback", + "superseded", + "deferred", + "human_decision", + "risk_tradeoff", + "final_decision", +} +DECISION_EVENT_STATUSES = { + "planned", + "active", + "selected", + "executed", + "skipped", + "abandoned", + "rolled_back", + "superseded", + "deferred", + "blocked", +} +DECISION_EXPLANATION_REQUIRED_KINDS = {"branch_abandoned", "rollback", "superseded"} +THREAD_PLAN_EVENT_KINDS = {"plan", "phase", "branch", "decision", "progress", "change", "summary"} +THREAD_PLAN_MARKER = "THREAD_PLAN_OK" EFFECT_ASSERTION_KINDS = { "file_exists", "file_nonempty", @@ -205,6 +269,45 @@ TOOL_KINDS = {"mcp", "skill", "workflow", "orchestrator", "subagent", "memory"} TOOL_STATUSES = {"enabled", "disabled", "optional", "recommended", "configured", "indexed"} ACTIVE_TOOL_STATUSES = {"enabled", "recommended", "configured", "indexed"} +CODEX_RUNTIME_TOOL = "multi_agent_v1.spawn_agent" +CODEX_RUNTIME_AGENT_TYPES = {"default", "explorer", "worker"} +SUBAGENT_CANDIDATE_LIMIT = 3 +CODEX_NATIVE_SUBAGENTS = [ + { + "id": "codex-default", + "runtime_agent_type": "default", + "task_fit": "capability_orchestration,knowledge_structuring,report_task,documentation_task", + "capability_fit": "codex_native,default_agent,general_execution", + "scope": "codex-native-subagent", + }, + { + "id": "codex-explorer", + "runtime_agent_type": "explorer", + "task_fit": "research_task,documentation_task,analysis_task,security_audit,architecture_design", + "capability_fit": "codex_native,exploration,read_only,research", + "scope": "codex-native-subagent", + }, + { + "id": "codex-worker", + "runtime_agent_type": "worker", + "task_fit": "engineering_change,route_bound_execution_guard,executable_control_plane,workflow_routing,tool_registry", + "capability_fit": "codex_native,implementation,read_write,tests", + "scope": "codex-native-subagent", + }, +] +SUBAGENT_PREFERENCES_BY_TASK = { + "analysis_task": ["codex-explorer", "maestro-analytics-engineer", "codex-default"], + "architecture_design": ["maestro-architect", "codex-explorer", "maestro-api-designer"], + "capability_orchestration": ["codex-default", "maestro-architect", "maestro-coder"], + "documentation_task": ["codex-explorer", "maestro-technical-writer", "codex-default"], + "engineering_change": ["codex-worker", "maestro-coder", "maestro-code-reviewer"], + "executable_control_plane": ["codex-worker", "maestro-architect", "maestro-coder"], + "report_task": ["codex-default", "codex-explorer", "maestro-technical-writer"], + "route_bound_execution_guard": ["codex-worker", "maestro-coder", "maestro-code-reviewer"], + "security_audit": ["maestro-security-engineer", "maestro-code-reviewer", "codex-explorer"], + "tool_registry": ["codex-worker", "maestro-architect", "codex-default"], + "workflow_routing": ["codex-worker", "maestro-architect", "maestro-coder"], +} DISPATCH_POLICY_SECTIONS = { "default_order", "always_consult_before", @@ -520,6 +623,41 @@ def validate_effect_policy(project_root: Path) -> list[CheckResult]: return results +def parse_decision_policy(project_root: Path) -> dict[str, Any]: + policy_path = project_root / ".agent-os" / "decision-policy.yaml" + if not policy_path.exists(): + return { + "exists": False, + "strictness": "warn", + "downgrade_reason": "", + } + scalars = parse_scalar_values(policy_path, {"strictness", "downgrade_reason"}) + return { + "exists": True, + "strictness": (scalars.get("strictness") or "warn").lower(), + "downgrade_reason": scalars.get("downgrade_reason", ""), + } + + +def validate_decision_policy(project_root: Path) -> list[CheckResult]: + policy = parse_decision_policy(project_root) + strictness = str(policy.get("strictness", "warn")) + results = [ + CheckResult( + strictness in DECISION_STRICTNESS_LEVELS, + "decision_policy", + f"strictness={strictness}" if strictness in DECISION_STRICTNESS_LEVELS else f"invalid strictness={strictness}", + ) + ] + if not policy.get("exists"): + results.append(CheckResult(True, "decision_policy", "missing decision-policy defaults to strictness=warn")) + return results + if strictness == "off": + reason = str(policy.get("downgrade_reason", "")).strip() + results.append(CheckResult(bool(reason), "decision_policy", "strictness=off requires downgrade_reason")) + return results + + def parse_named_blocks(path: Path) -> list[dict[str, str]]: blocks: list[dict[str, str]] = [] current: dict[str, str] | None = None @@ -657,6 +795,13 @@ def load_write_policy(project_root: Path) -> dict[str, list[str]]: return parse_simple_list_sections(policy_path, WRITE_POLICY_SECTIONS) +def load_local_external_write_policy(project_root: Path) -> dict[str, list[str]]: + policy_path = project_root / ".knowledgeos-local" / "write-policy.local.yaml" + if not policy_path.exists(): + return {section: [] for section in LOCAL_EXTERNAL_WRITE_POLICY_SECTIONS} + return parse_simple_list_sections(policy_path, LOCAL_EXTERNAL_WRITE_POLICY_SECTIONS) + + def load_read_policy(project_root: Path) -> dict[str, list[str]]: policy_path = project_root / ".agent-os" / "read-policy.yaml" if not policy_path.exists(): @@ -806,8 +951,74 @@ def choose_tools(entries: list[dict[str, str]], kind: str, task_type: str, capab return chosen +def runtime_agent_type_for_entry(entry: dict[str, str]) -> str: + explicit = entry.get("runtime_agent_type", "").strip() + if explicit in CODEX_RUNTIME_AGENT_TYPES: + return explicit + capability_fit = set(split_csv(entry.get("capability_fit", ""))) + entry_id = entry.get("id", "") + if entry_id == "codex-default": + return "default" + if entry_id == "codex-explorer": + return "explorer" + if entry_id == "codex-worker": + return "worker" + if entry_id.startswith("maestro-"): + if "full" in capability_fit or "read_write" in capability_fit: + return "worker" + if "read_only" in capability_fit or "read_shell" in capability_fit: + return "explorer" + return "default" + return "" + + +def is_runtime_callable_subagent(entry: dict[str, str]) -> bool: + if entry.get("kind") != "subagent": + return False + runtime_tool = entry.get("runtime_tool", "") + runtime_type = runtime_agent_type_for_entry(entry) + return runtime_tool == CODEX_RUNTIME_TOOL and runtime_type in CODEX_RUNTIME_AGENT_TYPES + + +def select_subagent_candidates(entries: list[dict[str, str]], task_type: str, limit: int = SUBAGENT_CANDIDATE_LIMIT) -> list[dict[str, str]]: + active = [entry for entry in entries if entry.get("kind") == "subagent" and entry.get("status") in ACTIVE_TOOL_STATUSES] + by_id = {entry.get("id", ""): entry for entry in active} + selected: list[dict[str, str]] = [] + + def add(entry: dict[str, str] | None) -> None: + if not entry: + return + if entry in selected: + return + selected.append(entry) + + for preferred_id in SUBAGENT_PREFERENCES_BY_TASK.get(task_type, []): + add(by_id.get(preferred_id)) + if len(selected) >= limit: + return selected[:limit] + + fitted = choose_tools(entries, "subagent", task_type) + fitted.sort( + key=lambda item: ( + 0 if is_runtime_callable_subagent(item) else 1, + 0 if item.get("id", "").startswith("codex-") else 1, + item.get("id", ""), + ) + ) + for entry in fitted: + add(entry) + if len(selected) >= limit: + return selected[:limit] + + for fallback_id in ["codex-default", "maestro-architect", "codex-explorer", "codex-worker"]: + add(by_id.get(fallback_id)) + if len(selected) >= limit: + return selected[:limit] + return selected[:limit] + + def tool_summary(entry: dict[str, str]) -> dict[str, Any]: - return { + summary = { "id": entry.get("id", ""), "kind": entry.get("kind", ""), "status": entry.get("status", ""), @@ -816,6 +1027,62 @@ def tool_summary(entry: dict[str, str]) -> dict[str, Any]: "human_gate": entry.get("human_gate", ""), "execution_mode": entry.get("execution_mode", ""), } + for key in ["runtime", "runtime_tool", "runtime_agent_type", "adapter", "adapter_role", "source_path"]: + value = entry.get(key, "") + if value: + summary[key] = value + if summary.get("kind") == "subagent" and not summary.get("runtime_agent_type"): + inferred = runtime_agent_type_for_entry(entry) + if inferred: + summary["runtime_agent_type"] = inferred + if summary.get("kind") == "subagent": + summary["runtime_callable"] = is_runtime_callable_subagent(entry) + return summary + + +def dispatch_tool_runtime_callable(item: dict[str, Any]) -> bool: + return ( + str(item.get("kind", "")) == "subagent" + and str(item.get("runtime_tool", "")) == CODEX_RUNTIME_TOOL + and str(item.get("runtime_agent_type", "")) in CODEX_RUNTIME_AGENT_TYPES + ) + + +def summarize_dispatch_plan(dispatch: dict[str, Any]) -> dict[str, Any]: + steps = dispatch.get("steps", []) + planned_tools: list[dict[str, Any]] = [] + for step in steps: + if not isinstance(step, dict): + continue + stage = str(step.get("stage", "")) + for tool in step.get("tools", []) or []: + if not isinstance(tool, dict): + continue + planned_tools.append({**tool, "stage": stage, "required": bool(step.get("required"))}) + planned_agents = [ + item + for item in planned_tools + if str(item.get("kind", "")) in AGENT_CAPABILITY_KINDS or str(item.get("stage", "")) in {"branch_builder", "orchestrator", "subagent"} + ] + runtime_callable_agents = [item for item in planned_agents if dispatch_tool_runtime_callable(item)] + required_stages = [str(step.get("stage", "")) for step in steps if isinstance(step, dict) and step.get("required")] + marker = ( + f"AGENT_DISPATCH_PLAN agents={len(planned_agents)} " + f"runtime_callable={len(runtime_callable_agents)} capabilities={len(planned_tools)} required={len(required_stages)}" + ) + return { + "dispatch_marker": "AGENT_DISPATCH_PLAN", + "marker": marker, + "dispatch_summary": { + "planned_agents": len(planned_agents), + "runtime_callable_agents": len(runtime_callable_agents), + "planned_capabilities": len(planned_tools), + "required_stages": required_stages, + "declared_agents": [str(item.get("id", "")) for item in planned_agents if item.get("id")], + "planned_agent_ids": [str(item.get("id", "")) for item in planned_agents if item.get("id")], + "runtime_callable_agent_ids": [str(item.get("id", "")) for item in runtime_callable_agents if item.get("id")], + }, + } def build_dispatch_plan(project_root: Path, task_id: str) -> dict[str, Any]: @@ -858,9 +1125,9 @@ def add_step(stage: str, reason: str, tools: list[dict[str, str]] | None = None, add_step("orchestrator", "coordinate role-specific subagents with execution_mode=ask", orchestrators, required=True) if "subagent" in default_order: - subagents = choose_tools(entries, "subagent", task_type) + subagents = select_subagent_candidates(entries, task_type) if subagents: - add_step("subagent", "use role-specific subagent or planning adapter when registered", subagents) + add_step("subagent", "use up to three role-specific runtime subagent candidates when useful", subagents) if "mcp" in default_order: mcp_tools = choose_tools(entries, "mcp", task_type) @@ -882,7 +1149,7 @@ def add_step(stage: str, reason: str, tools: list[dict[str, str]] | None = None, consult.append(item) consult = sorted(set(consult)) - return { + result = { "status": "dispatch_ready", "task": task, "route": route, @@ -894,6 +1161,8 @@ def add_step(stage: str, reason: str, tools: list[dict[str, str]] | None = None, "agent_opinion_required": True, "agent_opinion_prompt": "Pause before execution, state your recommended next move, name the tradeoff, and ask the human whether to proceed.", } + result.update(summarize_dispatch_plan(result)) + return result def path_for_policy(project_root: Path, target: str) -> tuple[Path, str, bool]: @@ -945,6 +1214,7 @@ def route_output_match(relative: str, allowed_outputs: list[str]) -> str | None: def classify_write(project_root: Path, target: str) -> dict[str, Any]: policy = load_write_policy(project_root) + local_external_policy = load_local_external_write_policy(project_root) absolute, relative, inside = path_for_policy(project_root, target) absolute_value = absolute.as_posix() @@ -969,6 +1239,35 @@ def classify_write(project_root: Path, target: str) -> dict[str, Any]: } if not inside: + external_forbidden_match = matches_any( + absolute_value, + local_external_policy["external_forbidden_without_human_gate"], + ) + if external_forbidden_match: + return { + "decision": "human_gate_required", + "reason": f"matches external_forbidden_without_human_gate pattern {external_forbidden_match}", + "path": absolute_value, + "inside_project": False, + } + + external_controlled_match = matches_any( + absolute_value, + local_external_policy["external_controlled"], + ) + if external_controlled_match: + receipt_match = matches_any( + absolute_value, + local_external_policy["external_require_receipt_for"], + ) + return { + "decision": "allow", + "reason": f"matches external_controlled pattern {external_controlled_match}", + "path": absolute_value, + "inside_project": False, + "receipt_required": bool(receipt_match), + "receipt_pattern": receipt_match, + } return { "decision": "deny", "reason": "path is outside the project root and no explicit allow policy matched", @@ -1015,6 +1314,25 @@ def classify_route_write(project_root: Path, task_id: str, target: str) -> dict[ "route": route, } + if not write_decision.get("inside_project", True): + if str(route.get("allow_external_controlled", "")).lower() == "true": + return { + **write_decision, + "task_id": task_id, + "route_status": "allowed_by_external_route", + "route_output_match": "external_controlled", + "route": route, + } + return { + "decision": "route_output_denied", + "reason": f"path is allowed by local external write policy but task {task_id} route does not allow external controlled writes", + "path": write_decision["path"], + "inside_project": False, + "task_id": task_id, + "allowed_outputs": route.get("allowed_outputs", []), + "route": route, + } + allowed_outputs = route.get("allowed_outputs", []) if not allowed_outputs: return { @@ -1535,6 +1853,7 @@ def write_task_plan(project_root: Path, task_id: str, run_id: str, *, summary: s "- Record public lifecycle checkpoints with phase-task.", "- Record MCP, skill, subagent, orchestrator, or important script use with capability-event.", "- Complete only after eval-task, verify-lifecycle, and postflight pass.", + "- For medium, high, or complex tasks, include a readable FLOW_OK Mission Flow in the final answer.", "", ] write_text(run_dir / "plan.md", "\n".join(lines).rstrip() + "\n") @@ -1686,10 +2005,18 @@ def capability_events_path(run_dir: Path) -> Path: return run_dir / "capability-events.ndjson" +def subagent_catalog_snapshot_path(run_dir: Path) -> Path: + return run_dir / "subagent-catalog.json" + + def effect_assertions_path(run_dir: Path) -> Path: return run_dir / "effect-assertions.ndjson" +def decision_events_path(run_dir: Path) -> Path: + return run_dir / "decision-events.ndjson" + + def step_events_path(run_dir: Path) -> Path: return run_dir / "step-events.ndjson" @@ -1768,124 +2095,541 @@ def load_effect_assertions(run_dir: Path) -> list[dict[str, Any]]: return assertions -def short_marker_value(value: str, limit: int = 96) -> str: - cleaned = " ".join(value.strip().split()) - if len(cleaned) <= limit: - return cleaned - return cleaned[: max(0, limit - 3)].rstrip() + "..." +def load_decision_events(run_dir: Path) -> list[dict[str, Any]]: + path = decision_events_path(run_dir) + if not path.exists(): + return [] + events: list[dict[str, Any]] = [] + for line_number, raw in enumerate(read_text(path).splitlines(), start=1): + if not raw.strip(): + continue + try: + item = json.loads(raw) + except json.JSONDecodeError: + item = {"_invalid_json": raw, "_line": line_number} + events.append(item) + return events -def task_declared_outputs(project_root: Path, task_id: str) -> list[str]: - tasks_path = project_root / ".agent-os" / "tasks.yaml" - if not tasks_path.exists(): - raise FileNotFoundError(f"missing tasks file: {tasks_path}") - return parse_task_list_field(tasks_path, "outputs").get(task_id, []) +def threads_root(project_root: Path) -> Path: + return project_root / ".agent-os" / "threads" -def output_status(project_root: Path, output: str) -> tuple[bool, str]: - cleaned = output.strip().strip('"').strip("'") - if not cleaned: - return True, "empty output ignored" - if cleaned.startswith("~"): - return False, f"{cleaned} uses home-relative path" - candidate = Path(cleaned) - if candidate.is_absolute(): - absolute = candidate.resolve() - detail = absolute.as_posix() - else: - absolute = (project_root / cleaned).resolve() - detail = cleaned - try: - absolute.relative_to(project_root.resolve()) - except ValueError: - return False, f"{detail} escapes project root" - if any(char in cleaned for char in "*?[]"): - matches = list(project_root.glob(cleaned)) - return bool(matches), f"{cleaned} matched {len(matches)} path(s)" - return absolute.exists(), f"{detail} exists" if absolute.exists() else f"{detail} missing" +def current_thread_path(project_root: Path) -> Path: + return threads_root(project_root) / "current.json" -def output_path(project_root: Path, output: str) -> Path: - cleaned = output.strip().strip('"').strip("'") - candidate = Path(cleaned).expanduser() - return candidate.resolve() if candidate.is_absolute() else (project_root / cleaned).resolve() +def thread_dir(project_root: Path, thread_id: str) -> Path: + return threads_root(project_root) / thread_id -def missing_declared_outputs(project_root: Path, task_id: str) -> list[str]: - outputs = task_declared_outputs(project_root, task_id) - missing: list[str] = [] - for output in outputs: - ok, detail = output_status(project_root, output) - if not ok: - missing.append(detail) - return missing +def thread_plan_path(project_root: Path, thread_id: str) -> Path: + return thread_dir(project_root, thread_id) / "thread-plan.ndjson" -def ensure_safe_project_root(project_root: Path) -> None: - resolved = project_root.expanduser().resolve() - if resolved == Path.home() or resolved.parent == resolved or resolved.name in BROAD_PROJECT_NAMES: - raise ValueError(f"refusing broad/container project root: {resolved}") - if any(part in {"Library", ".Trash"} for part in resolved.parts): - raise ValueError(f"refusing private/system-like project root: {resolved}") +def thread_command_events_path(project_root: Path, thread_id: str) -> Path: + return thread_dir(project_root, thread_id) / "command-events.ndjson" -def move_or_delete_path(path: Path, backup_root: Path | None, *, purge: bool, dry_run: bool) -> dict[str, str]: - action = "delete" if purge else "archive" - result = {"action": action, "path": str(path)} - if not path.exists(): - result["action"] = "skip" - result["reason"] = "missing" - return result - if purge: - if not dry_run: - if path.is_dir(): - shutil.rmtree(path) - else: - path.unlink() - return result - if backup_root is None: - raise ValueError("backup_root is required when purge is false") - target = backup_root / path.name - suffix = 1 - while target.exists(): - target = backup_root / f"{path.name}.{suffix}" +def thread_meta_path(project_root: Path, thread_id: str) -> Path: + return thread_dir(project_root, thread_id) / "thread.json" + + +def utc_event_stamp() -> str: + return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ") + + +def make_thread_id(project_root: Path, title: str) -> str: + base = f"THREAD-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}-{safe_slug(title)[:36]}" + candidate = base.rstrip("-") + suffix = 2 + while thread_dir(project_root, candidate).exists(): + candidate = f"{base}-{suffix}".rstrip("-") suffix += 1 - result["target"] = str(target) - if not dry_run: - backup_root.mkdir(parents=True, exist_ok=True) - shutil.move(str(path), str(target)) - return result + return candidate -def set_task_statuses(project_root: Path, from_statuses: set[str], to_status: str, *, dry_run: bool = False) -> list[dict[str, str]]: - tasks = parse_tasks(project_root / ".agent-os" / "tasks.yaml") - actions: list[dict[str, str]] = [] - for task in tasks: - status = task.get("status", "") - task_id = task.get("id", "") - if task_id and status in from_statuses: - actions.append({"action": "set-task-status", "task_id": task_id, "from": status, "to": to_status}) - if not dry_run: - set_task_status(project_root, task_id, to_status) - return actions +def append_thread_command_event(project_root: Path, thread_id: str, event_type: str, **extra: Any) -> None: + record = { + "event_type": event_type, + "thread_id": thread_id, + "generated_by": "knowledgeos", + "timestamp": datetime.now(timezone.utc).isoformat(), + **extra, + } + path = thread_command_events_path(project_root, thread_id) + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n") -def reopen_task( +def load_thread_meta(project_root: Path, thread_id: str) -> dict[str, Any]: + path = thread_meta_path(project_root, thread_id) + if not path.exists(): + raise FileNotFoundError(f"missing thread metadata: {path}") + return json.loads(read_text(path)) + + +def load_current_thread(project_root: Path) -> dict[str, Any]: + path = current_thread_path(project_root) + if not path.exists(): + raise FileNotFoundError(f"missing current thread pointer: {path}") + current = json.loads(read_text(path)) + thread_id = str(current.get("thread_id", "")) + if not thread_id: + raise ValueError("current thread pointer is missing thread_id") + if not thread_dir(project_root, thread_id).exists(): + raise FileNotFoundError(f"current thread directory missing: {thread_id}") + return current + + +def load_thread_events(project_root: Path, thread_id: str) -> list[dict[str, Any]]: + path = thread_plan_path(project_root, thread_id) + if not path.exists(): + return [] + events: list[dict[str, Any]] = [] + for line_number, raw in enumerate(read_text(path).splitlines(), start=1): + if not raw.strip(): + continue + try: + item = json.loads(raw) + except json.JSONDecodeError: + item = {"_invalid_json": raw, "_line": line_number} + events.append(item) + return events + + +def write_thread_current(project_root: Path, meta: dict[str, Any]) -> None: + current = { + "thread_id": meta.get("thread_id", ""), + "title": meta.get("title", ""), + "spec_id": meta.get("spec_id", ""), + "created_at": meta.get("created_at", ""), + "last_updated_at": meta.get("last_updated_at", ""), + } + write_text(current_thread_path(project_root), json.dumps(current, indent=2, ensure_ascii=False) + "\n") + + +def update_thread_meta(project_root: Path, thread_id: str, **updates: Any) -> dict[str, Any]: + meta = load_thread_meta(project_root, thread_id) + meta.update(updates) + meta["last_updated_at"] = datetime.now(timezone.utc).isoformat() + write_text(thread_meta_path(project_root, thread_id), json.dumps(meta, indent=2, ensure_ascii=False) + "\n") + write_thread_current(project_root, meta) + return meta + + +def thread_event_label(text: str, limit: int = 52) -> str: + cleaned = " ".join(str(text).split()) + if len(cleaned) > limit: + cleaned = cleaned[: max(0, limit - 3)].rstrip() + "..." + return cleaned.replace('"', "'").replace("[", "(").replace("]", ")") + + +def append_thread_plan_event( project_root: Path, - task_id: str, *, - status: str, - reason: str, - archive_outputs: bool = False, - purge_outputs: bool = False, - dry_run: bool = False, + thread_id: str, + kind: str, + text: str, + linked_spec_id: str = "", + linked_task_id: str = "", + linked_run_id: str = "", + parent_event_id: str = "", + event_type: str = "thread-plan append", ) -> dict[str, Any]: - ensure_safe_project_root(project_root) - task = find_task(project_root, task_id) - if status not in TASK_STATUSES: - raise ValueError(f"invalid task status: {status}") - if status == "completed": - raise ValueError("reopen-task target status cannot be completed") + if kind not in THREAD_PLAN_EVENT_KINDS: + raise ValueError(f"invalid thread-plan kind: {kind}") + if not text.strip(): + raise ValueError("thread-plan text is required") + if not thread_dir(project_root, thread_id).exists(): + raise FileNotFoundError(f"thread not found: {thread_id}") + event_id = f"TPE-{utc_event_stamp()}-{safe_slug(kind)}" + record = { + "event_id": event_id, + "thread_id": thread_id, + "kind": kind, + "text": text.strip(), + "linked_spec_id": linked_spec_id.strip(), + "linked_task_id": linked_task_id.strip(), + "linked_run_id": linked_run_id.strip(), + "parent_event_id": parent_event_id.strip(), + "timestamp": datetime.now(timezone.utc).isoformat(), + "append_only": True, + } + path = thread_plan_path(project_root, thread_id) + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n") + update_thread_meta(project_root, thread_id) + append_thread_command_event( + project_root, + thread_id, + event_type, + event_id=event_id, + kind=kind, + linked_task_id=linked_task_id.strip(), + linked_run_id=linked_run_id.strip(), + linked_spec_id=linked_spec_id.strip(), + ) + render_thread_plan_markdown(project_root, thread_id) + marker = f"{THREAD_PLAN_MARKER} action=append thread={thread_id} kind={kind}" + return { + "status": "recorded", + "thread_plan_marker": THREAD_PLAN_MARKER, + "marker": marker, + "thread_id": thread_id, + "event_id": event_id, + "ledger": str(path), + "record": record, + } + + +def start_thread_plan(project_root: Path, *, title: str, spec_id: str = "") -> dict[str, Any]: + if not title.strip(): + raise ValueError("thread-plan title is required") + thread_id = make_thread_id(project_root, title) + created_at = datetime.now(timezone.utc).isoformat() + meta = { + "thread_id": thread_id, + "title": title.strip(), + "spec_id": spec_id.strip(), + "created_at": created_at, + "last_updated_at": created_at, + } + thread_dir(project_root, thread_id).mkdir(parents=True, exist_ok=True) + write_text(thread_meta_path(project_root, thread_id), json.dumps(meta, indent=2, ensure_ascii=False) + "\n") + write_thread_current(project_root, meta) + result = append_thread_plan_event( + project_root, + thread_id=thread_id, + kind="plan", + text=f"总体计划:{title.strip()}", + linked_spec_id=spec_id, + event_type="thread-plan start", + ) + result.update({"status": "started", "title": title.strip(), "spec_id": spec_id.strip()}) + result["marker"] = f"{THREAD_PLAN_MARKER} action=start thread={thread_id}" + return result + + +def render_thread_plan_mermaid(project_root: Path, thread_id: str) -> str: + meta = load_thread_meta(project_root, thread_id) + events = [event for event in load_thread_events(project_root, thread_id) if "_invalid_json" not in event] + lines = ["flowchart LR"] + lines.append(f' S["Start: {thread_event_label(str(meta.get("title", thread_id)))}"]') + previous = "S" + branch_anchor = "S" + if not events: + lines.append(' E["No plan notes yet"]') + lines.append(" S --> E") + for index, event in enumerate(events, start=1): + node = f"N{index}" + kind = str(event.get("kind", "summary")) + label = thread_event_label(str(event.get("text", ""))) + lines.append(f' {node}["{label}"]') + if kind == "branch": + lines.append(f" {branch_anchor} -.-> {node}") + else: + lines.append(f" {previous} --> {node}") + previous = node + branch_anchor = node + lines.extend( + [ + "", + " classDef start fill:#E8F3FF,stroke:#2B6CB0,color:#102A43;", + " classDef plan fill:#ECFEFF,stroke:#0891B2,color:#083344;", + " classDef phase fill:#E9FBEF,stroke:#2F855A,color:#123524;", + " classDef branch fill:#FFF7ED,stroke:#EA580C,color:#431407;", + " classDef decision fill:#F3E8FF,stroke:#6B46C1,color:#2D174D;", + " classDef progress fill:#DCFCE7,stroke:#15803D,color:#052E16;", + " class S start;", + ] + ) + classes: dict[str, list[str]] = {"plan": [], "phase": [], "branch": [], "decision": [], "progress": []} + for index, event in enumerate(events, start=1): + kind = str(event.get("kind", "summary")) + css_kind = kind if kind in classes else ("decision" if kind in {"change", "summary"} else "plan") + classes.setdefault(css_kind, []).append(f"N{index}") + for kind, nodes in classes.items(): + if nodes: + lines.append(f" class {','.join(nodes)} {kind};") + return "\n".join(lines) + + +def render_thread_plan_markdown(project_root: Path, thread_id: str) -> dict[str, Any]: + meta = load_thread_meta(project_root, thread_id) + events = [event for event in load_thread_events(project_root, thread_id) if "_invalid_json" not in event] + by_kind: dict[str, list[dict[str, Any]]] = {kind: [] for kind in THREAD_PLAN_EVENT_KINDS} + for event in events: + by_kind.setdefault(str(event.get("kind", "")), []).append(event) + current_line = next((event for event in reversed(events) if str(event.get("kind")) in {"progress", "phase", "decision", "change", "summary"}), None) + lines = [ + f"# Thread Plan {thread_id}", + "", + f"目标:{meta.get('title', '')}", + "", + f"当前工作线:{current_line.get('text', '还没有记录进展。') if current_line else '还没有记录进展。'}", + "", + "## 总体计划", + "", + ] + for event in by_kind.get("plan", []): + lines.append(f"- {event.get('text', '')}") + if not by_kind.get("plan"): + lines.append("- 尚未记录总体计划。") + lines.extend(["", "## Plan A / Plan B", ""]) + branch_events = by_kind.get("branch", []) + if branch_events: + for index, event in enumerate(branch_events, start=1): + label = chr(ord("A") + min(index - 1, 25)) + lines.append(f"- Plan {label}: {event.get('text', '')}") + else: + lines.append("- 当前没有分支计划。") + lines.extend(["", "## Phase A / Phase B", ""]) + phase_events = by_kind.get("phase", []) + if phase_events: + for index, event in enumerate(phase_events, start=1): + label = chr(ord("A") + min(index - 1, 25)) + lines.append(f"- Phase {label}: {event.get('text', '')}") + else: + lines.append("- 当前没有阶段拆分。") + lines.extend(["", "## 当前选择", ""]) + choice_events = by_kind.get("decision", []) + by_kind.get("change", []) + if choice_events: + for event in choice_events: + lines.append(f"- {event.get('text', '')}") + else: + lines.append("- 尚未记录改路或选择。") + lines.extend(["", "## 已完成", ""]) + progress_events = by_kind.get("progress", []) + if progress_events: + for event in progress_events: + lines.append(f"- {event.get('text', '')}") + else: + lines.append("- 尚未记录完成项。") + lines.extend(["", "## 下一步", ""]) + summary_events = by_kind.get("summary", []) + if summary_events: + lines.append(f"- {summary_events[-1].get('text', '')}") + else: + lines.append("- 等待下一轮计划推进。") + lines.extend(["", "## 关联证据", ""]) + linked = [ + event + for event in events + if event.get("linked_spec_id") or event.get("linked_task_id") or event.get("linked_run_id") + ] + if linked: + for event in linked: + parts = [str(event.get(key, "")) for key in ["linked_spec_id", "linked_task_id", "linked_run_id"] if event.get(key)] + lines.append(f"- {event.get('event_id', '')}: {' / '.join(parts)}") + else: + lines.append("- 尚未关联 spec/task/run。") + lines.extend(["", "## Mermaid", "", "```mermaid", render_thread_plan_mermaid(project_root, thread_id), "```", ""]) + source_path = thread_dir(project_root, thread_id) / "thread-plan.md" + markdown = "\n".join(lines).rstrip() + "\n" + write_text(source_path, markdown) + return { + "status": "rendered", + "thread_plan_marker": THREAD_PLAN_MARKER, + "marker": f"{THREAD_PLAN_MARKER} action=render thread={thread_id} format=markdown", + "thread_id": thread_id, + "format": "markdown", + "output": str(source_path), + "markdown": markdown, + "event_count": len(events), + } + + +def render_thread_plan_html(project_root: Path, thread_id: str) -> dict[str, Any]: + markdown_result = render_thread_plan_markdown(project_root, thread_id) + source_path = thread_plan_path(project_root, thread_id) + if not source_path.exists(): + raise FileNotFoundError(source_path) + source_sha = sha256_file(source_path) + mermaid = render_thread_plan_mermaid(project_root, thread_id) + body_html, _title, sections = markdown_to_html_fragment(markdown_result["markdown"], anchor_prefix=safe_slug(thread_id.lower())) + body_html = ( + "
Visual plan map" + f"
{html_escape(mermaid)}
" + "
" + + body_html + ) + output_path = thread_dir(project_root, thread_id) / "thread-map.html" + fragment_path = output_path.with_suffix(".fragment.html") + manifest_path = output_path.with_suffix(".manifest.json") + generated_at = datetime.now(timezone.utc).isoformat() + source_rel = project_relative(project_root, source_path) + fragment_html = build_html_fragment( + kind="thread-plan", + title=f"Thread Plan {thread_id}", + source_rel=source_rel, + source_sha=source_sha, + run_id="not run-bound", + body_html=body_html, + sections=sections, + generated_at=generated_at, + ) + write_text(fragment_path, fragment_html) + full_html = build_html_document( + title=f"Thread Plan {thread_id}", + kind="thread-plan", + body=fragment_html, + source_rel=source_rel, + source_sha=source_sha, + run_id="not run-bound", + generated_at=generated_at, + theme=HTML_DEFAULT_THEME, + ) + write_text(output_path, full_html) + manifest = { + "schema_version": "knowledgeos.html-report.v1", + "kind": "thread-plan", + "title": f"Thread Plan {thread_id}", + "thread_id": thread_id, + "source": source_rel, + "source_sha256": source_sha, + "output": project_relative(project_root, output_path), + "fragment": project_relative(project_root, fragment_path), + "sections": sections, + "generated_at": generated_at, + "html_source_of_truth": False, + "notice": HTML_SOURCE_TRUTH_NOTICE, + } + write_text(manifest_path, json.dumps(manifest, indent=2, ensure_ascii=False) + "\n") + return { + "status": "rendered", + "thread_plan_marker": THREAD_PLAN_MARKER, + "marker": f"{THREAD_PLAN_MARKER} action=render thread={thread_id} format=html", + "thread_id": thread_id, + "format": "html", + "output": str(output_path), + "manifest": str(manifest_path), + "source": str(source_path), + "source_sha256": source_sha, + "html_source_of_truth": False, + } + + +def short_marker_value(value: str, limit: int = 96) -> str: + cleaned = " ".join(value.strip().split()) + if len(cleaned) <= limit: + return cleaned + return cleaned[: max(0, limit - 3)].rstrip() + "..." + + +def task_declared_outputs(project_root: Path, task_id: str) -> list[str]: + tasks_path = project_root / ".agent-os" / "tasks.yaml" + if not tasks_path.exists(): + raise FileNotFoundError(f"missing tasks file: {tasks_path}") + return parse_task_list_field(tasks_path, "outputs").get(task_id, []) + + +def output_status(project_root: Path, output: str) -> tuple[bool, str]: + cleaned = output.strip().strip('"').strip("'") + if not cleaned: + return True, "empty output ignored" + if cleaned.startswith("~"): + return False, f"{cleaned} uses home-relative path" + candidate = Path(cleaned) + if candidate.is_absolute(): + absolute = candidate.resolve() + detail = absolute.as_posix() + else: + absolute = (project_root / cleaned).resolve() + detail = cleaned + try: + absolute.relative_to(project_root.resolve()) + except ValueError: + return False, f"{detail} escapes project root" + if any(char in cleaned for char in "*?[]"): + matches = list(project_root.glob(cleaned)) + return bool(matches), f"{cleaned} matched {len(matches)} path(s)" + return absolute.exists(), f"{detail} exists" if absolute.exists() else f"{detail} missing" + + +def output_path(project_root: Path, output: str) -> Path: + cleaned = output.strip().strip('"').strip("'") + candidate = Path(cleaned).expanduser() + return candidate.resolve() if candidate.is_absolute() else (project_root / cleaned).resolve() + + +def missing_declared_outputs(project_root: Path, task_id: str) -> list[str]: + outputs = task_declared_outputs(project_root, task_id) + missing: list[str] = [] + for output in outputs: + ok, detail = output_status(project_root, output) + if not ok: + missing.append(detail) + return missing + + +def ensure_safe_project_root(project_root: Path) -> None: + resolved = project_root.expanduser().resolve() + if resolved == Path.home() or resolved.parent == resolved or resolved.name in BROAD_PROJECT_NAMES: + raise ValueError(f"refusing broad/container project root: {resolved}") + if any(part in {"Library", ".Trash"} for part in resolved.parts): + raise ValueError(f"refusing private/system-like project root: {resolved}") + + +def move_or_delete_path(path: Path, backup_root: Path | None, *, purge: bool, dry_run: bool) -> dict[str, str]: + action = "delete" if purge else "archive" + result = {"action": action, "path": str(path)} + if not path.exists(): + result["action"] = "skip" + result["reason"] = "missing" + return result + if purge: + if not dry_run: + if path.is_dir(): + shutil.rmtree(path) + else: + path.unlink() + return result + if backup_root is None: + raise ValueError("backup_root is required when purge is false") + target = backup_root / path.name + suffix = 1 + while target.exists(): + target = backup_root / f"{path.name}.{suffix}" + suffix += 1 + result["target"] = str(target) + if not dry_run: + backup_root.mkdir(parents=True, exist_ok=True) + shutil.move(str(path), str(target)) + return result + + +def set_task_statuses(project_root: Path, from_statuses: set[str], to_status: str, *, dry_run: bool = False) -> list[dict[str, str]]: + tasks = parse_tasks(project_root / ".agent-os" / "tasks.yaml") + actions: list[dict[str, str]] = [] + for task in tasks: + status = task.get("status", "") + task_id = task.get("id", "") + if task_id and status in from_statuses: + actions.append({"action": "set-task-status", "task_id": task_id, "from": status, "to": to_status}) + if not dry_run: + set_task_status(project_root, task_id, to_status) + return actions + + +def reopen_task( + project_root: Path, + task_id: str, + *, + status: str, + reason: str, + archive_outputs: bool = False, + purge_outputs: bool = False, + dry_run: bool = False, +) -> dict[str, Any]: + ensure_safe_project_root(project_root) + task = find_task(project_root, task_id) + if status not in TASK_STATUSES: + raise ValueError(f"invalid task status: {status}") + if status == "completed": + raise ValueError("reopen-task target status cannot be completed") backup_root = project_root / ".agent-os" / "backups" / f"reopen-{safe_slug(task_id)}-{now_stamp()}" actions: list[dict[str, str]] = [] @@ -2447,6 +3191,7 @@ def deep_validate_project(project_root: Path, *, allow_placeholders: bool = Fals phases = parse_simple_list_sections(agent_os / "fabric-link.yaml", {"phase_keys"}).get("phase_keys", []) results.append(CheckResult(phases == EXPECTED_PHASE_KEYS, "phase_keys", f"{phases}")) results.extend(validate_phase_policy(project_root)) + results.extend(validate_decision_policy(project_root)) results.extend(validate_effect_policy(project_root)) policy = load_write_policy(project_root) @@ -2640,6 +3385,11 @@ def build_agent_guide(project_root: Path) -> str: "", "Use this checklist before substantial work.", "", + "0. Start every conversation with a visible KnowledgeOS routing judgment.", + " - Relay KOS_DECISION with project state, work class, required flow, and reason.", + " - Use answer-only for simple non-mutating replies, but still state the decision.", + " - Use task, spec, thread-plan, or full lifecycle when work is substantial.", + "", "1. Read the entry contract.", " - AGENTS.md", "", @@ -2649,6 +3399,7 @@ def build_agent_guide(project_root: Path) -> str: " - .agent-os/tasks.yaml", " - .agent-os/specs.yaml", " - .agent-os/phase-policy.yaml", + " - .agent-os/decision-policy.yaml", " - .agent-os/effect-policy.yaml", " - .agent-os/decisions.yaml", " - .agent-os/evals.yaml", @@ -2661,9 +3412,11 @@ def build_agent_guide(project_root: Path) -> str: f" - {bin_path} tool-registry --project-root {project_root}", f" - {bin_path} create-spec --project-root {project_root} --title # when the user asks to create/align spec", f" - {bin_path} align-spec --project-root {project_root} --task-id <task-id>", + f" - {bin_path} thread-plan current --project-root {project_root} # restore the chat-level plan when one exists; relay THREAD_PLAN_OK", + f" - {bin_path} thread-plan start --project-root {project_root} --title <natural-language-goal> # when a long-lived plan/spec starts; relay THREAD_PLAN_OK", f" - {bin_path} create-task --project-root {project_root} --title <title> --type <type> --output <path> --acceptance <check>", f" - {bin_path} route-task --project-root {project_root} --task-id <task-id>", - f" - {bin_path} dispatch-task --project-root {project_root} --task-id <task-id>", + f" - {bin_path} dispatch-task --project-root {project_root} --task-id <task-id>; echo or relay AGENT_DISPATCH_PLAN", f" - {bin_path} check-route-write --project-root {project_root} --task-id <task-id> --path <planned-path>", "", "4. Start work through a run envelope.", @@ -2681,13 +3434,19 @@ def build_agent_guide(project_root: Path) -> str: f" - {bin_path} dispatch-task --project-root {project_root} --task-id <task-id> --run-id <run-id>;", f" - {bin_path} trace-step --project-root {project_root} --task-id <task-id> --run-id <run-id> --step <step> --note <public-trace> --evidence <evidence>; echo or relay TRACE_OK;", f" - {bin_path} phase-task --project-root {project_root} --task-id <task-id> --run-id <run-id> --phase <phase> --status completed --note <public-trace> --evidence <evidence>; echo or relay CHECKPOINT_OK;", - f" - {bin_path} capability-event --project-root {project_root} --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose <purpose> before/after MCP, skill, subagent, orchestrator, or important script use; echo or relay CAPABILITY_OK;", + f" - {bin_path} capability-event --project-root {project_root} --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose <purpose> before/after MCP, skill, plugin/app, browser/Chrome/GitHub/security connector, subagent, orchestrator, shell, file_read, or important script use; echo or relay CAPABILITY_OK;", + f" - {bin_path} dispatch-report --project-root {project_root} --task-id <task-id> --run-id <run-id>; echo or relay AGENT_DISPATCH_OK as the full capability dispatch report, not only subagent usage;", + f" - {bin_path} decision-event --project-root {project_root} --task-id <task-id> --run-id <run-id> --kind <kind> --title <title> --summary <summary> --reason <reason> --evidence <evidence>; echo or relay DECISION_OK when the plan branches, changes, rolls back, or abandons a route;", + f" - {bin_path} thread-plan append --project-root {project_root} --thread-id <thread-id> --kind <plan|phase|branch|decision|progress|change|summary> --text <natural-language-note> when the chat-level plan changes or advances; relay THREAD_PLAN_OK;", + f" - {bin_path} thread-plan link-run --project-root {project_root} --thread-id <thread-id> --task-id <task-id> --run-id <run-id> to connect a run to the long-lived plan;", f" - {bin_path} artifact-assert --project-root {project_root} --task-id <task-id> --run-id <run-id> --kind <kind> --path <artifact>; echo or relay EFFECT_OK;", f" - {bin_path} eval-task --project-root {project_root} --task-id <task-id> --run-id <run-id>;", f" - {bin_path} verify-context --project-root {project_root} --task-id <task-id> --run-id <run-id>;", f" - {bin_path} verify-lifecycle --project-root {project_root} --task-id <task-id> --run-id <run-id>;", f" - {bin_path} verify-effects --project-root {project_root} --task-id <task-id> --run-id <run-id>; echo or relay EFFECT_VERIFY_OK;", + f" - {bin_path} verify-decisions --project-root {project_root} --task-id <task-id> --run-id <run-id>; echo or relay DECISION_VERIFY_OK;", f" - {bin_path} complete-task --project-root {project_root} --task-id <task-id> --run-id <run-id> --summary <summary>", + f" - for medium, high, or complex tasks, include the returned FLOW_OK Mermaid Mission Flow; if needed run {bin_path} flow-summary --project-root {project_root} --run-id <run-id>.", "", "7. For shared-fabric hosts, finish with canonical postflight.", " - report [SYNC_OK] only after postflight succeeds.", @@ -2716,31 +3475,36 @@ def build_startup_prompt(project_root: Path) -> str: "", "Before substantial work:", "", + "0. At the start of every conversation, make a visible KnowledgeOS judgment and relay `KOS_DECISION` with project state (`managed` or `unmanaged`), work class (`simple`, `substantial`, or `blocked`), required flow (`answer-only`, `task`, `spec`, `thread-plan`, or `full lifecycle`), and reason. This is a public routing decision, not hidden reasoning.", "1. Read `AGENTS.md`.", - "2. Read `.agent-os/workspace.yaml`, `.agent-os/project.yaml`, `.agent-os/tasks.yaml`, `.agent-os/specs.yaml`, `.agent-os/phase-policy.yaml`, `.agent-os/effect-policy.yaml`, `.agent-os/decisions.yaml`, `.agent-os/evals.yaml`, `.agent-os/fabric-link.yaml`, `.agent-os/read-policy.yaml`, `.agent-os/write-policy.yaml`, `.agent-os/dispatch-policy.yaml`, and `.agent-os/tool-registry.yaml`.", + "2. Read `.agent-os/workspace.yaml`, `.agent-os/project.yaml`, `.agent-os/tasks.yaml`, `.agent-os/specs.yaml`, `.agent-os/phase-policy.yaml`, `.agent-os/decision-policy.yaml`, `.agent-os/effect-policy.yaml`, `.agent-os/decisions.yaml`, `.agent-os/evals.yaml`, `.agent-os/fabric-link.yaml`, `.agent-os/read-policy.yaml`, `.agent-os/write-policy.yaml`, `.agent-os/dispatch-policy.yaml`, and `.agent-os/tool-registry.yaml`.", f"3. Run `{bin_path} doctor --project-root {project_root} --summary` and do not proceed if it fails.", f"4. If the user says `create spec`, `align spec`, `对齐spec`, or equivalent, run `{bin_path} create-spec --project-root {project_root} --title \"<title>\"` or `{bin_path} align-spec --project-root {project_root} --task-id <task-id>` before execution.", - f"5. Select or confirm one task id from `.agent-os/tasks.yaml`; if the user asks for new work and no ready task fits, run `{bin_path} create-task --project-root {project_root} --title \"<title>\" --type <type> --output <path> --acceptance \"<check>\"`.", - f"6. Run `{bin_path} route-task --project-root {project_root} --task-id <task-id>`.", - f"7. Run `{bin_path} dispatch-task --project-root {project_root} --task-id <task-id>` before invoking subagents, MCP tools, skills, workflows, or scripts.", - f"8. Before planned mutation, run `{bin_path} check-route-write --project-root {project_root} --task-id <task-id> --path <planned-path>`.", - f"9. Create run evidence with `{bin_path} run-task --project-root {project_root} --task-id <task-id>`; this writes `spec-snapshot.md` and `context-pack.md`.", - f"10. Write/update the execution context with `{bin_path} context-pack --project-root {project_root} --task-id <task-id> --run-id <run-id>` and `{bin_path} plan-task --project-root {project_root} --task-id <task-id> --run-id <run-id>`.", - "11. Pause at consultation checkpoints, state your recommended next move, name the tradeoff, and ask the human whether to proceed.", - f"12. Record dispatch evidence with `{bin_path} dispatch-task --project-root {project_root} --task-id <task-id> --run-id <run-id>` after the run exists.", - f"13. Record public operational progress with `{bin_path} trace-step --project-root {project_root} --task-id <task-id> --run-id <run-id> --step <step> --note \"<public trace>\" --evidence \"<command/file/user confirmation>\"`; relay the returned `TRACE_OK` marker.", - f"14. Record public phase evidence with `{bin_path} phase-task --project-root {project_root} --task-id <task-id> --run-id <run-id> --phase <route|plan|review|dispatch|execute|report> --status completed --note \"<public trace>\" --evidence \"<command/file/user confirmation>\"`; relay the returned `CHECKPOINT_OK` marker.", - f"15. Record MCP, skill, subagent, orchestrator, or important script use with `{bin_path} capability-event --project-root {project_root} --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose \"<purpose>\"`; relay the returned `CAPABILITY_OK` marker.", - f"16. Verify real side effects with `{bin_path} artifact-assert --project-root {project_root} --task-id <task-id> --run-id <run-id> --kind <kind> --path <artifact>`; relay the returned `EFFECT_OK` marker.", - f"17. Run `{bin_path} eval-task --project-root {project_root} --task-id <task-id> --run-id <run-id>`; do not manually append eval status.", - f"18. Run `{bin_path} verify-context --project-root {project_root} --task-id <task-id> --run-id <run-id>`, `{bin_path} verify-lifecycle --project-root {project_root} --task-id <task-id> --run-id <run-id>`, and `{bin_path} verify-effects --project-root {project_root} --task-id <task-id> --run-id <run-id>`; relay the returned `EFFECT_VERIFY_OK` marker before claiming effect verification success.", - f"19. Use `{bin_path} complete-task --project-root {project_root} --task-id <task-id> --run-id <run-id> --summary \"<summary>\"`; it must enforce spec/context/plan, lifecycle, capability visibility, effect verification, and required postflight.", - "20. If a shared-fabric postflight hook is configured, report `[SYNC_OK]` only after `complete-task` returns `sync_status: SYNC_OK`.", - f"21. For reset requests, run `{bin_path} reset-project --project-root {project_root} --mode <soft|hard> --dry-run` before destructive action.", - f"22. For old-project reorganization requests, run `{bin_path} migrate-legacy-project --project-root {project_root} --write-plan` before moving files.", - f"23. For historical/superseded files that should be stored but not read by default, run `{bin_path} archive-legacy-project --project-root {project_root} --write-plan` before moving files into `archive/`.", + f"5. If the user starts a durable plan/spec conversation, run `{bin_path} thread-plan current --project-root {project_root}` or `{bin_path} thread-plan start --project-root {project_root} --title \"<natural language goal>\"`; append natural-language progress with `{bin_path} thread-plan append --project-root {project_root} --thread-id <thread-id> --kind <kind> --text \"<plain note>\"` when the plan changes or advances, and relay `THREAD_PLAN_OK`.", + f"6. Select or confirm one task id from `.agent-os/tasks.yaml`; if the user asks for new work and no ready task fits, run `{bin_path} create-task --project-root {project_root} --title \"<title>\" --type <type> --output <path> --acceptance \"<check>\"`.", + f"7. Run `{bin_path} route-task --project-root {project_root} --task-id <task-id>`.", + f"8. Run `{bin_path} dispatch-task --project-root {project_root} --task-id <task-id>` before invoking subagents, MCP tools, skills, workflows, or scripts; relay `AGENT_DISPATCH_PLAN`.", + f"9. Before planned mutation, run `{bin_path} check-route-write --project-root {project_root} --task-id <task-id> --path <planned-path>`.", + f"10. Create run evidence with `{bin_path} run-task --project-root {project_root} --task-id <task-id>`; this writes `spec-snapshot.md` and `context-pack.md`.", + f"11. Write/update the execution context with `{bin_path} context-pack --project-root {project_root} --task-id <task-id> --run-id <run-id>` and `{bin_path} plan-task --project-root {project_root} --task-id <task-id> --run-id <run-id>`.", + "12. Pause at consultation checkpoints, state your recommended next move, name the tradeoff, and ask the human whether to proceed.", + f"13. Record dispatch evidence with `{bin_path} dispatch-task --project-root {project_root} --task-id <task-id> --run-id <run-id>` after the run exists.", + f"14. Record public operational progress with `{bin_path} trace-step --project-root {project_root} --task-id <task-id> --run-id <run-id> --step <step> --note \"<public trace>\" --evidence \"<command/file/user confirmation>\"`; relay the returned `TRACE_OK` marker.", + f"15. Record public phase evidence with `{bin_path} phase-task --project-root {project_root} --task-id <task-id> --run-id <run-id> --phase <route|plan|review|dispatch|execute|report> --status completed --note \"<public trace>\" --evidence \"<command/file/user confirmation>\"`; relay the returned `CHECKPOINT_OK` marker.", + f"16. Record MCP, skill, plugin/app, browser/Chrome/GitHub/security connector, subagent, orchestrator, shell, file_read, or important script use with `{bin_path} capability-event --project-root {project_root} --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose \"<purpose>\"`; relay the returned `CAPABILITY_OK` marker.", + f"17. Summarize actual capability dispatch with `{bin_path} dispatch-report --project-root {project_root} --task-id <task-id> --run-id <run-id>`; relay `AGENT_DISPATCH_OK`. Treat it as a full capability report: agents invoked/skipped, MCP, skills, plugins/apps, browser/Chrome/GitHub/security connectors, scripts, shell, file reads, evidence files, and gaps. If no subagent was used, still report `agents=0` and explain why.", + f"18. Record public decision changes with `{bin_path} decision-event --project-root {project_root} --task-id <task-id> --run-id <run-id> --kind <kind> --title \"<title>\" --summary \"<summary>\" --reason \"<reason>\" --evidence \"<evidence>\"`; relay the returned `DECISION_OK` marker when plans branch, change, roll back, or abandon a route.", + f"19. Verify real side effects with `{bin_path} artifact-assert --project-root {project_root} --task-id <task-id> --run-id <run-id> --kind <kind> --path <artifact>`; relay the returned `EFFECT_OK` marker.", + f"20. Run `{bin_path} eval-task --project-root {project_root} --task-id <task-id> --run-id <run-id>`; do not manually append eval status.", + f"21. Run `{bin_path} verify-context --project-root {project_root} --task-id <task-id> --run-id <run-id>`, `{bin_path} verify-lifecycle --project-root {project_root} --task-id <task-id> --run-id <run-id>`, `{bin_path} verify-effects --project-root {project_root} --task-id <task-id> --run-id <run-id>`, and `{bin_path} verify-decisions --project-root {project_root} --task-id <task-id> --run-id <run-id>`; relay `EFFECT_VERIFY_OK` and `DECISION_VERIFY_OK` before claiming verification success.", + f"22. Use `{bin_path} complete-task --project-root {project_root} --task-id <task-id> --run-id <run-id> --summary \"<summary>\"`; it must enforce spec/context/plan, lifecycle, capability visibility, effect verification, decision verification, visible dispatch reporting, and required postflight.", + f"23. For medium, high, or complex tasks, include the returned `FLOW_OK` Mermaid Mission Flow in the final answer; if needed, run `{bin_path} flow-summary --project-root {project_root} --run-id <run-id>`.", + "24. If a shared-fabric postflight hook is configured, report `[SYNC_OK]` only after `complete-task` returns `sync_status: SYNC_OK`.", + f"25. For reset requests, run `{bin_path} reset-project --project-root {project_root} --mode <soft|hard> --dry-run` before destructive action.", + f"26. For old-project reorganization requests, run `{bin_path} migrate-legacy-project --project-root {project_root} --write-plan` before moving files.", + f"27. For historical/superseded files that should be stored but not read by default, run `{bin_path} archive-legacy-project --project-root {project_root} --write-plan` before moving files into `archive/`.", "", - "Never claim boot, route, dispatch, write safety, spec alignment, context pack, plan, trace, checkpoint, capability, effect, eval, completion, or sync success without command evidence.", + "Never skip the initial `KOS_DECISION`. Never claim boot, route, dispatch, write safety, spec alignment, thread plan, context pack, plan, trace, checkpoint, capability, agent dispatch, decision, effect, eval, completion, flow, or sync success without command evidence.", "", ] ) @@ -2866,6 +3630,31 @@ def build_runtime_adapters_state(project_root: Path, *, show_paths: bool = False } ) + runtime_subagents: list[dict[str, Any]] = [] + try: + registry_entries = load_tool_registry(project_root) + except FileNotFoundError: + registry_entries = [] + for entry in registry_entries: + if entry.get("kind") != "subagent": + continue + if entry.get("status") not in ACTIVE_TOOL_STATUSES: + continue + if not is_runtime_callable_subagent(entry): + continue + runtime_subagents.append( + { + "id": entry.get("id", ""), + "status": "registered", + "runtime": entry.get("runtime", "codex"), + "runtime_tool": entry.get("runtime_tool", ""), + "runtime_agent_type": runtime_agent_type_for_entry(entry), + "adapter": entry.get("adapter", ""), + "execution_mode": entry.get("execution_mode", ""), + "human_gate": entry.get("human_gate", ""), + } + ) + missing_required = [item["id"] for item in adapters if item["status"] != "available" and not item["optional"]] available_count = sum(1 for item in adapters if item["status"] == "available") return { @@ -2877,9 +3666,11 @@ def build_runtime_adapters_state(project_root: Path, *, show_paths: bool = False "available_count": available_count, "total": len(adapters), "adapters": adapters, + "runtime_subagents": sorted(runtime_subagents, key=lambda item: item["id"]), "policy": { "default_runtime": "mock", "real_cli_execution": "disabled_until_adapter_phase", + "native_subagent_execution": "delegated_to_codex_runtime_tool", "project_cwd_allowed": False, "project_mutation_allowed": False, "os_route_required_for_mutation": True, @@ -2887,6 +3678,168 @@ def build_runtime_adapters_state(project_root: Path, *, show_paths: bool = False } +def maestro_role_spec_path(subagent_id: str) -> Path: + root = knowledgeos_root_from_file() + runtime_path = root / "capability-layer" / "subagents" / "maestro" / f"{subagent_id}.yaml" + if runtime_path.exists(): + return runtime_path + return root / "templates" / "capability-layer" / "subagents" / "maestro" / f"{subagent_id}.yaml" + + +def load_maestro_role_spec(subagent_id: str) -> dict[str, str]: + path = maestro_role_spec_path(subagent_id) + if not path.exists(): + return {} + keys = {"id", "display_name", "runtime_agent_type", "default_scope", "write_policy_hint", "role_prompt", "recommended_task_fit"} + return parse_scalar_values(path, keys) + + +def build_default_subagent_role_prompt(entry: dict[str, str]) -> str: + subagent_id = entry.get("id", "") + runtime_type = runtime_agent_type_for_entry(entry) or "default" + if subagent_id.startswith("maestro-"): + readable = subagent_id.removeprefix("maestro-").replace("-", " ") + return ( + f"Act as the Maestro {readable} adapter. Use the {runtime_type} Codex subagent runtime. " + "Return concise findings, concrete evidence, and any gaps. Do not mutate files unless the parent task route allows it." + ) + return ( + f"Act as {subagent_id} through the Codex {runtime_type} subagent runtime. " + "Return concise findings, concrete evidence, and any gaps." + ) + + +def active_runtime_subagent_catalog(project_root: Path) -> dict[str, str]: + catalog = { + entry.get("id", ""): runtime_agent_type_for_entry(entry) + for entry in load_tool_registry(project_root) + if entry.get("kind") == "subagent" + and entry.get("status") in ACTIVE_TOOL_STATUSES + and is_runtime_callable_subagent(entry) + } + catalog.pop("", None) + return catalog + + +def standard_runtime_subagent_catalog() -> dict[str, str]: + catalog = {item["id"]: item["runtime_agent_type"] for item in CODEX_NATIVE_SUBAGENTS} + root = knowledgeos_root_from_file() + role_dir = root / "capability-layer" / "subagents" / "maestro" + if not role_dir.exists(): + role_dir = root / "templates" / "capability-layer" / "subagents" / "maestro" + for path in sorted(role_dir.glob("maestro-*.yaml")): + values = parse_scalar_values(path, {"id", "runtime_agent_type"}) + subagent_id = values.get("id", "") + runtime_type = values.get("runtime_agent_type", "") + if subagent_id and runtime_type in CODEX_RUNTIME_AGENT_TYPES: + catalog[subagent_id] = runtime_type + return catalog + + +def subagent_catalog_sha256(catalog: dict[str, str]) -> str: + payload = json.dumps(catalog, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(payload.encode("utf-8")).hexdigest() + + +def subagent_snapshot_integrity_sha256(snapshot: dict[str, Any]) -> str: + payload = {key: value for key, value in snapshot.items() if key != "snapshot_integrity_sha256"} + serialized = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(serialized.encode("utf-8")).hexdigest() + + +def ensure_subagent_catalog_snapshot(run_dir: Path, task_id: str, run_id: str, catalog: dict[str, str]) -> dict[str, Any]: + path = subagent_catalog_snapshot_path(run_dir) + if path.exists(): + snapshot = json.loads(read_text(path)) + return snapshot + required_catalog = standard_runtime_subagent_catalog() + expected_catalog = dict(required_catalog) + expected_catalog.update({subagent_id: runtime_type for subagent_id, runtime_type in catalog.items() if subagent_id not in expected_catalog}) + snapshot = { + "schema_version": "knowledgeos.subagent-catalog.v1", + "task_id": task_id, + "run_id": run_id, + "roles": dict(sorted(expected_catalog.items())), + "catalog_sha256": subagent_catalog_sha256(expected_catalog), + "captured_at": datetime.now(timezone.utc).isoformat(), + "evidence_model": "parent_attested_runtime", + } + snapshot["snapshot_integrity_sha256"] = subagent_snapshot_integrity_sha256(snapshot) + path.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + append_command_event( + run_dir, + "subagent-catalog-snapshot", + task_id, + run_id, + status="recorded", + catalog_sha256=snapshot["catalog_sha256"], + role_count=len(expected_catalog), + ) + return snapshot + + +def build_subagent_adapter(project_root: Path, subagent_id: str, *, task_id: str = "", run_id: str = "", purpose: str = "") -> dict[str, Any]: + entries = load_tool_registry(project_root) + entry = next((item for item in entries if item.get("id") == subagent_id), None) + if not entry: + raise KeyError(f"unknown subagent id: {subagent_id}") + if entry.get("kind") != "subagent": + raise ValueError(f"{subagent_id} is not a subagent entry") + runtime_type = runtime_agent_type_for_entry(entry) + if runtime_type not in CODEX_RUNTIME_AGENT_TYPES: + raise ValueError(f"{subagent_id} does not declare a supported runtime_agent_type") + runtime_tool = entry.get("runtime_tool", "") + if runtime_tool != CODEX_RUNTIME_TOOL: + raise ValueError(f"{subagent_id} is not backed by {CODEX_RUNTIME_TOOL}") + role_spec = load_maestro_role_spec(subagent_id) if subagent_id.startswith("maestro-") else {} + role_prompt = role_spec.get("role_prompt") or build_default_subagent_role_prompt(entry) + role_prompt = f"{role_prompt}\n\n{SUBAGENT_RUNTIME_BOUNDARY}" + capability_event_suggestion = ( + f"knowledgeos capability-event --project-root . --task-id {task_id or '<task-id>'} " + f"--run-id {run_id or '<run-id>'} --kind subagent --id {subagent_id} " + f"--purpose \"{purpose or 'Codex runtime subagent delegated work'}\"" + ) + runtime_challenge = "" + catalog_sha256 = "" + if run_id and task_id: + run_dir = ensure_run_belongs_to_task(project_root, task_id, run_id) + snapshot = ensure_subagent_catalog_snapshot(run_dir, task_id, run_id, active_runtime_subagent_catalog(project_root)) + catalog_sha256 = str(snapshot.get("catalog_sha256", "")) + challenge_source = f"{task_id}:{run_id}:{subagent_id}:{time.time_ns()}:{os.urandom(16).hex()}" + runtime_challenge = hashlib.sha256(challenge_source.encode("utf-8")).hexdigest()[:24] + append_command_event( + run_dir, + "subagent-adapter", + task_id, + run_id, + status="ready", + subagent_id=subagent_id, + runtime_tool=runtime_tool, + runtime_agent_type=runtime_type, + runtime_challenge=runtime_challenge, + catalog_sha256=catalog_sha256, + ) + return { + "status": "ready", + "subagent_adapter_marker": "SUBAGENT_ADAPTER_OK", + "marker": f"SUBAGENT_ADAPTER_OK id={subagent_id} runtime_agent_type={runtime_type}", + "id": subagent_id, + "runtime": entry.get("runtime", "codex"), + "runtime_tool": runtime_tool, + "runtime_agent_type": runtime_type, + "runtime_challenge": runtime_challenge, + "catalog_sha256": catalog_sha256, + "evidence_model": "parent_attested_runtime", + "host_runtime_verified": False, + "adapter": entry.get("adapter", ""), + "adapter_role": entry.get("adapter_role", ""), + "role_prompt": role_prompt, + "role_spec": f"capability-layer/subagents/maestro/{subagent_id}.yaml" if subagent_id.startswith("maestro-") else "", + "capability_event_suggestion": capability_event_suggestion, + "actual_execution": "call Codex runtime tool multi_agent_v1.spawn_agent with runtime_agent_type and role_prompt", + } + + def build_workbench_state(project_root: Path, *, show_paths: bool = False, skip_external_checks: bool = False) -> dict[str, Any]: project_root = project_root.expanduser().resolve() generated_at = datetime.now(timezone.utc).isoformat() @@ -3470,6 +4423,22 @@ def record_task_phase( def record_dispatch_event(project_root: Path, task_id: str, run_id: str, dispatch: dict[str, Any]) -> dict[str, Any]: run_dir = ensure_run_belongs_to_task(project_root, task_id, run_id) required_stages = [step.get("stage", "") for step in dispatch.get("steps", []) if step.get("required")] + planned_tools: list[dict[str, str]] = [] + for step in dispatch.get("steps", []): + stage = str(step.get("stage", "")) + for tool in step.get("tools", []) or []: + if isinstance(tool, dict): + planned_tools.append( + { + "stage": stage, + "kind": str(tool.get("kind", "")), + "id": str(tool.get("id", "")), + "required": str(bool(step.get("required"))).lower(), + "runtime_tool": str(tool.get("runtime_tool", "")), + "runtime_agent_type": str(tool.get("runtime_agent_type", "")), + "runtime_callable": str(bool(tool.get("runtime_callable"))).lower(), + } + ) append_command_event( run_dir, "dispatch-task", @@ -3477,6 +4446,7 @@ def record_dispatch_event(project_root: Path, task_id: str, run_id: str, dispatc run_id, status=str(dispatch.get("status", "")), required_stages=required_stages, + planned_tools=planned_tools, ) return {"run_id": run_id, "required_stages": required_stages, "command_events": str(command_events_path(run_dir))} @@ -3491,6 +4461,8 @@ def record_capability_event( purpose: str, status: str = "completed", evidence: str = "", + invocation_id: str = "", + recovers_event_id: str = "", ) -> dict[str, Any]: run_dir = ensure_run_belongs_to_task(project_root, task_id, run_id) if kind not in CAPABILITY_EVENT_KINDS: @@ -3516,6 +4488,10 @@ def record_capability_event( "evidence": evidence.strip(), "timestamp": datetime.now(timezone.utc).isoformat(), } + if invocation_id.strip(): + record["invocation_id"] = invocation_id.strip() + if recovers_event_id.strip(): + record["recovers_event_id"] = recovers_event_id.strip() event_path = capability_events_path(run_dir) with event_path.open("a", encoding="utf-8") as handle: handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n") @@ -3540,22 +4516,520 @@ def record_capability_event( } -def record_trace_step( +def verify_subagent_catalog( project_root: Path, task_id: str, run_id: str, *, - step: str, - status: str = "completed", - note: str = "", - evidence: str = "", + native_min_successes: int = DEFAULT_NATIVE_SUBAGENT_SUCCESSES, ) -> dict[str, Any]: + if native_min_successes < DEFAULT_NATIVE_SUBAGENT_SUCCESSES: + raise ValueError( + f"--native-min-successes cannot be lower than {DEFAULT_NATIVE_SUBAGENT_SUCCESSES}" + ) run_dir = ensure_run_belongs_to_task(project_root, task_id, run_id) - if step not in OPERATIONAL_TRACE_STEPS: - raise ValueError(f"invalid step {step!r}; expected one of {OPERATIONAL_TRACE_STEPS}") - if not note.strip(): - raise ValueError("--note is required") - record = { + current_catalog = active_runtime_subagent_catalog(project_root) + snapshot_path = subagent_catalog_snapshot_path(run_dir) + snapshot: dict[str, Any] = {} + snapshot_error = "" + if snapshot_path.exists(): + try: + snapshot = json.loads(read_text(snapshot_path)) + except (json.JSONDecodeError, OSError) as exc: + snapshot_error = f"invalid_catalog_snapshot: {exc}" + else: + snapshot_error = "missing_catalog_snapshot" + registered = { + str(subagent_id): str(runtime_type) + for subagent_id, runtime_type in (snapshot.get("roles", {}) if isinstance(snapshot.get("roles", {}), dict) else {}).items() + } + snapshot_sha256 = str(snapshot.get("catalog_sha256", "")) + validation_started_at = str(snapshot.get("captured_at", "")) + expected_snapshot_sha256 = subagent_catalog_sha256(registered) if registered else "" + required_snapshot_fields = { + "schema_version": "knowledgeos.subagent-catalog.v1", + "task_id": task_id, + "run_id": run_id, + } + for field, expected in required_snapshot_fields.items(): + if snapshot and snapshot.get(field) != expected: + snapshot_error = f"catalog_snapshot_{field}_mismatch" + break + if snapshot and not validation_started_at: + snapshot_error = "catalog_snapshot_missing_captured_at" + elif snapshot and snapshot.get("evidence_model") != "parent_attested_runtime": + snapshot_error = "catalog_snapshot_evidence_model_mismatch" + elif snapshot and not snapshot_sha256: + snapshot_error = "catalog_snapshot_missing_hash" + elif snapshot_sha256 != expected_snapshot_sha256: + snapshot_error = "catalog_snapshot_hash_mismatch" + elif snapshot: + try: + captured_at = datetime.fromisoformat(validation_started_at) + if captured_at.tzinfo is None: + raise ValueError("timezone required") + except ValueError: + snapshot_error = "catalog_snapshot_invalid_captured_at" + if snapshot and not snapshot_error: + observed_integrity = str(snapshot.get("snapshot_integrity_sha256", "")) + if not observed_integrity: + snapshot_error = "catalog_snapshot_missing_integrity" + elif observed_integrity != subagent_snapshot_integrity_sha256(snapshot): + snapshot_error = "catalog_snapshot_integrity_mismatch" + required_catalog = standard_runtime_subagent_catalog() + missing_standard_roles = sorted(set(required_catalog) - set(registered)) + standard_type_mismatches = { + subagent_id: {"expected": runtime_type, "observed": registered.get(subagent_id, "")} + for subagent_id, runtime_type in required_catalog.items() + if registered.get(subagent_id) != runtime_type + } + catalog_drift = bool(registered) and current_catalog != registered + native_ids = { + item["id"] + for item in CODEX_NATIVE_SUBAGENTS + if item["id"] in registered + } + adapter_role_ids: set[str] = set() + adapter_mismatches: list[dict[str, str]] = [] + adapter_challenges: dict[str, set[str]] = {subagent_id: set() for subagent_id in registered} + for event in load_command_events(run_dir): + if event.get("generated_by") != "knowledgeos" or event.get("event_type") != "subagent-adapter": + continue + if event.get("task_id") != task_id or event.get("run_id") != run_id or event.get("status") != "ready": + continue + if validation_started_at and str(event.get("timestamp", "")) <= validation_started_at: + continue + subagent_id = str(event.get("subagent_id", "")) + if subagent_id not in registered: + continue + observed_type = str(event.get("runtime_agent_type", "")) + event_catalog_sha256 = str(event.get("catalog_sha256", "")) + runtime_challenge = str(event.get("runtime_challenge", "")) + if observed_type == registered[subagent_id] and event_catalog_sha256 == snapshot_sha256 and runtime_challenge: + adapter_role_ids.add(subagent_id) + adapter_challenges[subagent_id].add(runtime_challenge) + else: + adapter_mismatches.append( + { + "id": subagent_id, + "expected_runtime_agent_type": registered[subagent_id], + "observed_runtime_agent_type": observed_type, + "catalog_sha256": event_catalog_sha256, + } + ) + missing_adapter_role_ids = sorted(set(registered) - adapter_role_ids) + success_nonces: dict[str, set[str]] = {subagent_id: set() for subagent_id in registered} + invalid_events: list[dict[str, str]] = [] + consumed_challenges: set[str] = set() + ignored_reused_challenge_count = 0 + for event in load_capability_events(run_dir): + if "_invalid_json" in event or event.get("kind") != "subagent": + continue + if validation_started_at and str(event.get("timestamp", "")) <= validation_started_at: + continue + evidence = str(event.get("evidence", "")) + if SUBAGENT_SMOKE_MARKER not in evidence: + continue + subagent_id = str(event.get("id", "")) + reason = "" + if subagent_id not in registered: + reason = "unknown_runtime_subagent" + elif str(event.get("status", "")).strip().lower() != "completed": + reason = "status_not_completed" + else: + marker_match = re.search( + rf"(?:^|;\s*){re.escape(SUBAGENT_SMOKE_MARKER)} id={re.escape(subagent_id)}(?: round=\d+)? nonce=([A-Za-z0-9._-]+)(?:;|$)", + evidence, + ) + ordered_tokens = [token.strip() for token in evidence.split(";")] + tokens = set(ordered_tokens) + challenge_tokens = [token for token in ordered_tokens if token.startswith("adapter_challenge=")] + observed_challenge = challenge_tokens[0].partition("=")[2] if len(challenge_tokens) == 1 else "" + if not marker_match: + reason = "marker_or_role_id_mismatch" + elif "cleanup=completed" not in tokens: + reason = "missing_cleanup" if not any(token.startswith("cleanup=") for token in tokens) else "cleanup_not_completed" + elif "role_contract=passed" not in tokens: + reason = "missing_role_contract" if not any(token.startswith("role_contract=") for token in tokens) else "role_contract_not_passed" + elif not challenge_tokens: + reason = "missing_adapter_challenge" + elif len(challenge_tokens) != 1 or not observed_challenge: + reason = "duplicate_or_malformed_adapter_challenge" + elif observed_challenge not in adapter_challenges.get(subagent_id, set()): + reason = "adapter_challenge_mismatch" + elif observed_challenge in consumed_challenges: + ignored_reused_challenge_count += 1 + else: + consumed_challenges.add(observed_challenge) + success_nonces[subagent_id].add(marker_match.group(1)) + if reason: + invalid_events.append( + { + "capability_event_id": str(event.get("capability_event_id", "")), + "id": subagent_id, + "reason": reason, + } + ) + + covered_role_ids = sorted(subagent_id for subagent_id, nonces in success_nonces.items() if nonces) + missing_role_ids = sorted(set(registered) - set(covered_role_ids)) + success_counts = {subagent_id: len(success_nonces[subagent_id]) for subagent_id in sorted(registered)} + native_shortfalls = { + subagent_id: success_counts[subagent_id] + for subagent_id in sorted(native_ids) + if success_counts[subagent_id] < native_min_successes + } + catalog_empty = not registered + passed = ( + not catalog_empty + and not snapshot_error + and not catalog_drift + and not missing_standard_roles + and not standard_type_mismatches + and not missing_adapter_role_ids + and not adapter_mismatches + and not missing_role_ids + and not native_shortfalls + and not invalid_events + ) + marker_name = SUBAGENT_CATALOG_MARKER if passed else "SUBAGENT_CATALOG_FAIL" + marker = ( + f"{marker_name} roles={len(covered_role_ids)}/{len(registered)} " + f"native_min={native_min_successes} invalid={len(invalid_events)}" + ) + return { + "schema_version": "knowledgeos.verify-subagents.v1", + "status": "passed" if passed else "failed", + "subagent_catalog_marker": SUBAGENT_CATALOG_MARKER if passed else "", + "marker": marker, + "task_id": task_id, + "run_id": run_id, + "registered_role_count": len(registered), + "covered_role_count": len(covered_role_ids), + "registered_role_ids": sorted(registered), + "catalog_empty": catalog_empty, + "catalog_snapshot": str(snapshot_path), + "catalog_sha256": snapshot_sha256, + "validation_started_at": validation_started_at, + "catalog_snapshot_error": snapshot_error, + "catalog_drift": catalog_drift, + "current_role_ids": sorted(current_catalog), + "missing_standard_roles": missing_standard_roles, + "standard_type_mismatches": standard_type_mismatches, + "evidence_model": "parent_attested_runtime", + "host_runtime_verified": False, + "adapter_role_ids": sorted(adapter_role_ids), + "missing_adapter_role_ids": missing_adapter_role_ids, + "adapter_mismatches": adapter_mismatches, + "covered_role_ids": covered_role_ids, + "missing_role_ids": missing_role_ids, + "success_counts": success_counts, + "native_role_ids": sorted(native_ids), + "native_min_successes": native_min_successes, + "native_shortfalls": native_shortfalls, + "invalid_events": invalid_events, + "ignored_reused_challenge_count": ignored_reused_challenge_count, + "ledger": str(capability_events_path(run_dir)), + } + + +def build_dispatch_report(project_root: Path, task_id: str, run_id: str) -> dict[str, Any]: + run_dir = ensure_run_belongs_to_task(project_root, task_id, run_id) + events = [event for event in load_capability_events(run_dir) if "_invalid_json" not in event] + invoked_events = [ + event + for event in events + if str(event.get("status", "completed")).strip().lower() not in SKIPPED_CAPABILITY_STATUSES + ] + skipped_events = [ + event + for event in events + if str(event.get("status", "completed")).strip().lower() in SKIPPED_CAPABILITY_STATUSES + ] + agent_events = [event for event in invoked_events if str(event.get("kind", "")) in AGENT_CAPABILITY_KINDS] + skipped_agent_events = [event for event in skipped_events if str(event.get("kind", "")) in AGENT_CAPABILITY_KINDS] + unique_agent_keys = {(str(event.get("kind", "")), str(event.get("id", ""))) for event in agent_events} + unique_skipped_agent_keys = {(str(event.get("kind", "")), str(event.get("id", ""))) for event in skipped_agent_events} + observed_runtime_gap_events = [ + event + for event in skipped_agent_events + if str(event.get("status", "")).strip().lower() in RUNTIME_GAP_CAPABILITY_STATUSES + ] + runtime_gap_events: list[dict[str, Any]] = [] + resolved_runtime_gaps: list[dict[str, Any]] = [] + consumed_recovery_event_ids: set[str] = set() + for gap_event in observed_runtime_gap_events: + gap_status = str(gap_event.get("status", "")).strip().lower().replace("-", "_") + if gap_status in {"timeout", "timed_out"}: + gap_status = "timed_out" + recovery = next( + ( + event + for event in agent_events + if event.get("kind") == gap_event.get("kind") + and event.get("id") == gap_event.get("id") + and event.get("purpose") == gap_event.get("purpose") + and str(event.get("status", "")).strip().lower() == "completed" + and str(event.get("timestamp", "")) > str(gap_event.get("timestamp", "")) + and event.get("recovers_event_id") == gap_event.get("capability_event_id") + and str(event.get("capability_event_id", "")) not in consumed_recovery_event_ids + ), + None, + ) + if recovery: + consumed_recovery_event_ids.add(str(recovery.get("capability_event_id", ""))) + resolved_runtime_gaps.append({"gap": gap_event, "recovery": recovery}) + else: + runtime_gap_events.append(gap_event) + counts_by_kind: dict[str, int] = {kind: 0 for kind in sorted(CAPABILITY_EVENT_KINDS)} + for event in invoked_events: + kind = str(event.get("kind", "unknown")) or "unknown" + counts_by_kind[kind] = counts_by_kind.get(kind, 0) + 1 + skipped_by_kind: dict[str, int] = {kind: 0 for kind in sorted(CAPABILITY_EVENT_KINDS)} + for event in skipped_events: + kind = str(event.get("kind", "unknown")) or "unknown" + skipped_by_kind[kind] = skipped_by_kind.get(kind, 0) + 1 + command_events = load_command_events(run_dir) + dispatch_events = [ + event + for event in command_events + if event.get("generated_by") == "knowledgeos" + and event.get("event_type") == "dispatch-task" + and event.get("task_id") == task_id + and event.get("run_id") == run_id + ] + latest_dispatch_event = dispatch_events[-1] if dispatch_events else {} + required_stages = [str(item) for item in latest_dispatch_event.get("required_stages", []) or []] + planned_tools = latest_dispatch_event.get("planned_tools", []) or [] + def normalized_token(value: str) -> str: + return value.strip().lower().replace("_", "-") + + recorded_stage_tokens = { + normalized_token(str(event.get("kind", ""))) + for event in events + } | { + normalized_token(str(event.get("id", ""))) + for event in events + } + planned_tools_by_stage: dict[str, list[dict[str, Any]]] = {} + for item in planned_tools: + if isinstance(item, dict): + planned_tools_by_stage.setdefault(str(item.get("stage", "")), []).append(item) + def required_stage_has_record(stage: str) -> bool: + normalized_stage = normalized_token(stage) + if normalized_stage in recorded_stage_tokens: + return True + for item in planned_tools_by_stage.get(stage, []): + if normalized_token(str(item.get("kind", ""))) in recorded_stage_tokens: + return True + if normalized_token(str(item.get("id", ""))) in recorded_stage_tokens: + return True + return any(normalized_stage in normalized_token(str(event.get("purpose", ""))) for event in events) + + required_without_record = [ + stage + for stage in required_stages + if stage and not required_stage_has_record(stage) + ] + gaps: list[str] = [] + if not events: + gaps.append("no capability-event records were written; report no external capability use with an explicit reason") + if required_without_record: + gaps.append("required dispatch stages without capability-event or skip reason: " + ", ".join(required_without_record)) + if runtime_gap_events: + gaps.append( + "runtime subagent gaps: " + + ", ".join( + f"{event.get('id', '')}={event.get('status', '')}" + for event in runtime_gap_events + ) + ) + if not gaps: + gaps.append("none") + marker = f"AGENT_DISPATCH_OK agents={len(unique_agent_keys)} capabilities={len(invoked_events)} skipped_agents={len(unique_skipped_agent_keys)} run={run_id}" + report_path = run_dir / "dispatch-report.md" + lines = [ + "# Full Capability Dispatch Report", + "", + f"Run: {run_id}", + "", + f"Task: {task_id}", + "", + f"Marker: {marker}", + "", + "Meaning: AGENT_DISPATCH_OK summarizes all recorded external and mounted capabilities, not only subagents.", + "", + f"Agents Invoked: {len(unique_agent_keys)} unique agents across {len(agent_events)} events", + "", + f"Agents Skipped Or Gapped: {len(unique_skipped_agent_keys)} unique agents across {len(skipped_agent_events)} events", + "", + f"Capabilities Invoked: {len(invoked_events)}", + "", + f"Capabilities Skipped: {len(skipped_events)}", + "", + f"Evidence: `{project_relative(project_root, capability_events_path(run_dir))}`, `{project_relative(project_root, command_events_path(run_dir))}`", + "", + "## By Kind", + "", + ] + for kind, count in sorted(counts_by_kind.items()): + skipped = skipped_by_kind.get(kind, 0) + lines.append(f"- {kind}: used={count}, skipped={skipped}") + lines.extend(["", "## Used Capabilities", ""]) + if invoked_events: + for event in invoked_events: + kind = str(event.get("kind", "unknown")) + capability_id = str(event.get("id", "")) + purpose = str(event.get("purpose", "")) + status = str(event.get("status", "completed")) + lines.append(f"- {kind}: {capability_id} ({status}) - {purpose}") + else: + lines.append("- none") + lines.extend(["", "## Skipped Or Not Needed", ""]) + if skipped_events: + for event in skipped_events: + kind = str(event.get("kind", "unknown")) + capability_id = str(event.get("id", "")) + purpose = str(event.get("purpose", "")) + evidence = str(event.get("evidence", "")) + suffix = f" Evidence: {evidence}" if evidence else "" + lines.append(f"- {kind}: {capability_id} - {purpose}.{suffix}") + else: + lines.append("- none") + lines.extend(["", "## Dispatch Plan Evidence", ""]) + if planned_tools: + for item in planned_tools: + if isinstance(item, dict): + lines.append(f"- {item.get('stage', '')}: {item.get('kind', '')}/{item.get('id', '')} required={item.get('required', '')}") + else: + lines.append("- no run-bound dispatch plan was recorded") + lines.extend(["", "## Gaps", ""]) + for gap in gaps: + lines.append(f"- {gap}") + lines.extend(["", "## Resolved Runtime Gaps", ""]) + if resolved_runtime_gaps: + for item in resolved_runtime_gaps: + gap_event = item["gap"] + lines.append(f"- {gap_event.get('id', '')}: {gap_event.get('status', '')} -> completed") + else: + lines.append("- none") + write_text(report_path, "\n".join(lines) + "\n") + append_command_event( + run_dir, + "dispatch-report", + task_id, + run_id, + status="reported", + agent_count=len(unique_agent_keys), + agent_event_count=len(agent_events), + capability_count=len(invoked_events), + skipped_count=len(skipped_events), + skipped_agent_count=len(unique_skipped_agent_keys), + skipped_agent_event_count=len(skipped_agent_events), + runtime_gap_count=len(runtime_gap_events), + resolved_runtime_gap_count=len(resolved_runtime_gaps), + ) + return { + "status": "reported", + "dispatch_report_marker": "AGENT_DISPATCH_OK", + "marker": marker, + "full_capability_report": True, + "agent_count": len(unique_agent_keys), + "agent_event_count": len(agent_events), + "skipped_agent_count": len(unique_skipped_agent_keys), + "skipped_agent_event_count": len(skipped_agent_events), + "runtime_gap_count": len(runtime_gap_events), + "resolved_runtime_gap_count": len(resolved_runtime_gaps), + "capability_count": len(invoked_events), + "skipped_count": len(skipped_events), + "counts_by_kind": counts_by_kind, + "skipped_by_kind": skipped_by_kind, + "required_stages": required_stages, + "required_without_record": required_without_record, + "gaps": gaps, + "evidence": { + "capability_events": str(capability_events_path(run_dir)), + "command_events": str(command_events_path(run_dir)), + "dispatch_report": str(report_path), + }, + "agents": [ + { + "kind": str(event.get("kind", "")), + "id": str(event.get("id", "")), + "purpose": str(event.get("purpose", "")), + "status": str(event.get("status", "completed")), + } + for event in agent_events + ], + "skipped_agents": [ + { + "kind": str(event.get("kind", "")), + "id": str(event.get("id", "")), + "purpose": str(event.get("purpose", "")), + "status": str(event.get("status", "skipped")), + "evidence": str(event.get("evidence", "")), + } + for event in skipped_agent_events + ], + "runtime_gaps": [ + { + "kind": str(event.get("kind", "")), + "id": str(event.get("id", "")), + "purpose": str(event.get("purpose", "")), + "status": str(event.get("status", "")), + "evidence": str(event.get("evidence", "")), + } + for event in runtime_gap_events + ], + "resolved_runtime_gaps": [ + { + "id": str(item["gap"].get("id", "")), + "purpose": str(item["gap"].get("purpose", "")), + "status": str(item["gap"].get("status", "")), + "recovery_status": str(item["recovery"].get("status", "completed")), + "recovery_evidence": str(item["recovery"].get("evidence", "")), + } + for item in resolved_runtime_gaps + ], + "events": [ + { + "kind": str(event.get("kind", "")), + "id": str(event.get("id", "")), + "purpose": str(event.get("purpose", "")), + "status": str(event.get("status", "completed")), + } + for event in invoked_events + ], + "skipped": [ + { + "kind": str(event.get("kind", "")), + "id": str(event.get("id", "")), + "purpose": str(event.get("purpose", "")), + "status": str(event.get("status", "skipped")), + "evidence": str(event.get("evidence", "")), + } + for event in skipped_events + ], + "report": str(report_path), + } + + +def record_trace_step( + project_root: Path, + task_id: str, + run_id: str, + *, + step: str, + status: str = "completed", + note: str = "", + evidence: str = "", +) -> dict[str, Any]: + run_dir = ensure_run_belongs_to_task(project_root, task_id, run_id) + if step not in OPERATIONAL_TRACE_STEPS: + raise ValueError(f"invalid step {step!r}; expected one of {OPERATIONAL_TRACE_STEPS}") + if not note.strip(): + raise ValueError("--note is required") + record = { "task_id": task_id, "run_id": run_id, "step": step, @@ -3572,6 +5046,264 @@ def record_trace_step( return {"status": "recorded", "trace_marker": "TRACE_OK", "marker": marker, "ledger": str(event_path), "record": record} +def effect_assertion_id_exists(run_dir: Path, assertion_id: str, task_id: str, run_id: str) -> bool: + return any( + assertion.get("assertion_id") == assertion_id + and assertion.get("task_id") == task_id + and assertion.get("run_id") == run_id + and assertion.get("status") == "passed" + for assertion in load_effect_assertions(run_dir) + if "_invalid_json" not in assertion + ) + + +def record_decision_event( + project_root: Path, + task_id: str, + run_id: str, + *, + kind: str, + status: str, + title: str, + summary: str, + reason: str, + evidence: str, + parent_id: str = "", + options: list[str] | None = None, + chosen: str = "", + linked_step: str = "", + linked_capability_event_id: str = "", + linked_effect_assertion_id: str = "", +) -> dict[str, Any]: + run_dir = ensure_run_belongs_to_task(project_root, task_id, run_id) + if kind not in DECISION_EVENT_KINDS: + raise ValueError(f"invalid decision kind {kind!r}; expected one of {sorted(DECISION_EVENT_KINDS)}") + if status not in DECISION_EVENT_STATUSES: + raise ValueError(f"invalid decision status {status!r}; expected one of {sorted(DECISION_EVENT_STATUSES)}") + for label, value in [("--title", title), ("--summary", summary), ("--reason", reason), ("--evidence", evidence)]: + if not value.strip(): + raise ValueError(f"{label} is required") + if linked_step and linked_step not in OPERATIONAL_TRACE_STEPS: + raise ValueError(f"invalid linked step {linked_step!r}; expected one of {OPERATIONAL_TRACE_STEPS}") + capability_link = linked_capability_event_id.strip() + if capability_link and not capability_event_id_exists(run_dir, capability_link, task_id, run_id): + raise ValueError(f"capability event not found for decision: {capability_link}") + effect_link = linked_effect_assertion_id.strip() + if effect_link and not effect_assertion_id_exists(run_dir, effect_link, task_id, run_id): + raise ValueError(f"effect assertion not found for decision: {effect_link}") + + decision_id = ( + "DEC-" + + datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ") + + "-" + + safe_slug(f"{kind}-{title.strip()}")[:48] + ) + clean_options = [item.strip() for item in (options or []) if item.strip()] + record = { + "decision_id": decision_id, + "parent_id": parent_id.strip(), + "task_id": task_id, + "run_id": run_id, + "kind": kind, + "status": status, + "title": title.strip(), + "summary": summary.strip(), + "reason": reason.strip(), + "options": clean_options, + "chosen": chosen.strip(), + "evidence": evidence.strip(), + "linked_step": linked_step.strip(), + "linked_capability_event_id": capability_link, + "linked_effect_assertion_id": effect_link, + "timestamp": datetime.now(timezone.utc).isoformat(), + } + event_path = decision_events_path(run_dir) + with event_path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n") + append_command_event( + run_dir, + "decision-event", + task_id, + run_id, + decision_id=decision_id, + kind=kind, + status=status, + ) + marker = f"DECISION_OK kind={kind} status={status} title={short_marker_value(title)}" + return { + "status": "recorded", + "decision_marker": "DECISION_OK", + "decision_id": decision_id, + "marker": marker, + "ledger": str(event_path), + "record": record, + } + + +def query_decision_events( + project_root: Path, + *, + run_id: str, + task_id: str = "", + kind: str = "", + status: str = "", + parent_id: str = "", +) -> dict[str, Any]: + run_dir = resolve_run_dir(project_root, run_id) + events = [event for event in load_decision_events(run_dir) if "_invalid_json" not in event] + if task_id: + events = [event for event in events if event.get("task_id") == task_id] + if kind: + events = [event for event in events if event.get("kind") == kind] + if status: + events = [event for event in events if event.get("status") == status] + if parent_id: + events = [event for event in events if event.get("parent_id") == parent_id] + return { + "status": "ok", + "run_id": run_id, + "count": len(events), + "events": events, + "ledger": str(decision_events_path(run_dir)), + } + + +def decision_event_has_command_event(run_dir: Path, event: dict[str, Any], task_id: str, run_id: str) -> bool: + return has_command_event( + run_dir, + "decision-event", + task_id, + run_id, + decision_id=str(event.get("decision_id", "")), + kind=str(event.get("kind", "")), + status=str(event.get("status", "")), + ) + + +def build_decision_verify_marker(result: dict[str, Any]) -> str: + return ( + "DECISION_VERIFY_OK " + f"status={result.get('status', '')} " + f"strictness={result.get('strictness', '')} " + f"decisions={len(result.get('events', []))} " + f"warnings={len(result.get('warnings', []))} " + f"errors={len(result.get('errors', []))}" + ) + + +def verify_decisions(project_root: Path, task_id: str, run_id: str) -> dict[str, Any]: + policy = parse_decision_policy(project_root) + strictness = str(policy.get("strictness", "warn")) + if strictness not in DECISION_STRICTNESS_LEVELS: + raise ValueError(f"invalid decision strictness: {strictness}") + run_dir = ensure_run_belongs_to_task(project_root, task_id, run_id) + errors: list[dict[str, Any]] = [] + warnings: list[dict[str, Any]] = [] + + if strictness == "off": + reason = str(policy.get("downgrade_reason", "")).strip() + if not reason: + errors.append({"label": "missing_downgrade_reason", "detail": "strictness=off requires downgrade_reason"}) + result = { + "status": "failed" if errors else "disabled", + "task_id": task_id, + "run_id": run_id, + "strictness": strictness, + "downgrade_reason": reason, + "events": [], + "errors": errors, + "warnings": warnings, + "ledger": str(decision_events_path(run_dir)), + } + result["decision_verify_marker"] = "DECISION_VERIFY_OK" + result["marker"] = build_decision_verify_marker(result) + append_command_event( + run_dir, + "verify-decisions", + task_id, + run_id, + status=str(result["status"]), + strictness=strictness, + decision_verify_marker=result["decision_verify_marker"], + marker=result["marker"], + ) + return result + + raw_events = load_decision_events(run_dir) + valid_events: list[dict[str, Any]] = [] + seen_ids: set[str] = set() + duplicate_ids: set[str] = set() + parent_ids: set[str] = set() + for event in raw_events: + if "_invalid_json" in event: + errors.append({"label": "invalid_decision_event", "detail": f"line {event.get('_line')} is not JSON"}) + continue + if event.get("task_id") != task_id or event.get("run_id") != run_id: + errors.append({"label": "decision_scope_mismatch", "detail": event}) + continue + decision_id = str(event.get("decision_id", "")).strip() + if not decision_id: + errors.append({"label": "decision_missing_id", "detail": event.get("title", "")}) + continue + if decision_id in seen_ids: + duplicate_ids.add(decision_id) + seen_ids.add(decision_id) + for field in ["title", "summary", "reason", "evidence"]: + if not str(event.get(field, "")).strip(): + errors.append({"label": f"decision_missing_{field}", "detail": decision_id}) + kind = str(event.get("kind", "")) + status = str(event.get("status", "")) + if kind not in DECISION_EVENT_KINDS: + errors.append({"label": "invalid_decision_kind", "detail": kind}) + if status not in DECISION_EVENT_STATUSES: + errors.append({"label": "invalid_decision_status", "detail": status}) + if (kind in DECISION_EXPLANATION_REQUIRED_KINDS or status in {"abandoned", "rolled_back", "superseded"}) and not str(event.get("reason", "")).strip(): + errors.append({"label": "decision_missing_explanation", "detail": decision_id}) + if not decision_event_has_command_event(run_dir, event, task_id, run_id): + errors.append({"label": "decision_missing_command_event", "detail": decision_id}) + parent = str(event.get("parent_id", "")).strip() + if parent: + parent_ids.add(parent) + valid_events.append(event) + for decision_id in sorted(duplicate_ids): + errors.append({"label": "decision_duplicate_id", "detail": decision_id}) + for parent in sorted(parent_ids): + if parent not in seen_ids: + errors.append({"label": "decision_orphan_parent", "detail": parent}) + + if not valid_events: + if strictness == "enforce": + errors.append({"label": "no_decision_events", "detail": "decision-policy strictness=enforce requires decision evidence"}) + else: + warnings.append({"label": "no_decision_events", "detail": "no decision graph events were recorded"}) + + status = "failed" if errors else ("warning" if warnings else "passed") + result = { + "status": status, + "task_id": task_id, + "run_id": run_id, + "strictness": strictness, + "downgrade_reason": str(policy.get("downgrade_reason", "")).strip(), + "events": valid_events, + "errors": errors, + "warnings": warnings, + "ledger": str(decision_events_path(run_dir)), + } + result["decision_verify_marker"] = "DECISION_VERIFY_OK" + result["marker"] = build_decision_verify_marker(result) + append_command_event( + run_dir, + "verify-decisions", + task_id, + run_id, + status=status, + strictness=strictness, + decision_verify_marker=result["decision_verify_marker"], + marker=result["marker"], + ) + return result + + def get_json_key(data: Any, key_path: str) -> Any: current = data for part in key_path.split("."): @@ -4372,6 +6104,81 @@ def ensure_archive_write_guard(project_root: Path, *, apply: bool, backup_name: return action +def registry_block_from_entry(entry: dict[str, str]) -> str: + ordered_keys = [ + "kind", + "status", + "scope", + "invocation", + "task_fit", + "capability_fit", + "runtime", + "runtime_tool", + "runtime_agent_type", + "adapter", + "adapter_role", + "human_gate", + "execution_mode", + "source_path", + "notes", + ] + lines = [f" - id: {entry['id']}"] + for key in ordered_keys: + value = entry.get(key, "") + if value != "": + lines.append(f" {key}: {value}") + return "\n".join(lines) + + +def codex_native_registry_entries() -> list[dict[str, str]]: + entries: list[dict[str, str]] = [] + for item in CODEX_NATIVE_SUBAGENTS: + entries.append( + { + "id": item["id"], + "kind": "subagent", + "status": "recommended", + "scope": item["scope"], + "invocation": "via_codex_runtime_spawn_agent", + "task_fit": item["task_fit"], + "capability_fit": item["capability_fit"], + "runtime": "codex", + "runtime_tool": CODEX_RUNTIME_TOOL, + "runtime_agent_type": item["runtime_agent_type"], + "human_gate": "true", + "execution_mode": "ask", + "source_path": f"codex-runtime:{CODEX_RUNTIME_TOOL}", + "notes": "Codex native runtime subagent. Dispatch plan generates intent; actual spawn happens through Codex runtime.", + } + ) + return entries + + +def ensure_runtime_native_subagents(project_root: Path, *, apply: bool, backup_name: str) -> dict[str, Any] | None: + registry_path = project_root / ".agent-os" / "tool-registry.yaml" + if not registry_path.exists(): + return None + existing_ids = {entry.get("id", "") for entry in load_tool_registry(project_root)} + missing = [entry for entry in codex_native_registry_entries() if entry["id"] not in existing_ids] + if not missing: + return None + action: dict[str, Any] = { + "action": "add_runtime_native_subagents", + "path": str(registry_path), + "missing_ids": [entry["id"] for entry in missing], + "backup": "", + } + if apply: + backup = backup_project_file(project_root, registry_path, backup_name) + action["backup"] = str(backup) + addition = "\n\n # Codex native runtime subagents. Actual spawn is performed by Codex runtime tools.\n" + "\n\n".join( + registry_block_from_entry(entry) for entry in missing + ) + current = read_text(registry_path).rstrip() + write_text(registry_path, current + addition + "\n") + return action + + def audit_project_mount( project_root: Path, root: Path, @@ -4397,8 +6204,14 @@ def audit_project_mount( ) current_governance = fabric.get("governance_root", "") current_capability = fabric.get("capability_root") or fabric.get("implementation_root", "") - if "Antigravity_Skills" in current_governance: - issues.append("legacy_antigravity_governance_root") + current_governance_path = Path(current_governance).expanduser() if current_governance else None + if ( + current_governance_path + and project_root != root + and current_governance_path.name == "global-agent-fabric" + and current_governance_path != governance_root + ): + issues.append("legacy_preos_governance_root") if current_governance and Path(current_governance).expanduser() != governance_root and project_root != root: issues.append("noncanonical_governance_root") if current_capability and Path(current_capability).expanduser() != capability_root and project_root != root: @@ -4417,9 +6230,12 @@ def audit_project_mount( archive_action = ensure_archive_write_guard(project_root, apply=False, backup_name=backup_name) if archive_action: issues.append("missing_archive_write_guard") + native_subagents_action = ensure_runtime_native_subagents(project_root, apply=False, backup_name=backup_name) + if native_subagents_action: + issues.append("missing_runtime_native_subagents") should_rewrite_mount = project_root != root and ( - "legacy_antigravity_governance_root" in issues + "legacy_preos_governance_root" in issues or "noncanonical_governance_root" in issues or "noncanonical_capability_root" in issues or "postflight_hook_missing_or_not_executable" in issues @@ -4454,6 +6270,9 @@ def audit_project_mount( archive_action = ensure_archive_write_guard(project_root, apply=apply, backup_name=backup_name) if archive_action: actions.append(archive_action) + native_subagents_action = ensure_runtime_native_subagents(project_root, apply=apply, backup_name=backup_name) + if native_subagents_action: + actions.append(native_subagents_action) # Re-read after optional repair so the reported final status reflects the # actual mount state rather than the pre-repair state. @@ -4637,11 +6456,23 @@ def complete_task( effects = verify_effects(project_root, task_id, run_id) if effects.get("status") == "failed": raise ValueError("effect verification failed: " + json.dumps(effects.get("errors", []), ensure_ascii=False)) + decisions = verify_decisions(project_root, task_id, run_id) + if decisions.get("status") == "failed": + raise ValueError("decision verification failed: " + json.dumps(decisions.get("errors", []), ensure_ascii=False)) + dispatch_report = build_dispatch_report(project_root, task_id, run_id) postflight = run_postflight_gate(project_root, run_dir, summary, allow_pending_postflight) - completed_at = datetime.now(timezone.utc).isoformat() - receipt_lines = [ - "# Receipt", + mission_flow: dict[str, Any] | None = None + if task.get("complexity", "medium") in MISSION_FLOW_COMPLEXITIES: + mission_flow = write_mission_flow_markdown( + project_root, + task_id, + run_id, + sync_status=str(postflight.get("sync_status", "")), + ) + completed_at = datetime.now(timezone.utc).isoformat() + receipt_lines = [ + "# Receipt", "", f"Run: {run_id}", "", @@ -4661,6 +6492,24 @@ def complete_task( "", f"Effect Verify Marker: {effects.get('marker', '')}", "", + f"Decision Verification Status: {decisions.get('status')}", + "", + f"Decision Verify Marker: {decisions.get('marker', '')}", + "", + f"Agent Dispatch Status: {dispatch_report.get('dispatch_report_marker', '')}", + "", + f"Agent Dispatch Marker: {dispatch_report.get('marker', '')}", + "", + f"Agents Invoked: {dispatch_report.get('agent_count', 0)}", + "", + f"Capabilities Invoked: {dispatch_report.get('capability_count', 0)}", + "", + f"Agent Dispatch Report: {dispatch_report.get('report', '')}", + "", + f"Mission Flow Marker: {mission_flow.get('marker', '') if mission_flow else 'not required'}", + "", + f"Mission Flow: {mission_flow.get('source', '') if mission_flow else 'not required for simple task'}", + "", f"Sync Status: {postflight.get('sync_status')}", "", ] @@ -4682,6 +6531,24 @@ def complete_task( "", ] ) + if decisions.get("strictness") == "off": + receipt_lines.extend( + [ + "DECISION_STRICTNESS_DOWNGRADED:", + "", + str(decisions.get("downgrade_reason", "")), + "", + ] + ) + if decisions.get("warnings"): + receipt_lines.extend( + [ + "Decision Warnings:", + "", + json.dumps(decisions.get("warnings", []), ensure_ascii=False), + "", + ] + ) if postflight.get("status_marker"): receipt_lines.extend([f"Status Marker: {postflight['status_marker']}", ""]) if postflight.get("pending_reason"): @@ -4726,6 +6593,17 @@ def complete_task( "context_contract_status": context_contract.get("status"), "effect_status": effects.get("status"), "effect_verify_marker": effects.get("marker", ""), + "decision_status": decisions.get("status"), + "decision_verify_marker": decisions.get("marker", ""), + "agent_dispatch_status": dispatch_report.get("dispatch_report_marker", ""), + "agent_dispatch_marker": dispatch_report.get("marker", ""), + "agents_invoked": dispatch_report.get("agent_count", 0), + "capabilities_invoked": dispatch_report.get("capability_count", 0), + "dispatch_report": dispatch_report.get("report", ""), + "flow_marker": mission_flow.get("flow_marker", "") if mission_flow else "", + "flow_summary_marker": mission_flow.get("marker", "") if mission_flow else "", + "flow_mermaid": mission_flow.get("mermaid", "") if mission_flow else "", + "flow_source": mission_flow.get("source", "") if mission_flow else "", "sync_status": postflight.get("sync_status"), "status_marker": postflight.get("status_marker", ""), "postflight": postflight.get("postflight", ""), @@ -4734,9 +6612,12 @@ def complete_task( } -HTML_REPORT_KINDS = {"receipt", "handoff", "rich-report"} +HTML_REPORT_KINDS = {"receipt", "handoff", "rich-report", "decision-map", "mission-flow"} HTML_DEFAULT_THEME = "knowledgeos-default" +HTML_PRESENTATION_MODES = {"default", "minimal", "bare", "fragment"} +HTML_DEFAULT_PRESENTATION = "default" HTML_SOURCE_TRUTH_NOTICE = "HTML is presentation, not source of truth." +MISSION_FLOW_COMPLEXITIES = {"medium", "high", "complex"} def sha256_file(path: Path) -> str: @@ -4766,6 +6647,42 @@ def html_escape(text: Any) -> str: return html_lib.escape(str(text), quote=True) +def parse_reporting_policy(project_root: Path) -> dict[str, Any]: + project_yaml = project_root / ".agent-os" / "project.yaml" + if not project_yaml.exists(): + return { + "html_sidecars": True, + "html_source_of_truth": False, + "html_presentation_default": HTML_DEFAULT_PRESENTATION, + "html_required_metadata": True, + } + scalars = parse_scalar_values( + project_yaml, + { + "html_sidecars", + "html_source_of_truth", + "html_presentation_default", + "html_required_metadata", + }, + ) + presentation = (scalars.get("html_presentation_default") or HTML_DEFAULT_PRESENTATION).lower() + return { + "html_sidecars": scalars.get("html_sidecars", "true").lower() != "false", + "html_source_of_truth": scalars.get("html_source_of_truth", "false").lower() == "true", + "html_presentation_default": presentation, + "html_required_metadata": scalars.get("html_required_metadata", "true").lower() != "false", + } + + +def resolve_html_presentation(project_root: Path, requested: str = "") -> str: + presentation = (requested or "").strip().lower() + if not presentation: + presentation = str(parse_reporting_policy(project_root).get("html_presentation_default") or HTML_DEFAULT_PRESENTATION) + if presentation not in HTML_PRESENTATION_MODES: + raise ValueError(f"unsupported HTML presentation mode: {presentation}") + return presentation + + def markdown_heading_anchor(title: str, used: set[str]) -> str: base = re.sub(r"[^a-z0-9\u4e00-\u9fff]+", "-", title.lower()).strip("-") if not base: @@ -4915,6 +6832,20 @@ def html_report_css(theme: str) -> str: summary {{ cursor: pointer; font-weight: 800; }} .kos-footer {{ margin-top: 28px; color: var(--kos-muted); font-size: 0.9rem; text-align: center; }} .kos-notice {{ color: var(--kos-warn); font-weight: 800; }} +.kos-flow {{ display: grid; gap: 14px; margin: 20px 0; }} +.kos-flow-card {{ + border: 1px solid var(--kos-line); + border-radius: 18px; + padding: 16px 18px; + background: #f8fbff; +}} +.kos-flow-card h3 {{ margin: 0 0 6px; }} +.kos-flow-card p {{ margin: 0; }} +.kos-flow-intent {{ background: #e8f3ff; border-color: #b8d9ff; }} +.kos-flow-guard {{ background: #fff7df; border-color: #f3d58a; }} +.kos-flow-work {{ background: #eafbf0; border-color: #b8e7c7; }} +.kos-flow-proof {{ background: #f3e8ff; border-color: #d8b4fe; }} +.kos-flow-done {{ background: #dcfce7; border-color: #86efac; }} @media (max-width: 860px) {{ .kos-shell {{ padding: 28px 14px; }} .kos-grid {{ grid-template-columns: 1fr; }} @@ -4935,6 +6866,7 @@ def build_html_fragment( body_html: str, sections: list[dict[str, Any]], generated_at: str, + presentation: str = HTML_DEFAULT_PRESENTATION, ) -> str: section_items = "\n".join( f'<li><a href="#{html_escape(item["id"])}">{html_escape(item["title"])}</a></li>' for item in sections[:12] @@ -4950,6 +6882,20 @@ def build_html_fragment( meta_html = "\n".join( f"<div><dt>{html_escape(label)}</dt><dd>{html_escape(value)}</dd></div>" for label, value in metadata ) + if presentation != "default": + return f""" +<section class="kos-fragment kos-fragment-{html_escape(presentation)}" data-kos-fragment="true" data-kind="{html_escape(kind)}" data-source-sha256="{html_escape(source_sha)}"> + {body_html} + <footer class="kos-evidence-footer"> + <details open> + <summary>Evidence metadata</summary> + <dl class="kos-meta">{meta_html}</dl> + <p class="kos-notice">{HTML_SOURCE_TRUTH_NOTICE}</p> + <nav class="kos-nav" aria-label="Report sections">{section_nav}</nav> + </details> + </footer> +</section> +""".strip() return f""" <article class="kos-report-fragment" data-kos-fragment="true" data-kind="{html_escape(kind)}" data-source-sha256="{html_escape(source_sha)}"> <div class="kos-eyebrow">{html_escape(kind)}</div> @@ -4974,7 +6920,30 @@ def build_html_document( run_id: str, generated_at: str, theme: str, + presentation: str = HTML_DEFAULT_PRESENTATION, ) -> str: + if presentation == "minimal": + return build_minimal_html_document( + title=title, + kind=kind, + body=body, + source_rel=source_rel, + source_sha=source_sha, + run_id=run_id, + generated_at=generated_at, + theme=theme, + ) + if presentation == "bare": + return build_bare_html_document( + title=title, + kind=kind, + body=body, + source_rel=source_rel, + source_sha=source_sha, + run_id=run_id, + generated_at=generated_at, + theme=theme, + ) nav_hint = "Composable sidecar report" subtitle = ( "A self-contained KnowledgeOS HTML sidecar generated from canonical Markdown, YAML, or NDJSON evidence. " @@ -4988,117 +6957,701 @@ def build_html_document( ("Generated", generated_at), ("Theme", theme), ] - meta_html = "\n".join( - f"<div><dt>{html_escape(label)}</dt><dd>{html_escape(value)}</dd></div>" for label, value in meta_rows + meta_html = "\n".join( + f"<div><dt>{html_escape(label)}</dt><dd>{html_escape(value)}</dd></div>" for label, value in meta_rows + ) + return f"""<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="knowledgeos-source-sha256" content="{html_escape(source_sha)}"> + <meta name="knowledgeos-source-path" content="{html_escape(source_rel)}"> + <title>{html_escape(title)} + + + +
+
+
{html_escape(nav_hint)}
+

{html_escape(title)}

+

{html_escape(subtitle)}

+
+
+
{body}
+ +
+
{HTML_SOURCE_TRUTH_NOTICE}
+
+ + +""" + + +def build_minimal_html_document( + *, + title: str, + kind: str, + body: str, + source_rel: str, + source_sha: str, + run_id: str, + generated_at: str, + theme: str, +) -> str: + meta_rows = [ + ("Kind", kind), + ("Run ID", run_id or "not run-bound"), + ("Source", source_rel), + ("Source SHA-256", source_sha), + ("Generated", generated_at), + ("Theme", theme), + ("Presentation", "minimal"), + ] + meta_html = "\n".join( + f"
{html_escape(label)}
{html_escape(value)}
" for label, value in meta_rows + ) + css = """ +* { box-sizing: border-box; } +body { margin: 0; color: #172033; background: #fbfcfe; font-family: Georgia, "Times New Roman", serif; line-height: 1.68; } +main { max-width: 880px; margin: 0 auto; padding: 42px 22px; } +h1, h2, h3 { line-height: 1.18; color: #101827; } +a { color: #155eef; } +pre { overflow: auto; padding: 14px; border: 1px solid #d8dee9; border-radius: 10px; background: #f3f6fb; } +blockquote { margin: 18px 0; padding: 12px 16px; border-left: 4px solid #94a3b8; background: #f8fafc; } +.kos-evidence-footer, .kos-page-evidence { margin-top: 34px; padding-top: 18px; border-top: 1px solid #d8dee9; font-size: 0.92rem; } +.kos-meta { display: grid; gap: 8px; margin: 0; } +.kos-meta div { overflow-wrap: anywhere; } +.kos-meta dt { color: #64748b; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.08em; } +.kos-meta dd { margin: 2px 0 0; font-weight: 700; } +.kos-notice { color: #92400e; font-weight: 700; } +""".strip() + return f""" + + + + + + + + {html_escape(title)} + + + +
+ {body} +
+

Evidence Metadata

+
{meta_html}
+

{HTML_SOURCE_TRUTH_NOTICE}

+
+
+ + +""" + + +def build_bare_html_document( + *, + title: str, + kind: str, + body: str, + source_rel: str, + source_sha: str, + run_id: str, + generated_at: str, + theme: str, +) -> str: + metadata = ( + f"kind={html_escape(kind)} | run={html_escape(run_id or 'not run-bound')} | " + f"source={html_escape(source_rel)} | sha256={html_escape(source_sha)} | " + f"generated={html_escape(generated_at)} | theme={html_escape(theme)} | presentation=bare" + ) + return f""" + + + + + + + + {html_escape(title)} + + + + {body} +
+

{metadata}

+

{HTML_SOURCE_TRUTH_NOTICE}

+
+ + +""" + + +def render_html_sidecar( + project_root: Path, + *, + kind: str, + source_path: Path, + output_path: Path, + run_id: str = "", + theme: str = HTML_DEFAULT_THEME, + presentation: str = HTML_DEFAULT_PRESENTATION, +) -> dict[str, Any]: + if kind not in HTML_REPORT_KINDS: + raise ValueError(f"unsupported render kind: {kind}") + if not source_path.exists(): + raise FileNotFoundError(source_path) + if source_path.suffix.lower() not in {".md", ".markdown", ".txt"}: + raise ValueError("render-html v1 expects a Markdown or text source") + markdown = read_text(source_path) + source_sha = content_fingerprint(markdown) + anchor_prefix = safe_slug(f"{kind}-{source_sha[:12]}") + body_html, title, sections = markdown_to_html_fragment(markdown, anchor_prefix=anchor_prefix) + if kind == "receipt" and run_id: + title = f"KnowledgeOS Receipt {run_id}" + elif kind == "handoff" and run_id: + title = f"KnowledgeOS Handoff {run_id}" + generated_at = datetime.now(timezone.utc).isoformat() + source_rel = project_relative(project_root, source_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + fragment_path = output_path.with_suffix(".fragment.html") + manifest_path = output_path.with_suffix(".manifest.json") + fragment_html = build_html_fragment( + kind=kind, + title=title, + source_rel=source_rel, + source_sha=source_sha, + run_id=run_id, + body_html=body_html, + sections=sections, + generated_at=generated_at, + presentation=presentation, + ) + write_text(fragment_path, fragment_html) + if presentation == "fragment": + output_rel = "" + else: + full_html = build_html_document( + title=title, + kind=kind, + body=fragment_html, + source_rel=source_rel, + source_sha=source_sha, + run_id=run_id, + generated_at=generated_at, + theme=theme, + presentation=presentation, + ) + write_text(output_path, full_html) + output_rel = project_relative(project_root, output_path) + manifest = { + "schema_version": "knowledgeos.html-report.v1", + "kind": kind, + "title": title, + "theme": theme, + "presentation": presentation, + "run_id": run_id, + "source": source_rel, + "source_sha256": source_sha, + "output": output_rel, + "fragment": project_relative(project_root, fragment_path), + "sections": sections, + "generated_at": generated_at, + "html_source_of_truth": False, + "html_required_metadata": True, + "notice": HTML_SOURCE_TRUTH_NOTICE, + } + write_text(manifest_path, json.dumps(manifest, indent=2, ensure_ascii=False) + "\n") + return { + "status": "rendered", + "kind": kind, + "title": title, + "output": str(output_path) if presentation != "fragment" else "", + "fragment": str(fragment_path), + "manifest": str(manifest_path), + "source": str(source_path), + "source_sha256": source_sha, + "run_id": run_id, + "presentation": presentation, + "html_source_of_truth": False, + } + + +def decision_events_to_markdown(run_id: str, events: list[dict[str, Any]]) -> str: + lines = [ + f"# KnowledgeOS Decision Map {run_id}", + "", + "This sidecar summarizes public Decision Graph events. It is not hidden chain-of-thought.", + "", + "## Main Path", + "", + ] + main_statuses = {"planned", "active", "selected", "executed"} + main_events = [event for event in events if str(event.get("status", "")) in main_statuses] + branch_events = [event for event in events if event not in main_events] + if not main_events: + lines.append("- No selected or executed decision path recorded.") + for event in main_events: + lines.append( + f"- {event.get('decision_id', '')}: {event.get('title', '')} " + f"({event.get('kind', '')}, {event.get('status', '')})" + ) + if event.get("summary"): + lines.append(f"- Summary: {event.get('summary')}") + if event.get("reason"): + lines.append(f"- Reason: {event.get('reason')}") + if event.get("chosen"): + lines.append(f"- Chosen: {event.get('chosen')}") + lines.extend(["", "## Abandoned Deferred And Recovery Branches", ""]) + if not branch_events: + lines.append("- No abandoned, deferred, superseded, blocked, or rollback branches recorded.") + for event in branch_events: + lines.append( + f"- {event.get('decision_id', '')}: {event.get('title', '')} " + f"({event.get('kind', '')}, {event.get('status', '')})" + ) + if event.get("parent_id"): + lines.append(f"- Parent: {event.get('parent_id')}") + if event.get("reason"): + lines.append(f"- Reason: {event.get('reason')}") + if event.get("evidence"): + lines.append(f"- Evidence: {event.get('evidence')}") + lines.extend(["", "## Full Decision Ledger", ""]) + for event in events: + options = ", ".join(str(item) for item in event.get("options", []) if str(item).strip()) + lines.append(f"- ID: {event.get('decision_id', '')}") + lines.append(f"- Parent: {event.get('parent_id', '') or 'root'}") + lines.append(f"- Title: {event.get('title', '')}") + lines.append(f"- Kind: {event.get('kind', '')}") + lines.append(f"- Status: {event.get('status', '')}") + if options: + lines.append(f"- Options: {options}") + if event.get("linked_capability_event_id"): + lines.append(f"- Linked Capability: {event.get('linked_capability_event_id')}") + if event.get("linked_effect_assertion_id"): + lines.append(f"- Linked Effect: {event.get('linked_effect_assertion_id')}") + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + +def render_decision_map_sidecar( + project_root: Path, + *, + run_id: str, + output_path: Path, + theme: str = HTML_DEFAULT_THEME, + presentation: str = HTML_DEFAULT_PRESENTATION, +) -> dict[str, Any]: + run_dir = resolve_run_dir(project_root, run_id) + source_path = decision_events_path(run_dir) + if not source_path.exists(): + raise FileNotFoundError(source_path) + events = [event for event in load_decision_events(run_dir) if "_invalid_json" not in event] + source_sha = sha256_file(source_path) + markdown = decision_events_to_markdown(run_id, events) + anchor_prefix = safe_slug(f"decision-map-{source_sha[:12]}") + body_html, title, sections = markdown_to_html_fragment(markdown, anchor_prefix=anchor_prefix) + generated_at = datetime.now(timezone.utc).isoformat() + source_rel = project_relative(project_root, source_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + fragment_path = output_path.with_suffix(".fragment.html") + manifest_path = output_path.with_suffix(".manifest.json") + fragment_html = build_html_fragment( + kind="decision-map", + title=title, + source_rel=source_rel, + source_sha=source_sha, + run_id=run_id, + body_html=body_html, + sections=sections, + generated_at=generated_at, + presentation=presentation, + ) + write_text(fragment_path, fragment_html) + if presentation == "fragment": + output_rel = "" + else: + full_html = build_html_document( + title=title, + kind="decision-map", + body=fragment_html, + source_rel=source_rel, + source_sha=source_sha, + run_id=run_id, + generated_at=generated_at, + theme=theme, + presentation=presentation, + ) + write_text(output_path, full_html) + output_rel = project_relative(project_root, output_path) + manifest = { + "schema_version": "knowledgeos.html-report.v1", + "kind": "decision-map", + "title": title, + "theme": theme, + "presentation": presentation, + "run_id": run_id, + "source": source_rel, + "source_sha256": source_sha, + "output": output_rel, + "fragment": project_relative(project_root, fragment_path), + "sections": sections, + "decision_count": len(events), + "generated_at": generated_at, + "html_source_of_truth": False, + "html_required_metadata": True, + "notice": HTML_SOURCE_TRUTH_NOTICE, + } + write_text(manifest_path, json.dumps(manifest, indent=2, ensure_ascii=False) + "\n") + return { + "status": "rendered", + "kind": "decision-map", + "title": title, + "output": str(output_path) if presentation != "fragment" else "", + "fragment": str(fragment_path), + "manifest": str(manifest_path), + "source": str(source_path), + "source_sha256": source_sha, + "run_id": run_id, + "decision_count": len(events), + "presentation": presentation, + "html_source_of_truth": False, + } + + +def mermaid_label(value: str, limit: int = 58) -> str: + cleaned = " ".join(str(value).split()) + if len(cleaned) > limit: + cleaned = cleaned[: max(0, limit - 3)].rstrip() + "..." + return cleaned.replace('"', "'").replace("[", "(").replace("]", ")") + + +def latest_phase_records_by_phase(run_dir: Path) -> dict[str, dict[str, Any]]: + latest: dict[str, dict[str, Any]] = {} + for record in load_phase_records(run_dir): + if "_invalid_json" in record: + continue + phase = str(record.get("phase", "")) + if phase: + latest[phase] = record + return latest + + +def count_valid_records(records: list[dict[str, Any]], status: str = "") -> int: + count = 0 + for record in records: + if "_invalid_json" in record: + continue + if status and str(record.get("status", "")) != status: + continue + count += 1 + return count + + +def mission_flow_stage_summary(project_root: Path, task_id: str, run_id: str, sync_status: str = "") -> dict[str, Any]: + task = find_task(project_root, task_id) + run_dir = ensure_run_belongs_to_task(project_root, task_id, run_id) + run_meta = parse_scalar_values(run_dir / "run.yaml", {"route_status", "status", "task_title", "task_type"}) + phase_latest = latest_phase_records_by_phase(run_dir) + phase_done = [phase for phase in EXPECTED_PHASE_KEYS if phase_latest.get(phase, {}).get("status") in {"completed", "skipped"}] + capability_count = count_valid_records(load_capability_events(run_dir)) + effect_count = count_valid_records(load_effect_assertions(run_dir), status="passed") + decision_count = count_valid_records(load_decision_events(run_dir)) + has_plan = (run_dir / "plan.md").exists() and (run_dir / "context-pack.md").exists() + eval_ok = eval_has_passed(run_dir / "eval.md") + postflight_text = read_text(run_dir / "postflight.md") if (run_dir / "postflight.md").exists() else "" + synced = sync_status == "SYNC_OK" or "[SYNC_OK]" in postflight_text + task_title = task.get("title") or run_meta.get("task_title") or task_id + proof_label = f"{effect_count} real check" + ("" if effect_count == 1 else "s") + tool_label = f"{capability_count} tool note" + ("" if capability_count == 1 else "s") + decision_label = f"{decision_count} decision" + ("" if decision_count == 1 else "s") + checkpoint_label = f"{len(phase_done)}/6 checkpoints" + return { + "task": task, + "run_dir": run_dir, + "title": task_title, + "complexity": task.get("complexity", "medium"), + "stages": [ + { + "id": "A", + "name": "Goal", + "kind": "intent", + "label": f"Goal: {task_title}", + "detail": "What the user wanted us to finish.", + "status": "set", + }, + { + "id": "B", + "name": "Health Check", + "kind": "guard", + "label": "Health check: OK", + "detail": "The project control plane was checked before work.", + "status": "ok", + }, + { + "id": "C", + "name": "Task & Plan", + "kind": "intent", + "label": "Task and plan: ready" if has_plan else "Task and plan: missing", + "detail": "The run has a context pack and a short working plan." if has_plan else "The run is missing plan/context files.", + "status": "ok" if has_plan else "attention", + }, + { + "id": "D", + "name": "Safe Writes", + "kind": "guard", + "label": "Safe writes: routed" if run_meta.get("route_status") == "routed" else "Safe writes: review needed", + "detail": "Planned file changes stayed inside the task route.", + "status": "ok" if run_meta.get("route_status") == "routed" else "attention", + }, + { + "id": "E", + "name": "Work Done", + "kind": "work", + "label": "Work done: recorded" if "execute" in phase_done else "Work done: not recorded", + "detail": "Implementation or analysis steps were recorded as public checkpoints.", + "status": "ok" if "execute" in phase_done else "attention", + }, + { + "id": "F", + "name": "Tools Used", + "kind": "work", + "label": f"Tools used: {tool_label}", + "detail": "Important shell, script, subagent, MCP, or skill use was made visible.", + "status": "ok" if capability_count else "quiet", + }, + { + "id": "G", + "name": "Proof", + "kind": "proof", + "label": f"Proof: {proof_label}", + "detail": "Real artifacts were checked after the work landed.", + "status": "ok" if effect_count else "quiet", + }, + { + "id": "H", + "name": "Decisions", + "kind": "proof", + "label": f"Decisions: {decision_label}", + "detail": "Route changes or important choices were summarized for humans.", + "status": "ok" if decision_count else "quiet", + }, + { + "id": "I", + "name": "Finish", + "kind": "done", + "label": "Finish: synced" if synced else ("Finish: checked" if eval_ok else "Finish: pending"), + "detail": f"Review status: {'passed' if eval_ok else 'pending'}; {checkpoint_label}.", + "status": "ok" if synced or eval_ok else "attention", + }, + ], + "counts": { + "capability_events": capability_count, + "effect_assertions": effect_count, + "decision_events": decision_count, + "phases_recorded": len(phase_done), + }, + "synced": synced, + "eval_passed": eval_ok, + } + + +def build_mission_flow_mermaid(summary: dict[str, Any]) -> str: + stages = summary["stages"] + lines = ["flowchart LR"] + for stage in stages: + lines.append(f' {stage["id"]}["{mermaid_label(stage["label"])}"]') + for left, right in zip(stages, stages[1:]): + lines.append(f' {left["id"]} --> {right["id"]}') + lines.extend( + [ + "", + " classDef intent fill:#E8F3FF,stroke:#2B6CB0,color:#102A43;", + " classDef guard fill:#FFF4D6,stroke:#B7791F,color:#3D2B00;", + " classDef work fill:#E9FBEF,stroke:#2F855A,color:#123524;", + " classDef proof fill:#F3E8FF,stroke:#6B46C1,color:#2D174D;", + " classDef done fill:#DCFCE7,stroke:#15803D,color:#052E16;", + ] + ) + by_kind: dict[str, list[str]] = {"intent": [], "guard": [], "work": [], "proof": [], "done": []} + for stage in stages: + by_kind.setdefault(stage["kind"], []).append(stage["id"]) + for kind, ids in by_kind.items(): + if ids: + lines.append(f" class {','.join(ids)} {kind};") + return "\n".join(lines) + + +def build_mission_flow_markdown(project_root: Path, task_id: str, run_id: str, sync_status: str = "") -> dict[str, Any]: + summary = mission_flow_stage_summary(project_root, task_id, run_id, sync_status=sync_status) + mermaid = build_mission_flow_mermaid(summary) + lines = [ + f"# Mission Flow {run_id}", + "", + "FLOW_OK", + "", + "A human-readable map of how this task moved from request to finish.", + "", + "## Flow", + "", + "```mermaid", + mermaid, + "```", + "", + "## Plain Summary", + "", + ] + for stage in summary["stages"]: + lines.append(f"- {stage['name']}: {stage['label']}. {stage['detail']}") + lines.extend( + [ + "", + "## Counts", + "", + f"- Tools made visible: {summary['counts']['capability_events']}", + f"- Artifact checks: {summary['counts']['effect_assertions']}", + f"- Decision notes: {summary['counts']['decision_events']}", + f"- Checkpoints recorded: {summary['counts']['phases_recorded']}/6", + "", + "HTML is presentation, not source of truth.", + ] ) - return f""" - - - - - - - {html_escape(title)} - - - -
-
-
{html_escape(nav_hint)}
-

{html_escape(title)}

-

{html_escape(subtitle)}

-
-
-
{body}
- -
-
{HTML_SOURCE_TRUTH_NOTICE}
-
- - -""" + return { + "status": "generated", + "flow_marker": "FLOW_OK", + "marker": f"FLOW_OK run={run_id} stages={len(summary['stages'])} synced={'yes' if summary['synced'] else 'no'}", + "task_id": task_id, + "run_id": run_id, + "complexity": summary["complexity"], + "mermaid": mermaid, + "markdown": "\n".join(lines).rstrip() + "\n", + "counts": summary["counts"], + "stages": summary["stages"], + } -def render_html_sidecar( +def write_mission_flow_markdown(project_root: Path, task_id: str, run_id: str, sync_status: str = "") -> dict[str, Any]: + result = build_mission_flow_markdown(project_root, task_id, run_id, sync_status=sync_status) + run_dir = ensure_run_belongs_to_task(project_root, task_id, run_id) + source_path = run_dir / "mission-flow.md" + write_text(source_path, result["markdown"]) + result["source"] = str(source_path) + return result + + +def mission_flow_cards_html(stages: list[dict[str, Any]]) -> str: + cards = [] + for stage in stages: + cards.append( + '
' + "

{name}

" + "

{label}

" + "

{detail}

" + "
".format( + kind=html_escape(stage["kind"]), + name=html_escape(stage["name"]), + label=html_escape(stage["label"]), + detail=html_escape(stage["detail"]), + ) + ) + return '
' + "\n".join(cards) + "
" + + +def render_mission_flow_sidecar( project_root: Path, *, - kind: str, - source_path: Path, + task_id: str, + run_id: str, output_path: Path, - run_id: str = "", theme: str = HTML_DEFAULT_THEME, + presentation: str = HTML_DEFAULT_PRESENTATION, ) -> dict[str, Any]: - if kind not in HTML_REPORT_KINDS: - raise ValueError(f"unsupported render kind: {kind}") - if not source_path.exists(): - raise FileNotFoundError(source_path) - if source_path.suffix.lower() not in {".md", ".markdown", ".txt"}: - raise ValueError("render-html v1 expects a Markdown or text source") - markdown = read_text(source_path) - source_sha = content_fingerprint(markdown) - anchor_prefix = safe_slug(f"{kind}-{source_sha[:12]}") - body_html, title, sections = markdown_to_html_fragment(markdown, anchor_prefix=anchor_prefix) - if kind == "receipt" and run_id: - title = f"KnowledgeOS Receipt {run_id}" - elif kind == "handoff" and run_id: - title = f"KnowledgeOS Handoff {run_id}" + flow = write_mission_flow_markdown(project_root, task_id, run_id) + source_path = Path(flow["source"]) + source_sha = sha256_file(source_path) generated_at = datetime.now(timezone.utc).isoformat() source_rel = project_relative(project_root, source_path) output_path.parent.mkdir(parents=True, exist_ok=True) fragment_path = output_path.with_suffix(".fragment.html") manifest_path = output_path.with_suffix(".manifest.json") + body_html = "\n".join( + [ + f"

Mission Flow {html_escape(run_id)}

", + "

A friendly map of how the task moved from request to finish.

", + mission_flow_cards_html(flow["stages"]), + "
Mermaid source", + f"
{html_escape(flow['mermaid'])}
", + "
", + ] + ) + sections = [{"id": "mission-flow", "title": "Mission Flow", "level": 1}] fragment_html = build_html_fragment( - kind=kind, - title=title, + kind="mission-flow", + title=f"Mission Flow {run_id}", source_rel=source_rel, source_sha=source_sha, run_id=run_id, body_html=body_html, sections=sections, generated_at=generated_at, + presentation=presentation, ) write_text(fragment_path, fragment_html) - full_html = build_html_document( - title=title, - kind=kind, - body=fragment_html, - source_rel=source_rel, - source_sha=source_sha, - run_id=run_id, - generated_at=generated_at, - theme=theme, - ) - write_text(output_path, full_html) + if presentation == "fragment": + output_rel = "" + else: + full_html = build_html_document( + title=f"Mission Flow {run_id}", + kind="mission-flow", + body=fragment_html, + source_rel=source_rel, + source_sha=source_sha, + run_id=run_id, + generated_at=generated_at, + theme=theme, + presentation=presentation, + ) + write_text(output_path, full_html) + output_rel = project_relative(project_root, output_path) manifest = { "schema_version": "knowledgeos.html-report.v1", - "kind": kind, - "title": title, + "kind": "mission-flow", + "title": f"Mission Flow {run_id}", "theme": theme, + "presentation": presentation, "run_id": run_id, + "task_id": task_id, "source": source_rel, "source_sha256": source_sha, - "output": project_relative(project_root, output_path), + "output": output_rel, "fragment": project_relative(project_root, fragment_path), "sections": sections, "generated_at": generated_at, + "flow_marker": "FLOW_OK", "html_source_of_truth": False, + "html_required_metadata": True, "notice": HTML_SOURCE_TRUTH_NOTICE, } write_text(manifest_path, json.dumps(manifest, indent=2, ensure_ascii=False) + "\n") return { "status": "rendered", - "kind": kind, - "title": title, - "output": str(output_path), + "kind": "mission-flow", + "title": f"Mission Flow {run_id}", + "output": str(output_path) if presentation != "fragment" else "", "fragment": str(fragment_path), "manifest": str(manifest_path), "source": str(source_path), "source_sha256": source_sha, "run_id": run_id, + "task_id": task_id, + "flow_marker": "FLOW_OK", + "presentation": presentation, "html_source_of_truth": False, } @@ -5119,7 +7672,10 @@ def compose_html_reports( compose_manifest_path: Path, output_path: Path, theme: str = HTML_DEFAULT_THEME, + presentation: str = HTML_DEFAULT_PRESENTATION, ) -> dict[str, Any]: + if presentation == "fragment": + raise ValueError("render-html --compose does not support fragment presentation") if not compose_manifest_path.exists(): raise FileNotFoundError(compose_manifest_path) compose_manifest = json.loads(read_text(compose_manifest_path)) @@ -5166,6 +7722,7 @@ def compose_html_reports( run_id=str(compose_manifest.get("run_id") or ""), generated_at=generated_at, theme=str(compose_manifest.get("theme") or theme), + presentation=presentation, ) write_text(output_path, full_html) manifest_path = output_path.with_suffix(".manifest.json") @@ -5173,6 +7730,7 @@ def compose_html_reports( "schema_version": "knowledgeos.html-composition.v1", "title": title, "theme": str(compose_manifest.get("theme") or theme), + "presentation": presentation, "source": project_relative(project_root, compose_manifest_path), "source_sha256": compose_source_sha, "output": project_relative(project_root, output_path), @@ -5180,6 +7738,7 @@ def compose_html_reports( "reports": child_manifests, "generated_at": generated_at, "html_source_of_truth": False, + "html_required_metadata": True, "notice": HTML_SOURCE_TRUTH_NOTICE, } write_text(manifest_path, json.dumps(manifest, indent=2, ensure_ascii=False) + "\n") @@ -5190,6 +7749,7 @@ def compose_html_reports( "report_count": len(child_manifests), "source": str(compose_manifest_path), "source_sha256": compose_source_sha, + "presentation": presentation, "html_source_of_truth": False, } @@ -5352,6 +7912,7 @@ def build_task_route(project_root: Path, task_id: str | None, task_type: str | N "route_order": profile.get("route_order", []), "eval_profile": profile.get("eval_profile", ""), "human_gate": profile.get("human_gate", ""), + "allow_external_controlled": profile.get("allow_external_controlled", ""), "allowed_outputs": profile.get("allowed_outputs", []), "notes": profile.get("notes", []), } @@ -5615,6 +8176,8 @@ def cmd_capability_event(args: argparse.Namespace) -> int: purpose=args.purpose, status=args.status, evidence=args.evidence, + invocation_id=args.invocation_id, + recovers_event_id=args.recovers_event_id, ) except (FileNotFoundError, KeyError, ValueError) as exc: print(str(exc), file=sys.stderr) @@ -5650,6 +8213,55 @@ def cmd_trace_step(args: argparse.Namespace) -> int: return 0 +def cmd_decision_event(args: argparse.Namespace) -> int: + project_root = Path(args.project_root).expanduser().resolve() + try: + result = record_decision_event( + project_root, + args.task_id, + args.run_id, + kind=args.kind, + status=args.status, + title=args.title, + summary=args.summary, + reason=args.reason, + evidence=args.evidence, + parent_id=args.parent_id or "", + options=args.option or [], + chosen=args.chosen or "", + linked_step=args.linked_step or "", + linked_capability_event_id=args.linked_capability_event_id or "", + linked_effect_assertion_id=args.linked_effect_assertion_id or "", + ) + except (FileNotFoundError, KeyError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + if args.json: + emit(result, True) + else: + print(result["marker"]) + print(f"ledger: {result['ledger']}") + return 0 + + +def cmd_decision_query(args: argparse.Namespace) -> int: + project_root = Path(args.project_root).expanduser().resolve() + try: + result = query_decision_events( + project_root, + run_id=args.run_id, + task_id=args.task_id or "", + kind=args.kind or "", + status=args.status or "", + parent_id=args.parent_id or "", + ) + except (FileNotFoundError, KeyError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + emit(result, args.json) + return 0 + + def cmd_artifact_assert(args: argparse.Namespace) -> int: project_root = Path(args.project_root).expanduser().resolve() try: @@ -5738,6 +8350,25 @@ def cmd_verify_effects(args: argparse.Namespace) -> int: return 2 if result.get("status") == "failed" else 0 +def cmd_verify_decisions(args: argparse.Namespace) -> int: + project_root = Path(args.project_root).expanduser().resolve() + try: + result = verify_decisions(project_root, args.task_id, args.run_id) + except (FileNotFoundError, KeyError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + if args.json: + emit(result, True) + else: + print(result["marker"]) + print(f"ledger: {result['ledger']}") + if result.get("warnings"): + print(f"warnings: {len(result['warnings'])}") + if result.get("errors"): + print(f"errors: {len(result['errors'])}") + return 2 if result.get("status") == "failed" else 0 + + def cmd_complete_task(args: argparse.Namespace) -> int: project_root = Path(args.project_root).expanduser().resolve() try: @@ -5802,13 +8433,65 @@ def cmd_dispatch_task(args: argparse.Namespace) -> int: except (FileNotFoundError, KeyError, ValueError) as exc: print(str(exc), file=sys.stderr) return 1 - emit(result, args.json) + if args.json: + emit(result, True) + else: + if result.get("dispatch_marker"): + summary = result.get("dispatch_summary", {}) + print(result.get("marker", "")) + print(f"planned_agents: {summary.get('planned_agents', 0)}") + print(f"planned_capabilities: {summary.get('planned_capabilities', 0)}") + if summary.get("planned_agent_ids"): + print("planned_agent_ids: " + ", ".join(summary.get("planned_agent_ids", []))) + emit(result, False) return 0 if result.get("status") == "dispatch_ready" or args.allow_unrouted else 2 +def cmd_dispatch_report(args: argparse.Namespace) -> int: + project_root = Path(args.project_root).expanduser().resolve() + try: + result = build_dispatch_report(project_root, args.task_id, args.run_id) + except (FileNotFoundError, KeyError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + if args.json: + emit(result, True) + else: + print(result["marker"]) + print("Full Capability Dispatch Report") + print(f"agents: {result['agent_count']}") + print(f"skipped_agents: {result.get('skipped_agent_count', 0)}") + print(f"runtime_gaps: {result.get('runtime_gap_count', 0)}") + print(f"capabilities: {result['capability_count']}") + print(f"skipped: {result.get('skipped_count', 0)}") + print("by_kind:") + for kind, count in sorted(result.get("counts_by_kind", {}).items()): + skipped = result.get("skipped_by_kind", {}).get(kind, 0) + print(f"- {kind}: used={count}, skipped={skipped}") + if result.get("events"): + print("used_capabilities:") + for event in result["events"]: + print(f"- {event['kind']}: {event['id']} ({event['status']}) - {event['purpose']}") + if result.get("skipped"): + print("skipped_or_not_needed:") + for event in result["skipped"]: + print(f"- {event['kind']}: {event['id']} ({event['status']}) - {event['purpose']}") + if result.get("runtime_gaps"): + print("runtime_gap_details:") + for event in result["runtime_gaps"]: + print(f"- {event['kind']}: {event['id']} ({event['status']}) - {event['evidence']}") + if result.get("gaps"): + print("gaps:") + for gap in result["gaps"]: + print(f"- {gap}") + print(f"report: {result['report']}") + return 0 + + def cmd_render_html(args: argparse.Namespace) -> int: project_root = Path(args.project_root).expanduser().resolve() try: + presentation = resolve_html_presentation(project_root, args.presentation) if args.compose: if not args.output: raise ValueError("render-html --compose requires --output") @@ -5817,6 +8500,7 @@ def cmd_render_html(args: argparse.Namespace) -> int: compose_manifest_path=resolve_project_artifact(project_root, args.compose), output_path=resolve_project_artifact(project_root, args.output), theme=args.theme, + presentation=presentation, ) emit(result, args.json) return 0 @@ -5836,6 +8520,35 @@ def cmd_render_html(args: argparse.Namespace) -> int: output_path=output_path, run_id=args.run_id, theme=args.theme, + presentation=presentation, + ) + elif args.kind == "decision-map": + if not args.run_id: + raise ValueError("render-html --kind decision-map requires --run-id") + run_dir = resolve_run_dir(project_root, args.run_id) + output_path = resolve_project_artifact(project_root, args.output) if args.output else run_dir / "decision-map.html" + result = render_decision_map_sidecar( + project_root, + run_id=args.run_id, + output_path=output_path, + theme=args.theme, + presentation=presentation, + ) + elif args.kind == "mission-flow": + if not args.run_id: + raise ValueError("render-html --kind mission-flow requires --run-id") + run_dir = resolve_run_dir(project_root, args.run_id) + task_id = args.task_id or parse_scalar_values(run_dir / "run.yaml", {"task_id"}).get("task_id", "") + if not task_id: + raise ValueError("could not resolve task id for mission-flow") + output_path = resolve_project_artifact(project_root, args.output) if args.output else run_dir / "mission-flow.html" + result = render_mission_flow_sidecar( + project_root, + task_id=task_id, + run_id=args.run_id, + output_path=output_path, + theme=args.theme, + presentation=presentation, ) elif args.kind == "rich-report": if not args.input: @@ -5848,6 +8561,7 @@ def cmd_render_html(args: argparse.Namespace) -> int: source_path=source_path, output_path=output_path, theme=args.theme, + presentation=presentation, ) else: raise ValueError(f"unsupported render kind: {args.kind}") @@ -5858,6 +8572,120 @@ def cmd_render_html(args: argparse.Namespace) -> int: return 0 +def cmd_flow_summary(args: argparse.Namespace) -> int: + project_root = Path(args.project_root).expanduser().resolve() + try: + run_dir = resolve_run_dir(project_root, args.run_id) + task_id = args.task_id or parse_scalar_values(run_dir / "run.yaml", {"task_id"}).get("task_id", "") + if not task_id: + raise ValueError("could not resolve task id for flow-summary") + result = write_mission_flow_markdown(project_root, task_id, args.run_id) + append_command_event( + run_dir, + "flow-summary", + task_id, + args.run_id, + status="generated", + source=project_relative(project_root, Path(result["source"])), + ) + except (FileNotFoundError, KeyError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + if args.json: + emit(result, True) + else: + print(result["marker"]) + if args.format == "markdown": + print(result["markdown"]) + else: + print("```mermaid") + print(result["mermaid"]) + print("```") + print(f"source: {result['source']}") + return 0 + + +def cmd_thread_plan(args: argparse.Namespace) -> int: + project_root = Path(args.project_root).expanduser().resolve() + try: + if args.thread_action == "start": + result = start_thread_plan(project_root, title=args.title, spec_id=args.spec_id or "") + elif args.thread_action == "current": + current = load_current_thread(project_root) + result = { + "status": "ok", + "thread_plan_marker": THREAD_PLAN_MARKER, + "marker": f"{THREAD_PLAN_MARKER} action=current thread={current['thread_id']}", + "current": current, + } + elif args.thread_action == "append": + result = append_thread_plan_event( + project_root, + thread_id=args.thread_id, + kind=args.kind, + text=args.text, + linked_spec_id=args.spec_id or "", + linked_task_id=args.task_id or "", + linked_run_id=args.run_id or "", + parent_event_id=args.parent_event_id or "", + ) + elif args.thread_action == "link-run": + ensure_run_belongs_to_task(project_root, args.task_id, args.run_id) + text = args.text or f"已关联执行记录:任务 {args.task_id} / 运行 {args.run_id}。" + result = append_thread_plan_event( + project_root, + thread_id=args.thread_id, + kind="progress", + text=text, + linked_task_id=args.task_id, + linked_run_id=args.run_id, + event_type="thread-plan link-run", + ) + result["status"] = "linked" + result["marker"] = f"{THREAD_PLAN_MARKER} action=link-run thread={args.thread_id} run={args.run_id}" + elif args.thread_action == "render": + if args.format == "markdown": + result = render_thread_plan_markdown(project_root, args.thread_id) + elif args.format == "html": + result = render_thread_plan_html(project_root, args.thread_id) + elif args.format == "mermaid": + mermaid = render_thread_plan_mermaid(project_root, args.thread_id) + result = { + "status": "rendered", + "thread_plan_marker": THREAD_PLAN_MARKER, + "marker": f"{THREAD_PLAN_MARKER} action=render thread={args.thread_id} format=mermaid", + "thread_id": args.thread_id, + "format": "mermaid", + "mermaid": mermaid, + } + else: + raise ValueError(f"unsupported thread-plan render format: {args.format}") + append_thread_command_event(project_root, args.thread_id, "thread-plan render", format=args.format) + else: + raise ValueError(f"unsupported thread-plan action: {args.thread_action}") + except (FileNotFoundError, KeyError, ValueError, json.JSONDecodeError) as exc: + print(str(exc), file=sys.stderr) + return 1 + if args.json: + emit(result, True) + else: + print(result.get("marker", f"{THREAD_PLAN_MARKER} action={args.thread_action}")) + if args.thread_action == "current": + current = result.get("current", {}) + print(f"thread_id: {current.get('thread_id', '')}") + print(f"title: {current.get('title', '')}") + print(f"spec_id: {current.get('spec_id', '')}") + elif args.thread_action == "render" and args.format == "mermaid": + print("```mermaid") + print(result["mermaid"]) + print("```") + elif result.get("output"): + print(f"output: {result['output']}") + elif result.get("ledger"): + print(f"ledger: {result['ledger']}") + return 0 + + def cmd_receipt(args: argparse.Namespace) -> int: project_root = Path(args.project_root).expanduser().resolve() receipt_dir = project_root / ".agent-os" / "receipts" @@ -5935,9 +8763,68 @@ def cmd_runtime_adapters(args: argparse.Namespace) -> int: print(f"real_cli_execution: {result['policy']['real_cli_execution']}") for adapter in result["adapters"]: print(f"- {adapter['id']}: {adapter['status']} ({adapter['execution_mode']})") + if result.get("runtime_subagents"): + print("runtime_subagents:") + for item in result["runtime_subagents"]: + print(f"- {item['id']}: {item['runtime_tool']}({item['runtime_agent_type']})") return 0 if result.get("status") == "ready" else 1 +def cmd_subagent_adapter(args: argparse.Namespace) -> int: + project_root = Path(args.project_root).expanduser().resolve() + try: + result = build_subagent_adapter( + project_root, + args.id, + task_id=args.task_id or "", + run_id=args.run_id or "", + purpose=args.purpose or "", + ) + except (FileNotFoundError, KeyError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + if args.json: + emit(result, True) + else: + print(result["marker"]) + print(f"runtime_tool: {result['runtime_tool']}") + print(f"runtime_agent_type: {result['runtime_agent_type']}") + print(f"capability_event: {result['capability_event_suggestion']}") + return 0 + + +def cmd_verify_subagents(args: argparse.Namespace) -> int: + project_root = Path(args.project_root).expanduser().resolve() + try: + result = verify_subagent_catalog( + project_root, + args.task_id, + args.run_id, + native_min_successes=args.native_min_successes, + ) + except (FileNotFoundError, KeyError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + if args.json: + emit(result, True) + else: + print(result["marker"]) + print(f"ledger: {result['ledger']}") + if result["catalog_empty"]: + print("catalog_empty: true") + if result["missing_adapter_role_ids"]: + print("missing_adapter_roles: " + ", ".join(result["missing_adapter_role_ids"])) + if result["adapter_mismatches"]: + print(f"adapter_mismatches: {len(result['adapter_mismatches'])}") + if result["missing_role_ids"]: + print("missing_roles: " + ", ".join(result["missing_role_ids"])) + if result["native_shortfalls"]: + print("native_shortfalls: " + ", ".join(f"{key}={value}" for key, value in result["native_shortfalls"].items())) + if result["invalid_events"]: + print(f"invalid_events: {len(result['invalid_events'])}") + return 0 if result.get("status") == "passed" else 2 + + def cmd_workbench_state(args: argparse.Namespace) -> int: project_root = Path(args.project_root).expanduser().resolve() try: @@ -6195,7 +9082,7 @@ def build_parser() -> argparse.ArgumentParser: phase_task_parser.add_argument("--json", action="store_true") phase_task_parser.set_defaults(func=cmd_phase_task) - capability_event_parser = sub.add_parser("capability-event", help="record an observable MCP, skill, subagent, or script capability call") + capability_event_parser = sub.add_parser("capability-event", help="record an observable mounted capability call") capability_event_parser.add_argument("--project-root", required=True) capability_event_parser.add_argument("--task-id", required=True) capability_event_parser.add_argument("--run-id", required=True) @@ -6204,6 +9091,8 @@ def build_parser() -> argparse.ArgumentParser: capability_event_parser.add_argument("--purpose", required=True, help="public purpose for the capability call") capability_event_parser.add_argument("--status", default="completed") capability_event_parser.add_argument("--evidence", default="") + capability_event_parser.add_argument("--invocation-id", default="", help="host runtime invocation id when available") + capability_event_parser.add_argument("--recovers-event-id", default="", help="exact timed-out capability event this completion resolves") capability_event_parser.add_argument("--json", action="store_true") capability_event_parser.set_defaults(func=cmd_capability_event) @@ -6218,6 +9107,35 @@ def build_parser() -> argparse.ArgumentParser: trace_step_parser.add_argument("--json", action="store_true") trace_step_parser.set_defaults(func=cmd_trace_step) + decision_event_parser = sub.add_parser("decision-event", help="record a public Decision Graph event for a run") + decision_event_parser.add_argument("--project-root", required=True) + decision_event_parser.add_argument("--task-id", required=True) + decision_event_parser.add_argument("--run-id", required=True) + decision_event_parser.add_argument("--kind", required=True, choices=sorted(DECISION_EVENT_KINDS)) + decision_event_parser.add_argument("--status", default="active", choices=sorted(DECISION_EVENT_STATUSES)) + decision_event_parser.add_argument("--title", required=True) + decision_event_parser.add_argument("--summary", default="") + decision_event_parser.add_argument("--reason", default="", help="public decision rationale; do not include hidden chain-of-thought") + decision_event_parser.add_argument("--evidence", default="", help="command, file, or user confirmation evidence") + decision_event_parser.add_argument("--parent-id", default="") + decision_event_parser.add_argument("--option", action="append", default=[]) + decision_event_parser.add_argument("--chosen", default="") + decision_event_parser.add_argument("--linked-step", default="", choices=["", *OPERATIONAL_TRACE_STEPS]) + decision_event_parser.add_argument("--linked-capability-event-id", default="") + decision_event_parser.add_argument("--linked-effect-assertion-id", default="") + decision_event_parser.add_argument("--json", action="store_true") + decision_event_parser.set_defaults(func=cmd_decision_event) + + decision_query_parser = sub.add_parser("decision-query", help="query public Decision Graph events for a run") + decision_query_parser.add_argument("--project-root", required=True) + decision_query_parser.add_argument("--run-id", required=True) + decision_query_parser.add_argument("--task-id", default="") + decision_query_parser.add_argument("--kind", default="", choices=["", *sorted(DECISION_EVENT_KINDS)]) + decision_query_parser.add_argument("--status", default="", choices=["", *sorted(DECISION_EVENT_STATUSES)]) + decision_query_parser.add_argument("--parent-id", default="") + decision_query_parser.add_argument("--json", action="store_true") + decision_query_parser.set_defaults(func=cmd_decision_query) + artifact_assert_parser = sub.add_parser("artifact-assert", help="verify a real side effect before recording EFFECT_OK evidence") artifact_assert_parser.add_argument("--project-root", required=True) artifact_assert_parser.add_argument("--task-id", required=True) @@ -6268,6 +9186,13 @@ def build_parser() -> argparse.ArgumentParser: verify_effects_parser.add_argument("--json", action="store_true") verify_effects_parser.set_defaults(func=cmd_verify_effects) + verify_decisions_parser = sub.add_parser("verify-decisions", help="verify Decision Graph events and command evidence for a run") + verify_decisions_parser.add_argument("--project-root", required=True) + verify_decisions_parser.add_argument("--task-id", required=True) + verify_decisions_parser.add_argument("--run-id", required=True) + verify_decisions_parser.add_argument("--json", action="store_true") + verify_decisions_parser.set_defaults(func=cmd_verify_decisions) + complete = sub.add_parser("complete-task", help="complete a task only after eval, lifecycle, outputs, and required postflight pass") complete.add_argument("--project-root", required=True) complete.add_argument("--task-id", required=True) @@ -6304,17 +9229,80 @@ def build_parser() -> argparse.ArgumentParser: dispatch.add_argument("--json", action="store_true") dispatch.set_defaults(func=cmd_dispatch_task) - render_html = sub.add_parser("render-html", help="render Markdown evidence into composable static HTML sidecars") + dispatch_report = sub.add_parser("dispatch-report", help="summarize all actual mounted capability events for a run") + dispatch_report.add_argument("--project-root", required=True) + dispatch_report.add_argument("--task-id", required=True) + dispatch_report.add_argument("--run-id", required=True) + dispatch_report.add_argument("--json", action="store_true") + dispatch_report.set_defaults(func=cmd_dispatch_report) + + render_html = sub.add_parser("render-html", help="render Markdown, flow, and Decision Graph evidence into composable static HTML sidecars") render_html.add_argument("--project-root", required=True) - render_html.add_argument("--kind", choices=sorted(HTML_REPORT_KINDS), help="receipt, handoff, or rich-report") - render_html.add_argument("--run-id", help="run id for receipt or handoff rendering") + render_html.add_argument("--kind", choices=sorted(HTML_REPORT_KINDS), help="receipt, handoff, rich-report, decision-map, or mission-flow") + render_html.add_argument("--run-id", help="run id for receipt, handoff, decision-map, or mission-flow rendering") + render_html.add_argument("--task-id", help="optional task id for mission-flow; defaults from run.yaml") render_html.add_argument("--input", help="Markdown source for rich-report") render_html.add_argument("--output", help="HTML output path; defaults beside the source") render_html.add_argument("--compose", help="compose report manifests into one HTML page") render_html.add_argument("--theme", default=HTML_DEFAULT_THEME) + render_html.add_argument( + "--presentation", + choices=sorted(HTML_PRESENTATION_MODES), + help="presentation shell: default, minimal, bare, or fragment; project reporting policy is used when omitted", + ) render_html.add_argument("--json", action="store_true") render_html.set_defaults(func=cmd_render_html) + flow_summary = sub.add_parser("flow-summary", help="print a friendly layered Mermaid mission flow for a run") + flow_summary.add_argument("--project-root", required=True) + flow_summary.add_argument("--run-id", required=True) + flow_summary.add_argument("--task-id", help="optional task id; defaults from run.yaml") + flow_summary.add_argument("--format", choices=["mermaid", "markdown"], default="mermaid") + flow_summary.add_argument("--json", action="store_true") + flow_summary.set_defaults(func=cmd_flow_summary) + + thread_plan = sub.add_parser("thread-plan", help="manage chat-level append-only natural-language plan ledgers") + thread_sub = thread_plan.add_subparsers(dest="thread_action", required=True) + thread_start = thread_sub.add_parser("start", help="start a chat-level Thread Plan Ledger") + thread_start.add_argument("--project-root", required=True) + thread_start.add_argument("--title", required=True) + thread_start.add_argument("--spec-id", default="") + thread_start.add_argument("--json", action="store_true") + thread_start.set_defaults(func=cmd_thread_plan) + + thread_current = thread_sub.add_parser("current", help="show the current active Thread Plan Ledger") + thread_current.add_argument("--project-root", required=True) + thread_current.add_argument("--json", action="store_true") + thread_current.set_defaults(func=cmd_thread_plan) + + thread_append = thread_sub.add_parser("append", help="append a natural-language plan note") + thread_append.add_argument("--project-root", required=True) + thread_append.add_argument("--thread-id", required=True) + thread_append.add_argument("--kind", required=True, choices=sorted(THREAD_PLAN_EVENT_KINDS)) + thread_append.add_argument("--text", required=True) + thread_append.add_argument("--spec-id", default="") + thread_append.add_argument("--task-id", default="") + thread_append.add_argument("--run-id", default="") + thread_append.add_argument("--parent-event-id", default="") + thread_append.add_argument("--json", action="store_true") + thread_append.set_defaults(func=cmd_thread_plan) + + thread_link = thread_sub.add_parser("link-run", help="link a task run to the current chat-level plan") + thread_link.add_argument("--project-root", required=True) + thread_link.add_argument("--thread-id", required=True) + thread_link.add_argument("--task-id", required=True) + thread_link.add_argument("--run-id", required=True) + thread_link.add_argument("--text", default="") + thread_link.add_argument("--json", action="store_true") + thread_link.set_defaults(func=cmd_thread_plan) + + thread_render = thread_sub.add_parser("render", help="render a thread plan as Markdown, Mermaid, or HTML") + thread_render.add_argument("--project-root", required=True) + thread_render.add_argument("--thread-id", required=True) + thread_render.add_argument("--format", choices=["markdown", "html", "mermaid"], default="markdown") + thread_render.add_argument("--json", action="store_true") + thread_render.set_defaults(func=cmd_thread_plan) + receipt = sub.add_parser("receipt", help="write a local project receipt") receipt.add_argument("--project-root", required=True) receipt.add_argument("--summary", required=True) @@ -6364,6 +9352,23 @@ def build_parser() -> argparse.ArgumentParser: runtime_adapters.add_argument("--json", action="store_true") runtime_adapters.set_defaults(func=cmd_runtime_adapters) + subagent_adapter = sub.add_parser("subagent-adapter", help="resolve a registered subagent into a Codex runtime adapter call package") + subagent_adapter.add_argument("--project-root", required=True) + subagent_adapter.add_argument("--id", required=True, help="registered subagent id, for example codex-explorer or maestro-architect") + subagent_adapter.add_argument("--task-id", help="optional task id; when paired with --run-id records command evidence") + subagent_adapter.add_argument("--run-id", help="optional run id; when paired with --task-id records command evidence") + subagent_adapter.add_argument("--purpose", default="", help="short public purpose for the suggested capability-event") + subagent_adapter.add_argument("--json", action="store_true") + subagent_adapter.set_defaults(func=cmd_subagent_adapter) + + verify_subagents = sub.add_parser("verify-subagents", help="verify challenge-bound parent attestations for an immutable runtime subagent catalog") + verify_subagents.add_argument("--project-root", required=True) + verify_subagents.add_argument("--task-id", required=True) + verify_subagents.add_argument("--run-id", required=True) + verify_subagents.add_argument("--native-min-successes", type=int, default=DEFAULT_NATIVE_SUBAGENT_SUCCESSES) + verify_subagents.add_argument("--json", action="store_true") + verify_subagents.set_defaults(func=cmd_verify_subagents) + preview = sub.add_parser("workbench-preview", help="serve the read-only Workbench preview with live project state") preview.add_argument("--project-root", required=True) preview.add_argument("--host", default="127.0.0.1") diff --git a/templates/capability-layer/subagents/maestro/maestro-accessibility-specialist.yaml b/templates/capability-layer/subagents/maestro/maestro-accessibility-specialist.yaml new file mode 100644 index 0000000..a8eac55 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-accessibility-specialist.yaml @@ -0,0 +1,7 @@ +id: maestro-accessibility-specialist +display_name: Maestro Accessibility Specialist +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Accessibility Specialist through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-analytics-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-analytics-engineer.yaml new file mode 100644 index 0000000..4237215 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-analytics-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-analytics-engineer +display_name: Maestro Analytics Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Analytics Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-api-designer.yaml b/templates/capability-layer/subagents/maestro/maestro-api-designer.yaml new file mode 100644 index 0000000..58bb4bd --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-api-designer.yaml @@ -0,0 +1,7 @@ +id: maestro-api-designer +display_name: Maestro Api Designer +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Api Designer through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-architect.yaml b/templates/capability-layer/subagents/maestro/maestro-architect.yaml new file mode 100644 index 0000000..871f96d --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-architect.yaml @@ -0,0 +1,7 @@ +id: maestro-architect +display_name: Maestro Architect +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Architect through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-cloud-architect.yaml b/templates/capability-layer/subagents/maestro/maestro-cloud-architect.yaml new file mode 100644 index 0000000..15618bd --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-cloud-architect.yaml @@ -0,0 +1,7 @@ +id: maestro-cloud-architect +display_name: Maestro Cloud Architect +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Cloud Architect through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-cobol-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-cobol-engineer.yaml new file mode 100644 index 0000000..10f0105 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-cobol-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-cobol-engineer +display_name: Maestro Cobol Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Cobol Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-code-reviewer.yaml b/templates/capability-layer/subagents/maestro/maestro-code-reviewer.yaml new file mode 100644 index 0000000..a310266 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-code-reviewer.yaml @@ -0,0 +1,7 @@ +id: maestro-code-reviewer +display_name: Maestro Code Reviewer +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,code_review +role_prompt: Act as Maestro Code Reviewer through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-coder.yaml b/templates/capability-layer/subagents/maestro/maestro-coder.yaml new file mode 100644 index 0000000..f1f62bf --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-coder.yaml @@ -0,0 +1,7 @@ +id: maestro-coder +display_name: Maestro Coder +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Coder through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-compliance-reviewer.yaml b/templates/capability-layer/subagents/maestro/maestro-compliance-reviewer.yaml new file mode 100644 index 0000000..1856135 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-compliance-reviewer.yaml @@ -0,0 +1,7 @@ +id: maestro-compliance-reviewer +display_name: Maestro Compliance Reviewer +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,security_audit,code_review +role_prompt: Act as Maestro Compliance Reviewer through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-content-strategist.yaml b/templates/capability-layer/subagents/maestro/maestro-content-strategist.yaml new file mode 100644 index 0000000..49e82e4 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-content-strategist.yaml @@ -0,0 +1,7 @@ +id: maestro-content-strategist +display_name: Maestro Content Strategist +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Content Strategist through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-copywriter.yaml b/templates/capability-layer/subagents/maestro/maestro-copywriter.yaml new file mode 100644 index 0000000..5027cf7 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-copywriter.yaml @@ -0,0 +1,7 @@ +id: maestro-copywriter +display_name: Maestro Copywriter +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Copywriter through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-data-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-data-engineer.yaml new file mode 100644 index 0000000..fe0120a --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-data-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-data-engineer +display_name: Maestro Data Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Data Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-database-administrator.yaml b/templates/capability-layer/subagents/maestro/maestro-database-administrator.yaml new file mode 100644 index 0000000..83a97e7 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-database-administrator.yaml @@ -0,0 +1,7 @@ +id: maestro-database-administrator +display_name: Maestro Database Administrator +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Database Administrator through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-db2-dba.yaml b/templates/capability-layer/subagents/maestro/maestro-db2-dba.yaml new file mode 100644 index 0000000..f7304d4 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-db2-dba.yaml @@ -0,0 +1,7 @@ +id: maestro-db2-dba +display_name: Maestro Db2 Dba +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Db2 Dba through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-debugger.yaml b/templates/capability-layer/subagents/maestro/maestro-debugger.yaml new file mode 100644 index 0000000..1f84ae4 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-debugger.yaml @@ -0,0 +1,7 @@ +id: maestro-debugger +display_name: Maestro Debugger +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Debugger through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-design-system-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-design-system-engineer.yaml new file mode 100644 index 0000000..ff951b5 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-design-system-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-design-system-engineer +display_name: Maestro Design System Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Design System Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-devops-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-devops-engineer.yaml new file mode 100644 index 0000000..79b3002 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-devops-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-devops-engineer +display_name: Maestro Devops Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Devops Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-hlasm-assembler-specialist.yaml b/templates/capability-layer/subagents/maestro/maestro-hlasm-assembler-specialist.yaml new file mode 100644 index 0000000..3cc705f --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-hlasm-assembler-specialist.yaml @@ -0,0 +1,7 @@ +id: maestro-hlasm-assembler-specialist +display_name: Maestro Hlasm Assembler Specialist +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Hlasm Assembler Specialist through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-i18n-specialist.yaml b/templates/capability-layer/subagents/maestro/maestro-i18n-specialist.yaml new file mode 100644 index 0000000..1b3a35b --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-i18n-specialist.yaml @@ -0,0 +1,7 @@ +id: maestro-i18n-specialist +display_name: Maestro I18N Specialist +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro I18N Specialist through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-ibm-i-specialist.yaml b/templates/capability-layer/subagents/maestro/maestro-ibm-i-specialist.yaml new file mode 100644 index 0000000..0a32152 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-ibm-i-specialist.yaml @@ -0,0 +1,7 @@ +id: maestro-ibm-i-specialist +display_name: Maestro Ibm I Specialist +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Ibm I Specialist through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-integration-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-integration-engineer.yaml new file mode 100644 index 0000000..b1e5391 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-integration-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-integration-engineer +display_name: Maestro Integration Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Integration Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-ml-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-ml-engineer.yaml new file mode 100644 index 0000000..34b931f --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-ml-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-ml-engineer +display_name: Maestro Ml Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Ml Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-mlops-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-mlops-engineer.yaml new file mode 100644 index 0000000..526b8ae --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-mlops-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-mlops-engineer +display_name: Maestro Mlops Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Mlops Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-mobile-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-mobile-engineer.yaml new file mode 100644 index 0000000..1756e59 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-mobile-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-mobile-engineer +display_name: Maestro Mobile Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Mobile Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-observability-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-observability-engineer.yaml new file mode 100644 index 0000000..55912df --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-observability-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-observability-engineer +display_name: Maestro Observability Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Observability Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-performance-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-performance-engineer.yaml new file mode 100644 index 0000000..adc9d85 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-performance-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-performance-engineer +display_name: Maestro Performance Engineer +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Performance Engineer through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-platform-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-platform-engineer.yaml new file mode 100644 index 0000000..473d2ce --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-platform-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-platform-engineer +display_name: Maestro Platform Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Platform Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-product-manager.yaml b/templates/capability-layer/subagents/maestro/maestro-product-manager.yaml new file mode 100644 index 0000000..a0477e7 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-product-manager.yaml @@ -0,0 +1,7 @@ +id: maestro-product-manager +display_name: Maestro Product Manager +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Product Manager through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-prompt-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-prompt-engineer.yaml new file mode 100644 index 0000000..5bd086d --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-prompt-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-prompt-engineer +display_name: Maestro Prompt Engineer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Prompt Engineer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-refactor.yaml b/templates/capability-layer/subagents/maestro/maestro-refactor.yaml new file mode 100644 index 0000000..8353ed4 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-refactor.yaml @@ -0,0 +1,7 @@ +id: maestro-refactor +display_name: Maestro Refactor +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Refactor through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-release-manager.yaml b/templates/capability-layer/subagents/maestro/maestro-release-manager.yaml new file mode 100644 index 0000000..ca56dfc --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-release-manager.yaml @@ -0,0 +1,7 @@ +id: maestro-release-manager +display_name: Maestro Release Manager +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Release Manager through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-security-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-security-engineer.yaml new file mode 100644 index 0000000..2e5d599 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-security-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-security-engineer +display_name: Maestro Security Engineer +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,security_audit +role_prompt: Act as Maestro Security Engineer through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-seo-specialist.yaml b/templates/capability-layer/subagents/maestro/maestro-seo-specialist.yaml new file mode 100644 index 0000000..de658de --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-seo-specialist.yaml @@ -0,0 +1,7 @@ +id: maestro-seo-specialist +display_name: Maestro Seo Specialist +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Seo Specialist through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-site-reliability-engineer.yaml b/templates/capability-layer/subagents/maestro/maestro-site-reliability-engineer.yaml new file mode 100644 index 0000000..d639591 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-site-reliability-engineer.yaml @@ -0,0 +1,7 @@ +id: maestro-site-reliability-engineer +display_name: Maestro Site Reliability Engineer +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Site Reliability Engineer through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-solutions-architect.yaml b/templates/capability-layer/subagents/maestro/maestro-solutions-architect.yaml new file mode 100644 index 0000000..ca42841 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-solutions-architect.yaml @@ -0,0 +1,7 @@ +id: maestro-solutions-architect +display_name: Maestro Solutions Architect +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Solutions Architect through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-technical-writer.yaml b/templates/capability-layer/subagents/maestro/maestro-technical-writer.yaml new file mode 100644 index 0000000..b8a5a09 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-technical-writer.yaml @@ -0,0 +1,7 @@ +id: maestro-technical-writer +display_name: Maestro Technical Writer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Technical Writer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-tester.yaml b/templates/capability-layer/subagents/maestro/maestro-tester.yaml new file mode 100644 index 0000000..3b6efe1 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-tester.yaml @@ -0,0 +1,7 @@ +id: maestro-tester +display_name: Maestro Tester +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,testing_task +role_prompt: Act as Maestro Tester through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-ux-designer.yaml b/templates/capability-layer/subagents/maestro/maestro-ux-designer.yaml new file mode 100644 index 0000000..5fa4f1b --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-ux-designer.yaml @@ -0,0 +1,7 @@ +id: maestro-ux-designer +display_name: Maestro Ux Designer +runtime_agent_type: worker +default_scope: maestro-specialist-agent +write_policy_hint: read_write +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Ux Designer through the Codex worker subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/maestro-zos-sysprog.yaml b/templates/capability-layer/subagents/maestro/maestro-zos-sysprog.yaml new file mode 100644 index 0000000..264b258 --- /dev/null +++ b/templates/capability-layer/subagents/maestro/maestro-zos-sysprog.yaml @@ -0,0 +1,7 @@ +id: maestro-zos-sysprog +display_name: Maestro Zos Sysprog +runtime_agent_type: explorer +default_scope: maestro-specialist-agent +write_policy_hint: read_only +recommended_task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task +role_prompt: Act as Maestro Zos Sysprog through the Codex explorer subagent runtime. Stay within the parent KnowledgeOS route, return concise evidence-backed findings, and report gaps or skipped work explicitly. diff --git a/templates/capability-layer/subagents/maestro/manifest.yaml b/templates/capability-layer/subagents/maestro/manifest.yaml new file mode 100644 index 0000000..f8bf93a --- /dev/null +++ b/templates/capability-layer/subagents/maestro/manifest.yaml @@ -0,0 +1,161 @@ +# Maestro adapter catalog. +# Source of truth for adapter-backed Maestro role prompts. +schema_version: knowledgeos.maestro-adapter.v1 +runtime_tool: multi_agent_v1.spawn_agent +roles: + - id: maestro-accessibility-specialist + display_name: Maestro Accessibility Specialist + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-accessibility-specialist.yaml + - id: maestro-analytics-engineer + display_name: Maestro Analytics Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-analytics-engineer.yaml + - id: maestro-api-designer + display_name: Maestro Api Designer + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-api-designer.yaml + - id: maestro-architect + display_name: Maestro Architect + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-architect.yaml + - id: maestro-cloud-architect + display_name: Maestro Cloud Architect + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-cloud-architect.yaml + - id: maestro-cobol-engineer + display_name: Maestro Cobol Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-cobol-engineer.yaml + - id: maestro-code-reviewer + display_name: Maestro Code Reviewer + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-code-reviewer.yaml + - id: maestro-coder + display_name: Maestro Coder + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-coder.yaml + - id: maestro-compliance-reviewer + display_name: Maestro Compliance Reviewer + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-compliance-reviewer.yaml + - id: maestro-content-strategist + display_name: Maestro Content Strategist + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-content-strategist.yaml + - id: maestro-copywriter + display_name: Maestro Copywriter + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-copywriter.yaml + - id: maestro-data-engineer + display_name: Maestro Data Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-data-engineer.yaml + - id: maestro-database-administrator + display_name: Maestro Database Administrator + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-database-administrator.yaml + - id: maestro-db2-dba + display_name: Maestro Db2 Dba + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-db2-dba.yaml + - id: maestro-debugger + display_name: Maestro Debugger + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-debugger.yaml + - id: maestro-design-system-engineer + display_name: Maestro Design System Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-design-system-engineer.yaml + - id: maestro-devops-engineer + display_name: Maestro Devops Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-devops-engineer.yaml + - id: maestro-hlasm-assembler-specialist + display_name: Maestro Hlasm Assembler Specialist + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-hlasm-assembler-specialist.yaml + - id: maestro-i18n-specialist + display_name: Maestro I18N Specialist + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-i18n-specialist.yaml + - id: maestro-ibm-i-specialist + display_name: Maestro Ibm I Specialist + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-ibm-i-specialist.yaml + - id: maestro-integration-engineer + display_name: Maestro Integration Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-integration-engineer.yaml + - id: maestro-ml-engineer + display_name: Maestro Ml Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-ml-engineer.yaml + - id: maestro-mlops-engineer + display_name: Maestro Mlops Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-mlops-engineer.yaml + - id: maestro-mobile-engineer + display_name: Maestro Mobile Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-mobile-engineer.yaml + - id: maestro-observability-engineer + display_name: Maestro Observability Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-observability-engineer.yaml + - id: maestro-performance-engineer + display_name: Maestro Performance Engineer + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-performance-engineer.yaml + - id: maestro-platform-engineer + display_name: Maestro Platform Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-platform-engineer.yaml + - id: maestro-product-manager + display_name: Maestro Product Manager + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-product-manager.yaml + - id: maestro-prompt-engineer + display_name: Maestro Prompt Engineer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-prompt-engineer.yaml + - id: maestro-refactor + display_name: Maestro Refactor + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-refactor.yaml + - id: maestro-release-manager + display_name: Maestro Release Manager + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-release-manager.yaml + - id: maestro-security-engineer + display_name: Maestro Security Engineer + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-security-engineer.yaml + - id: maestro-seo-specialist + display_name: Maestro Seo Specialist + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-seo-specialist.yaml + - id: maestro-site-reliability-engineer + display_name: Maestro Site Reliability Engineer + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-site-reliability-engineer.yaml + - id: maestro-solutions-architect + display_name: Maestro Solutions Architect + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-solutions-architect.yaml + - id: maestro-technical-writer + display_name: Maestro Technical Writer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-technical-writer.yaml + - id: maestro-tester + display_name: Maestro Tester + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-tester.yaml + - id: maestro-ux-designer + display_name: Maestro Ux Designer + runtime_agent_type: worker + spec: capability-layer/subagents/maestro/maestro-ux-designer.yaml + - id: maestro-zos-sysprog + display_name: Maestro Zos Sysprog + runtime_agent_type: explorer + spec: capability-layer/subagents/maestro/maestro-zos-sysprog.yaml diff --git a/templates/project-control-plane/.agent-os/decision-policy.yaml b/templates/project-control-plane/.agent-os/decision-policy.yaml new file mode 100644 index 0000000..a5258cc --- /dev/null +++ b/templates/project-control-plane/.agent-os/decision-policy.yaml @@ -0,0 +1,3 @@ +decision_policy: + strictness: warn + downgrade_reason: "" diff --git a/templates/project-control-plane/.agent-os/startup-prompt.md b/templates/project-control-plane/.agent-os/startup-prompt.md index 2138937..1ae04a4 100644 --- a/templates/project-control-plane/.agent-os/startup-prompt.md +++ b/templates/project-control-plane/.agent-os/startup-prompt.md @@ -9,25 +9,34 @@ This startup prompt is only the session trigger. Durable rules live in `AGENTS.m Before substantial work: 1. Read `AGENTS.md`. -2. Read `.agent-os/workspace.yaml`, `.agent-os/project.yaml`, `.agent-os/tasks.yaml`, `.agent-os/specs.yaml`, `.agent-os/phase-policy.yaml`, `.agent-os/effect-policy.yaml`, `.agent-os/decisions.yaml`, `.agent-os/evals.yaml`, `.agent-os/fabric-link.yaml`, `.agent-os/read-policy.yaml`, `.agent-os/write-policy.yaml`, `.agent-os/dispatch-policy.yaml`, and `.agent-os/tool-registry.yaml`. +2. Read `.agent-os/workspace.yaml`, `.agent-os/project.yaml`, `.agent-os/tasks.yaml`, `.agent-os/specs.yaml`, `.agent-os/phase-policy.yaml`, `.agent-os/decision-policy.yaml`, `.agent-os/effect-policy.yaml`, `.agent-os/decisions.yaml`, `.agent-os/evals.yaml`, `.agent-os/fabric-link.yaml`, `.agent-os/read-policy.yaml`, `.agent-os/write-policy.yaml`, `.agent-os/dispatch-policy.yaml`, and `.agent-os/tool-registry.yaml`. 3. Run `CHANGE_ME_KNOWLEDGEOS_BIN doctor --project-root CHANGE_ME_PROJECT_ROOT --summary` and do not proceed if it fails. -4. Select or confirm one task id from `.agent-os/tasks.yaml`; if no ready task fits the user's new request, run `CHANGE_ME_KNOWLEDGEOS_BIN create-task --project-root CHANGE_ME_PROJECT_ROOT --title "" --type <type> --output <path> --acceptance "<check>"`. -5. Run `CHANGE_ME_KNOWLEDGEOS_BIN route-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id>`. -6. Run `CHANGE_ME_KNOWLEDGEOS_BIN dispatch-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id>` before invoking subagents, MCP tools, skills, workflows, or scripts. -7. Before planned mutation, run `CHANGE_ME_KNOWLEDGEOS_BIN check-route-write --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --path <planned-path>`. -8. Create run evidence with `CHANGE_ME_KNOWLEDGEOS_BIN run-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id>`. -9. Pause at consultation checkpoints, state your recommended next move, name the tradeoff, and ask the human whether to proceed. -10. Record run-bound dispatch evidence with `CHANGE_ME_KNOWLEDGEOS_BIN dispatch-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`. -11. Record public operational progress with `CHANGE_ME_KNOWLEDGEOS_BIN trace-step --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --step <step> --note "<public trace>" --evidence "<command/file/user confirmation>"`, and relay the returned `TRACE_OK` marker. -12. Record lifecycle evidence with `CHANGE_ME_KNOWLEDGEOS_BIN phase-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --phase <route|plan|review|dispatch|execute|report> --status completed --note "<public trace>" --evidence "<command/file/user confirmation>"`, and relay the returned `CHECKPOINT_OK` marker. -13. Record MCP, skill, subagent, orchestrator, or important script use with `CHANGE_ME_KNOWLEDGEOS_BIN capability-event --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose "<purpose>"`, and relay the returned `CAPABILITY_OK` marker. -14. Verify real side effects with `CHANGE_ME_KNOWLEDGEOS_BIN artifact-assert --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --kind <kind> --path <artifact>` and relay the returned `EFFECT_OK` marker. -15. Run `CHANGE_ME_KNOWLEDGEOS_BIN eval-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`; do not manually append eval status. -16. Run `CHANGE_ME_KNOWLEDGEOS_BIN verify-context --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`, `CHANGE_ME_KNOWLEDGEOS_BIN verify-lifecycle --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`, and `CHANGE_ME_KNOWLEDGEOS_BIN verify-effects --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`; relay the returned `EFFECT_VERIFY_OK` marker before claiming effect verification success. -17. Use `CHANGE_ME_KNOWLEDGEOS_BIN complete-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --summary "<summary>"`; it enforces lifecycle, capability visibility, effect verification, and required postflight. -18. If a shared-fabric postflight hook is configured, report `[SYNC_OK]` only after `complete-task` returns `sync_status: SYNC_OK`. -19. For reset requests, run `CHANGE_ME_KNOWLEDGEOS_BIN reset-project --project-root CHANGE_ME_PROJECT_ROOT --mode <soft|hard> --dry-run` before any destructive action. -20. For old-project reorganization requests, run `CHANGE_ME_KNOWLEDGEOS_BIN migrate-legacy-project --project-root CHANGE_ME_PROJECT_ROOT --write-plan` before moving files. -21. For historical or superseded files that should be stored but not read by default, run `CHANGE_ME_KNOWLEDGEOS_BIN archive-legacy-project --project-root CHANGE_ME_PROJECT_ROOT --write-plan` before moving files into `archive/`. +4. If the user starts or continues a durable plan/spec conversation, run `CHANGE_ME_KNOWLEDGEOS_BIN thread-plan current --project-root CHANGE_ME_PROJECT_ROOT` or `CHANGE_ME_KNOWLEDGEOS_BIN thread-plan start --project-root CHANGE_ME_PROJECT_ROOT --title "<natural language goal>"`; append plain-language progress with `CHANGE_ME_KNOWLEDGEOS_BIN thread-plan append --project-root CHANGE_ME_PROJECT_ROOT --thread-id <thread-id> --kind <kind> --text "<plain note>"` and relay `THREAD_PLAN_OK`. +5. Select or confirm one task id from `.agent-os/tasks.yaml`; if no ready task fits the user's new request, run `CHANGE_ME_KNOWLEDGEOS_BIN create-task --project-root CHANGE_ME_PROJECT_ROOT --title "<title>" --type <type> --output <path> --acceptance "<check>"`. +6. Run `CHANGE_ME_KNOWLEDGEOS_BIN route-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id>`. +7. Run `CHANGE_ME_KNOWLEDGEOS_BIN dispatch-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id>` before invoking subagents, MCP tools, skills, workflows, or scripts, and relay `AGENT_DISPATCH_PLAN`. +7a. If `dispatch-task` selects `codex-*` or `maestro-*` subagents, run `CHANGE_ME_KNOWLEDGEOS_BIN subagent-adapter --project-root CHANGE_ME_PROJECT_ROOT --id <subagent-id>` to resolve the Codex runtime call package. Actual delegation must use the Codex runtime subagent tool, then be recorded with `capability-event --kind subagent`. +7b. Preserve every spawned agent id. For substantive work, use one multi-minute `wait_agent` window rather than repeated short polls. If it expires, send one interrupt requesting current findings, wait once more, and close only after a terminal result. Reconcile a late result with a completed capability event containing `recovered_from=timed_out`. +7c. A bounded spawned subagent executes the parent-assigned scope directly and returns findings. It must not create/reopen tasks or specs, run completion/postflight, or recursively dispatch subagents unless the parent explicitly delegated orchestration. +8. Before planned mutation, run `CHANGE_ME_KNOWLEDGEOS_BIN check-route-write --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --path <planned-path>`. +9. Create run evidence with `CHANGE_ME_KNOWLEDGEOS_BIN run-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id>`. +10. Pause at consultation checkpoints, state your recommended next move, name the tradeoff, and ask the human whether to proceed. +11. Record run-bound dispatch evidence with `CHANGE_ME_KNOWLEDGEOS_BIN dispatch-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`. +12. Record public operational progress with `CHANGE_ME_KNOWLEDGEOS_BIN trace-step --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --step <step> --note "<public trace>" --evidence "<command/file/user confirmation>"`, and relay the returned `TRACE_OK` marker. +13. Record lifecycle evidence with `CHANGE_ME_KNOWLEDGEOS_BIN phase-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --phase <route|plan|review|dispatch|execute|report> --status completed --note "<public trace>" --evidence "<command/file/user confirmation>"`, and relay the returned `CHECKPOINT_OK` marker. +14. Record MCP, skill, plugin/app, browser/Chrome/GitHub/security connector, subagent, orchestrator, shell, file_read, or important script use with `CHANGE_ME_KNOWLEDGEOS_BIN capability-event --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose "<purpose>"`, and relay the returned `CAPABILITY_OK` marker. +14a. If a planned `codex-*` or `maestro-*` subagent is not used or the runtime times out, record `capability-event --kind subagent --status skipped|timed_out|blocked|close_failed` with the reason before `dispatch-report`. +14aa. For an explicit catalog validation task, run `CHANGE_ME_KNOWLEDGEOS_BIN verify-subagents --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>` and relay `SUBAGENT_CATALOG_OK` only after challenge-bound parent attestations cover the immutable run catalog; do not claim independent host verification. +14b. Summarize actual capability dispatch with `CHANGE_ME_KNOWLEDGEOS_BIN dispatch-report --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>` and relay `AGENT_DISPATCH_OK`. +15. Record public decision changes with `CHANGE_ME_KNOWLEDGEOS_BIN decision-event --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --kind <kind> --title "<title>" --summary "<summary>" --reason "<reason>" --evidence "<evidence>"`, and relay the returned `DECISION_OK` marker when plans branch, change, roll back, or abandon a route. +16. Verify real side effects with `CHANGE_ME_KNOWLEDGEOS_BIN artifact-assert --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --kind <kind> --path <artifact>` and relay the returned `EFFECT_OK` marker. +17. Run `CHANGE_ME_KNOWLEDGEOS_BIN eval-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`; do not manually append eval status. +18. Run `CHANGE_ME_KNOWLEDGEOS_BIN verify-context --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`, `CHANGE_ME_KNOWLEDGEOS_BIN verify-lifecycle --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`, `CHANGE_ME_KNOWLEDGEOS_BIN verify-effects --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`, and `CHANGE_ME_KNOWLEDGEOS_BIN verify-decisions --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id>`; relay `EFFECT_VERIFY_OK` and `DECISION_VERIFY_OK` before claiming verification success. +19. Use `CHANGE_ME_KNOWLEDGEOS_BIN complete-task --project-root CHANGE_ME_PROJECT_ROOT --task-id <task-id> --run-id <run-id> --summary "<summary>"`; it enforces lifecycle, capability visibility, effect verification, decision verification, and required postflight. +20. For medium, high, or complex tasks, include the returned `FLOW_OK` Mermaid Mission Flow in the final answer; if needed, run `CHANGE_ME_KNOWLEDGEOS_BIN flow-summary --project-root CHANGE_ME_PROJECT_ROOT --run-id <run-id>`. +21. If a shared-fabric postflight hook is configured, report `[SYNC_OK]` only after `complete-task` returns `sync_status: SYNC_OK`. +22. For reset requests, run `CHANGE_ME_KNOWLEDGEOS_BIN reset-project --project-root CHANGE_ME_PROJECT_ROOT --mode <soft|hard> --dry-run` before any destructive action. +23. For old-project reorganization requests, run `CHANGE_ME_KNOWLEDGEOS_BIN migrate-legacy-project --project-root CHANGE_ME_PROJECT_ROOT --write-plan` before moving files. +24. For historical or superseded files that should be stored but not read by default, run `CHANGE_ME_KNOWLEDGEOS_BIN archive-legacy-project --project-root CHANGE_ME_PROJECT_ROOT --write-plan` before moving files into `archive/`. -Never claim boot, route, dispatch, write safety, trace, phase, lifecycle, capability, effect, eval, completion, or sync success without command evidence. +Never claim boot, route, dispatch, write safety, thread plan, trace, phase, lifecycle, capability, decision, effect, eval, completion, flow, or sync success without command evidence. diff --git a/templates/project-control-plane/.agent-os/tool-registry.yaml b/templates/project-control-plane/.agent-os/tool-registry.yaml index 7890ae7..d700f02 100644 --- a/templates/project-control-plane/.agent-os/tool-registry.yaml +++ b/templates/project-control-plane/.agent-os/tool-registry.yaml @@ -46,3 +46,711 @@ tools: task_fit: "*" capability_fit: process_memory,postflight_memory human_gate: false + + - id: codex-default + kind: subagent + status: recommended + scope: codex-native-subagent + invocation: via_codex_runtime_spawn_agent + task_fit: capability_orchestration,knowledge_structuring,report_task,documentation_task + capability_fit: codex_native,default_agent,general_execution + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: default + human_gate: true + execution_mode: ask + source_path: codex-runtime:multi_agent_v1.spawn_agent + notes: Codex native runtime subagent. Dispatch plan generates intent; actual spawn happens through Codex runtime. + + - id: codex-explorer + kind: subagent + status: recommended + scope: codex-native-subagent + invocation: via_codex_runtime_spawn_agent + task_fit: research_task,documentation_task,analysis_task,security_audit,architecture_design + capability_fit: codex_native,exploration,read_only,research + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + human_gate: true + execution_mode: ask + source_path: codex-runtime:multi_agent_v1.spawn_agent + notes: Codex native runtime subagent. Dispatch plan generates intent; actual spawn happens through Codex runtime. + + - id: codex-worker + kind: subagent + status: recommended + scope: codex-native-subagent + invocation: via_codex_runtime_spawn_agent + task_fit: engineering_change,route_bound_execution_guard,executable_control_plane,workflow_routing,tool_registry + capability_fit: codex_native,implementation,read_write,tests + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + human_gate: true + execution_mode: ask + source_path: codex-runtime:multi_agent_v1.spawn_agent + notes: Codex native runtime subagent. Dispatch plan generates intent; actual spawn happens through Codex runtime. + + - id: maestro-accessibility-specialist + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,accessibility_specialist,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-accessibility-specialist + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-accessibility-specialist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-analytics-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,analytics_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-analytics-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-analytics-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-api-designer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,api_designer,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-api-designer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-api-designer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-architect + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,architect,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-architect + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-architect.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-cloud-architect + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,cloud_architect,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-cloud-architect + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-cloud-architect.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-cobol-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,cobol_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-cobol-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-cobol-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-code-reviewer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,code_review + capability_fit: maestro_specialist,code_reviewer,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-code-reviewer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-code-reviewer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-coder + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,coder,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-coder + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-coder.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-compliance-reviewer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,security_audit,code_review + capability_fit: maestro_specialist,compliance_reviewer,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-compliance-reviewer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-compliance-reviewer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-content-strategist + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,content_strategist,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-content-strategist + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-content-strategist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-copywriter + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,copywriter,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-copywriter + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-copywriter.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-data-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,data_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-data-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-data-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-database-administrator + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,database_administrator,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-database-administrator + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-database-administrator.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-db2-dba + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,db2_dba,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-db2-dba + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-db2-dba.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-debugger + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,debugger,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-debugger + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-debugger.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-design-system-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,design_system_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-design-system-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-design-system-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-devops-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,devops_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-devops-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-devops-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-hlasm-assembler-specialist + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,hlasm_assembler_specialist,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-hlasm-assembler-specialist + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-hlasm-assembler-specialist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-i18n-specialist + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,i18n_specialist,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-i18n-specialist + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-i18n-specialist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-ibm-i-specialist + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,ibm_i_specialist,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-ibm-i-specialist + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-ibm-i-specialist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-integration-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,integration_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-integration-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-integration-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-ml-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,ml_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-ml-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-ml-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-mlops-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,mlops_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-mlops-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-mlops-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-mobile-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,mobile_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-mobile-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-mobile-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-observability-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,observability_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-observability-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-observability-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-performance-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,performance_engineer,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-performance-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-performance-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-platform-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,platform_engineer,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-platform-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-platform-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-product-manager + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,product_manager,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-product-manager + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-product-manager.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-prompt-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,prompt_engineer,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-prompt-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-prompt-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-refactor + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,refactor,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-refactor + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-refactor.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-release-manager + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,release_manager,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-release-manager + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-release-manager.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-security-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,security_audit + capability_fit: maestro_specialist,security_engineer,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-security-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-security-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-seo-specialist + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,seo_specialist,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-seo-specialist + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-seo-specialist.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-site-reliability-engineer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,site_reliability_engineer,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-site-reliability-engineer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-site-reliability-engineer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-solutions-architect + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,solutions_architect,read_only + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-solutions-architect + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-solutions-architect.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-technical-writer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,technical_writer,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-technical-writer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-technical-writer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-tester + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task,testing_task + capability_fit: maestro_specialist,tester,full + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-tester + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-tester.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-ux-designer + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,ux_designer,read_write + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: worker + adapter: maestro-codex-runtime + adapter_role: maestro-ux-designer + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-ux-designer.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. + + - id: maestro-zos-sysprog + kind: subagent + status: recommended + scope: maestro-specialist-agent + invocation: via_codex_runtime_spawn_agent_with_maestro_role + task_fit: capability_orchestration,executable_control_plane,engineering_change,architecture_design,documentation_task,release_task + capability_fit: maestro_specialist,zos_sysprog,read_shell + runtime: codex + runtime_tool: multi_agent_v1.spawn_agent + runtime_agent_type: explorer + adapter: maestro-codex-runtime + adapter_role: maestro-zos-sysprog + human_gate: true + execution_mode: ask + source_path: capability-layer/subagents/maestro/maestro-zos-sysprog.yaml + notes: Maestro role adapter backed by Codex runtime spawn_agent; KnowledgeOS registers intent and evidence, Codex performs actual dispatch. diff --git a/templates/project-control-plane/.agent-os/write-policy.yaml b/templates/project-control-plane/.agent-os/write-policy.yaml index b6263fc..b2db2c1 100644 --- a/templates/project-control-plane/.agent-os/write-policy.yaml +++ b/templates/project-control-plane/.agent-os/write-policy.yaml @@ -31,6 +31,7 @@ write_policy: - .agent-os/dispatch-policy.yaml - .agent-os/tool-registry.yaml - .agent-os/workflows/router.yaml + - .agent-os/decision-policy.yaml - .agent-os/effect-policy.yaml - .agent-os/phase-policy.yaml - .agent-os/fabric-link.yaml @@ -41,9 +42,16 @@ write_policy: - .agent-os/runs/**/eval.md - .agent-os/runs/**/phases.ndjson - .agent-os/runs/**/command-events.ndjson + - .agent-os/runs/**/decision-events.ndjson - .agent-os/runs/**/capability-events.ndjson - .agent-os/runs/**/effect-assertions.ndjson - .agent-os/runs/**/postflight.md + - .agent-os/threads/current.json + - .agent-os/threads/**/thread.json + - .agent-os/threads/**/thread-plan.ndjson + - .agent-os/threads/**/thread-plan.md + - .agent-os/threads/**/thread-map.html + - .agent-os/threads/**/command-events.ndjson require_receipt_for: - "*.md" diff --git a/templates/project-control-plane/AGENTS.md b/templates/project-control-plane/AGENTS.md index 6e3a1d7..cc6c78c 100644 --- a/templates/project-control-plane/AGENTS.md +++ b/templates/project-control-plane/AGENTS.md @@ -2,11 +2,17 @@ This project uses KnowledgeOS. +At the start of every conversation: + +- Make a visible KnowledgeOS judgment before acting: project state (`managed` or `unmanaged`), work class (`simple`, `substantial`, or `blocked`), required flow (`answer-only`, `task`, `spec`, `thread-plan`, or `full lifecycle`), and reason. +- Relay this as `KOS_DECISION` in user-visible text. This is a public routing decision, not hidden reasoning. +- If the project is managed and the work is substantial, continue with the command-evidenced lifecycle below. + Before substantial work: 1. Read `.agent-os/workspace.yaml`. 2. Read `.agent-os/project.yaml`. -3. Read `.agent-os/tasks.yaml`, `.agent-os/specs.yaml`, `.agent-os/phase-policy.yaml`, `.agent-os/decisions.yaml`, and `.agent-os/evals.yaml`. +3. Read `.agent-os/tasks.yaml`, `.agent-os/specs.yaml`, `.agent-os/phase-policy.yaml`, `.agent-os/decision-policy.yaml`, `.agent-os/decisions.yaml`, and `.agent-os/evals.yaml`. 4. Run `CHANGE_ME_KNOWLEDGEOS_BIN doctor --project-root . --summary`. 5. Run the configured shared-fabric boot hook from `.agent-os/fabric-link.yaml`. 6. Report `[BOOT_OK]` only after the hook succeeds. @@ -17,6 +23,7 @@ During substantial work: - Check `.agent-os/read-policy.yaml` before using broad project context; `archive/**` is cold storage and is not default context. - Check `.agent-os/write-policy.yaml` before writing. - If the user asks to create, align, or follow a spec, use `CHANGE_ME_KNOWLEDGEOS_BIN create-spec --project-root . --title "<title>"` or `CHANGE_ME_KNOWLEDGEOS_BIN align-spec --project-root . --task-id <task-id>` before execution. +- If the user starts or continues a durable plan/spec conversation, use `CHANGE_ME_KNOWLEDGEOS_BIN thread-plan current --project-root .` or `CHANGE_ME_KNOWLEDGEOS_BIN thread-plan start --project-root . --title "<natural language goal>"`; update it with `CHANGE_ME_KNOWLEDGEOS_BIN thread-plan append --project-root . --thread-id <thread-id> --kind <kind> --text "<plain note>"` when the plan changes or advances, and relay `THREAD_PLAN_OK`. - If no ready task fits the user's new request, use `CHANGE_ME_KNOWLEDGEOS_BIN create-task --project-root . --title "<title>" --type <type> --output <path> --acceptance "<check>"` instead of reopening unrelated prior work. - Use `CHANGE_ME_KNOWLEDGEOS_BIN route-task --project-root . --task-id <task-id>` before dispatching work. - Use `CHANGE_ME_KNOWLEDGEOS_BIN check-route-write --project-root . --task-id <task-id> --path <planned-path>` before planned mutations. @@ -24,14 +31,24 @@ During substantial work: - Use `.agent-os/tool-registry.yaml` to confirm the configured MCP, skills, workflows, orchestrators, and subagents. - Use `CHANGE_ME_KNOWLEDGEOS_BIN tool-registry --project-root .` before relying on registered capabilities. - Use `.agent-os/dispatch-policy.yaml` to choose capabilities in a visible order. -- Use `CHANGE_ME_KNOWLEDGEOS_BIN dispatch-task --project-root . --task-id <task-id>` before invoking subagents, MCP tools, or skills. +- Use `CHANGE_ME_KNOWLEDGEOS_BIN dispatch-task --project-root . --task-id <task-id>` before invoking subagents, MCP tools, or skills, and relay the returned `AGENT_DISPATCH_PLAN` marker. +- If `dispatch-task` selects `codex-*` or `maestro-*`, use `CHANGE_ME_KNOWLEDGEOS_BIN subagent-adapter --project-root . --id <subagent-id>` to resolve the Codex runtime call package before actual delegation. +- Actual `codex-*` and `maestro-*` delegation is performed by the host Codex runtime subagent tool, not by the KnowledgeOS CLI. After delegation, record `CHANGE_ME_KNOWLEDGEOS_BIN capability-event --project-root . --task-id <task-id> --run-id <run-id> --kind subagent --id <subagent-id> --purpose "<purpose>"`. +- Preserve the id returned by every spawn. For substantive work, wait once for several minutes instead of repeatedly polling. After a timeout, request current findings once, wait once more, and close only after a terminal result. Record a later recovery with `recovered_from=timed_out`. +- A bounded spawned subagent works directly on the parent-assigned scope. It must not restart the full KnowledgeOS lifecycle, create/reopen tasks or specs, run completion/postflight, or recursively dispatch subagents unless the parent explicitly delegated orchestration. +- If a planned subagent is skipped, times out, blocks, or cannot be closed, record `capability-event --kind subagent --id <subagent-id> --status skipped|timed_out|blocked|close_failed --purpose "<public reason>"` before `dispatch-report`. +- For an explicit whole-catalog validation task, use `CHANGE_ME_KNOWLEDGEOS_BIN verify-subagents --project-root . --task-id <task-id> --run-id <run-id>`; do not claim `SUBAGENT_CATALOG_OK` from adapter resolution, duplicate events, or unbound capability evidence, and do not claim independent host verification. - At consultation checkpoints, pause, state your recommendation, explain the tradeoff, and ask the human whether to proceed. - Use `CHANGE_ME_KNOWLEDGEOS_BIN run-task --project-root . --task-id <task-id>` to create run evidence. - Use `CHANGE_ME_KNOWLEDGEOS_BIN context-pack --project-root . --task-id <task-id> --run-id <run-id>` and `CHANGE_ME_KNOWLEDGEOS_BIN plan-task --project-root . --task-id <task-id> --run-id <run-id> --summary "<summary>"` before execution. - After `run-task` creates a run id, use `CHANGE_ME_KNOWLEDGEOS_BIN dispatch-task --project-root . --task-id <task-id> --run-id <run-id>` to record dispatch evidence for that run. - Use `CHANGE_ME_KNOWLEDGEOS_BIN trace-step --project-root . --task-id <task-id> --run-id <run-id> --step <step> --note "<public trace>" --evidence "<command/file/user confirmation>"` to record visible operational progress, and relay the returned `TRACE_OK` marker. - Use `CHANGE_ME_KNOWLEDGEOS_BIN phase-task --project-root . --task-id <task-id> --run-id <run-id> --phase <route|plan|review|dispatch|execute|report> --status completed --note "<public trace>" --evidence "<command/file/user confirmation>"` to record observable phase evidence, and relay the returned `CHECKPOINT_OK` marker. -- Use `CHANGE_ME_KNOWLEDGEOS_BIN capability-event --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose "<purpose>"` before or after MCP, skill, subagent, orchestrator, or important script use, and relay the returned `CAPABILITY_OK` marker. +- Use `CHANGE_ME_KNOWLEDGEOS_BIN capability-event --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --id <capability-id> --purpose "<purpose>"` before or after MCP, skill, plugin/app, browser/Chrome/GitHub/security connector, subagent, orchestrator, shell, file_read, or important script use, and relay the returned `CAPABILITY_OK` marker. +- Use `CHANGE_ME_KNOWLEDGEOS_BIN dispatch-report --project-root . --task-id <task-id> --run-id <run-id>` when summarizing actual capability dispatch, and relay the returned `AGENT_DISPATCH_OK` marker. +- Treat `AGENT_DISPATCH_OK` as a full capability dispatch report: include agents invoked/skipped, MCP, skills, plugins/apps, browser/Chrome/GitHub/security connectors, scripts, shell, file reads, evidence files, and any gaps. If no subagent was used, still report `agents=0` and explain why. +- Use `CHANGE_ME_KNOWLEDGEOS_BIN decision-event --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --title "<title>" --summary "<summary>" --reason "<reason>" --evidence "<evidence>"` when a plan branches, changes, rolls back, abandons a route, or records a major human decision, and relay the returned `DECISION_OK` marker. +- Use `CHANGE_ME_KNOWLEDGEOS_BIN thread-plan link-run --project-root . --thread-id <thread-id> --task-id <task-id> --run-id <run-id>` to connect a task run to the chat-level plan. - Use `CHANGE_ME_KNOWLEDGEOS_BIN artifact-assert --project-root . --task-id <task-id> --run-id <run-id> --kind <kind> --path <artifact>` to verify real side effects, and relay the returned `EFFECT_OK` marker. - Record run evidence under `.agent-os/runs/`. @@ -41,7 +58,11 @@ After substantial work: - Run `CHANGE_ME_KNOWLEDGEOS_BIN verify-context --project-root . --task-id <task-id> --run-id <run-id>`. - Run `CHANGE_ME_KNOWLEDGEOS_BIN verify-lifecycle --project-root . --task-id <task-id> --run-id <run-id>`. - Run `CHANGE_ME_KNOWLEDGEOS_BIN verify-effects --project-root . --task-id <task-id> --run-id <run-id>` and relay the returned `EFFECT_VERIFY_OK` marker before claiming effect verification success. -- Use `CHANGE_ME_KNOWLEDGEOS_BIN complete-task --project-root . --task-id <task-id> --run-id <run-id> --summary "<summary>"` to close task state. It enforces spec/context/plan, lifecycle evidence, effect evidence, and required postflight. +- Run `CHANGE_ME_KNOWLEDGEOS_BIN verify-decisions --project-root . --task-id <task-id> --run-id <run-id>` and relay the returned `DECISION_VERIFY_OK` marker before claiming decision verification success. +- Use `CHANGE_ME_KNOWLEDGEOS_BIN complete-task --project-root . --task-id <task-id> --run-id <run-id> --summary "<summary>"` to close task state. It enforces spec/context/plan, lifecycle evidence, visible dispatch reporting, effect evidence, decision evidence according to policy, and required postflight. +- Include the `AGENT_DISPATCH_OK` marker and full capability summary returned by `dispatch-report` or `complete-task` in the final answer for substantial work. +- For medium, high, or complex tasks, include the `FLOW_OK` Mermaid Mission Flow returned by `complete-task`; if needed, run `CHANGE_ME_KNOWLEDGEOS_BIN flow-summary --project-root . --run-id <run-id>` and relay `FLOW_OK`. +- Keep Mission Flow readable for humans: use simple labels like Goal, Health Check, Task & Plan, Safe Writes, Work Done, Tools Used, Proof, Decisions, and Finish. - Report `[SYNC_OK]` only after `complete-task` returns `sync_status: SYNC_OK`. Reset and migration: @@ -57,6 +78,10 @@ Never: - write to immutable paths; - bypass human-gated paths; - invoke generic unscoped subagents; -- claim boot, phase, lifecycle, eval, completion, or sync success without command evidence. +- claim boot, phase, lifecycle, decision, eval, completion, or sync success without command evidence. +- claim agent, subagent, tool, or script dispatch success without `AGENT_DISPATCH_PLAN` and `AGENT_DISPATCH_OK` command evidence. +- skip the initial `KOS_DECISION` routing judgment for any conversation. - manually write spec snapshots, context packs, plans, phase ledgers, or eval status instead of using KnowledgeOS commands. - manually write operational trace ledgers instead of using `trace-step`. +- manually write decision event ledgers instead of using `decision-event`. +- manually write thread plan ledgers instead of using `thread-plan`. diff --git a/tests/test_knowledgeos_cli.py b/tests/test_knowledgeos_cli.py index 0a526d6..5e5042e 100644 --- a/tests/test_knowledgeos_cli.py +++ b/tests/test_knowledgeos_cli.py @@ -86,6 +86,54 @@ def write_plan_context(self, project: Path, task_id: str, run_id: str, summary: summary, ) + def seed_subagent_catalog( + self, + project: Path, + task_id: str, + run_id: str, + *, + missing: set[str] | None = None, + native_attempts: int = 3, + reuse_native_challenge: bool = False, + ) -> list[str]: + missing = missing or set() + registered = [ + entry["id"] + for entry in cli.load_tool_registry(project) + if entry.get("kind") == "subagent" + and entry.get("status") in cli.ACTIVE_TOOL_STATUSES + and cli.is_runtime_callable_subagent(entry) + ] + native_ids = {item["id"] for item in cli.CODEX_NATIVE_SUBAGENTS} + for subagent_id in registered: + if subagent_id in missing: + cli.build_subagent_adapter(project, subagent_id, task_id=task_id, run_id=run_id, purpose="Strict catalog smoke.") + continue + attempts = native_attempts if subagent_id in native_ids else 1 + shared_adapter = ( + cli.build_subagent_adapter(project, subagent_id, task_id=task_id, run_id=run_id, purpose="Strict catalog smoke.") + if reuse_native_challenge and subagent_id in native_ids + else None + ) + for attempt in range(1, attempts + 1): + adapter = shared_adapter or cli.build_subagent_adapter( + project, subagent_id, task_id=task_id, run_id=run_id, purpose="Strict catalog smoke." + ) + nonce = f"TEST-{subagent_id}-{attempt}" + cli.record_capability_event( + project, + task_id, + run_id, + kind="subagent", + capability_id=subagent_id, + purpose="Strict catalog smoke.", + evidence=( + f"SUBAGENT_SMOKE_OK id={subagent_id} nonce={nonce}; cleanup=completed; " + f"role_contract=passed; adapter_challenge={adapter['runtime_challenge']}" + ), + ) + return sorted(registered) + def test_doctor_public_root_passes(self): result = self.run_cli("doctor", "--root", str(ROOT), "--json") payload = json.loads(result.stdout) @@ -163,7 +211,7 @@ def test_harness_audit_repairs_legacy_mount_and_missing_control_files(self): tmp_root = Path(tmp) project = tmp_root / "LegacyProject" project.mkdir() - old_governance = tmp_root / "Antigravity_Skills" / "global-agent-fabric" + old_governance = tmp_root / "Legacy_PreOS_Root" / "global-agent-fabric" desired_governance = tmp_root / "KnowledgeOS" / "global-agent-fabric" desired_capability = tmp_root / "KnowledgeOS" / "capability-layer" self.run_cli( @@ -193,6 +241,11 @@ def test_harness_audit_repairs_legacy_mount_and_missing_control_files(self): "\n".join(line for line in write_policy.read_text(encoding="utf-8").splitlines() if "archive/**" not in line) + "\n", encoding="utf-8", ) + registry_path = project / ".agent-os" / "tool-registry.yaml" + registry_text = registry_path.read_text(encoding="utf-8") + for subagent_id in ["codex-default", "codex-explorer", "codex-worker"]: + registry_text = re.sub(rf"\n - id: {subagent_id}\n(?: .*\n)*", "\n", registry_text) + registry_path.write_text(registry_text, encoding="utf-8") dry_run = self.run_cli( "harness-audit", @@ -212,11 +265,12 @@ def test_harness_audit_repairs_legacy_mount_and_missing_control_files(self): issues = dry_payload["projects"][0]["issues"] desired_governance = desired_governance.resolve() desired_capability = desired_capability.resolve() - self.assertIn("legacy_antigravity_governance_root", issues) + self.assertIn("legacy_preos_governance_root", issues) self.assertIn("postflight_hook_missing_or_not_executable", issues) self.assertIn("missing_control_file:.agent-os/specs.yaml", issues) self.assertIn("workflow_router_lifecycle_drift", issues) self.assertIn("missing_archive_write_guard", issues) + self.assertIn("missing_runtime_native_subagents", issues) self.assertFalse((desired_governance / "hooks" / "after-task.sh").exists()) self.assertFalse((project / ".agent-os" / "specs.yaml").exists()) @@ -250,6 +304,10 @@ def test_harness_audit_repairs_legacy_mount_and_missing_control_files(self): self.assertIn("verify-lifecycle --project-root .", upgraded_router) self.assertIn("verify-effects --project-root .", upgraded_router) self.assertIn("archive/**", write_policy.read_text(encoding="utf-8")) + registry_text = (project / ".agent-os" / "tool-registry.yaml").read_text(encoding="utf-8") + self.assertIn("id: codex-default", registry_text) + self.assertIn("id: codex-explorer", registry_text) + self.assertIn("id: codex-worker", registry_text) doctor = self.run_cli("doctor", "--root", str(ROOT), "--project-root", str(project), "--summary") self.assertIn("status: ok", doctor.stdout) @@ -528,12 +586,345 @@ def test_runtime_adapters_reports_readiness_without_execution(self): self.assertEqual(adapters["mock"]["execution"], "not_started") self.assertIn("gemini-cli", adapters) self.assertIn("codex-cli", adapters) + runtime_subagents = {item["id"]: item for item in payload["runtime_subagents"]} + self.assertIn("codex-default", runtime_subagents) + self.assertIn("codex-explorer", runtime_subagents) + self.assertIn("codex-worker", runtime_subagents) + self.assertEqual(runtime_subagents["maestro-architect"]["runtime_tool"], "multi_agent_v1.spawn_agent") + self.assertEqual(runtime_subagents["maestro-architect"]["runtime_agent_type"], "explorer") self.assertNotIn(str(ROOT), result.stdout) self.assertNotRegex(result.stdout, r"/usr/|/opt/|/Users/") plain = self.run_cli("runtime-adapters", "--project-root", str(ROOT)) self.assertIn("default_runtime: mock", plain.stdout) self.assertIn("real_cli_execution: disabled_until_adapter_phase", plain.stdout) + self.assertIn("runtime_subagents:", plain.stdout) + + def test_subagent_adapter_resolves_maestro_role_prompt(self): + result = self.run_cli("subagent-adapter", "--project-root", str(ROOT), "--id", "maestro-architect", "--json") + payload = json.loads(result.stdout) + self.assertEqual(payload["subagent_adapter_marker"], "SUBAGENT_ADAPTER_OK") + self.assertEqual(payload["runtime_tool"], "multi_agent_v1.spawn_agent") + self.assertEqual(payload["runtime_agent_type"], "explorer") + self.assertIn("Maestro Architect", payload["role_prompt"]) + self.assertIn("bounded delegated subagent", payload["role_prompt"]) + self.assertIn("Do not create or reopen KnowledgeOS tasks/specs", payload["role_prompt"]) + self.assertEqual(payload["runtime_challenge"], "") + self.assertEqual(payload["evidence_model"], "parent_attested_runtime") + self.assertFalse(payload["host_runtime_verified"]) + self.assertIn("--kind subagent --id maestro-architect", payload["capability_event_suggestion"]) + self.assertIn("SUBAGENT_ADAPTER_OK", payload["marker"]) + + def test_verify_subagents_accepts_unique_catalog_and_native_stability(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + registered = self.seed_subagent_catalog(project, "T001", run_id) + + result = self.run_cli( + "verify-subagents", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--json", + ) + payload = json.loads(result.stdout) + self.assertEqual(payload["status"], "passed") + self.assertEqual(payload["subagent_catalog_marker"], "SUBAGENT_CATALOG_OK") + self.assertEqual(payload["registered_role_ids"], registered) + self.assertEqual(payload["covered_role_count"], len(registered)) + self.assertEqual(payload["missing_role_ids"], []) + self.assertFalse(payload["catalog_drift"]) + self.assertEqual(payload["evidence_model"], "parent_attested_runtime") + self.assertFalse(payload["host_runtime_verified"]) + self.assertTrue(all(payload["success_counts"][subagent_id] == 3 for subagent_id in payload["native_role_ids"])) + + plain = self.run_cli( + "verify-subagents", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + ) + self.assertIn("SUBAGENT_CATALOG_OK", plain.stdout) + + def test_verify_subagents_rejects_duplicates_missing_roles_and_native_shortfalls(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + registered = [ + entry["id"] + for entry in cli.load_tool_registry(project) + if entry.get("kind") == "subagent" + and entry.get("status") in cli.ACTIVE_TOOL_STATUSES + and cli.is_runtime_callable_subagent(entry) + ] + missing_id = sorted(registered)[-1] + self.seed_subagent_catalog(project, "T001", run_id, missing={missing_id}, native_attempts=1) + duplicate_id = sorted(set(registered) - {missing_id})[0] + for attempt in range(2, 6): + cli.record_capability_event( + project, + "T001", + run_id, + kind="subagent", + capability_id=duplicate_id, + purpose="Duplicate catalog smoke.", + evidence=f"SUBAGENT_SMOKE_OK id={duplicate_id} nonce=DUP-{attempt}; cleanup=completed; role_contract=passed", + ) + + result = self.run_cli( + "verify-subagents", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--json", + check=False, + ) + self.assertEqual(result.returncode, 2) + payload = json.loads(result.stdout) + self.assertEqual(payload["status"], "failed") + self.assertEqual(payload["subagent_catalog_marker"], "") + self.assertIn(missing_id, payload["missing_role_ids"]) + self.assertTrue(payload["native_shortfalls"]) + self.assertNotIn("SUBAGENT_CATALOG_OK", payload["marker"]) + + def test_verify_subagents_rejects_invalid_strict_evidence(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + self.seed_subagent_catalog(project, "T001", run_id) + cli.record_capability_event( + project, + "T001", + run_id, + kind="subagent", + capability_id="unknown-runtime-agent", + purpose="Invalid strict catalog evidence.", + evidence="SUBAGENT_SMOKE_OK id=unknown-runtime-agent nonce=UNKNOWN-1; cleanup=completed; role_contract=passed", + ) + + result = self.run_cli( + "verify-subagents", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--json", + check=False, + ) + self.assertEqual(result.returncode, 2) + payload = json.loads(result.stdout) + self.assertEqual(payload["invalid_events"][0]["id"], "unknown-runtime-agent") + self.assertEqual(payload["invalid_events"][0]["reason"], "unknown_runtime_subagent") + + def test_verify_subagents_rejects_malformed_marker_and_unbound_attestation(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + self.seed_subagent_catalog(project, "T001", run_id) + cli.record_capability_event( + project, + "T001", + run_id, + kind="subagent", + capability_id="codex-explorer", + purpose="Malformed strict evidence.", + evidence="SUBAGENT_SMOKE_OK id=codex-explorer nonce=MALFORMED", + ) + + result = self.run_cli( + "verify-subagents", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json", check=False + ) + self.assertEqual(result.returncode, 2) + payload = json.loads(result.stdout) + self.assertIn("missing_cleanup", {item["reason"] for item in payload["invalid_events"]}) + + def test_verify_subagents_locks_catalog_snapshot_and_native_floor(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + registered = self.seed_subagent_catalog(project, "T001", run_id) + + lowered = self.run_cli( + "verify-subagents", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, + "--native-min-successes", "1", "--json", check=False, + ) + self.assertEqual(lowered.returncode, 1) + self.assertIn("cannot be lower", lowered.stderr) + + registry = project / ".agent-os" / "tool-registry.yaml" + registry_text = registry.read_text(encoding="utf-8") + entry_pattern = rf"(?ms)(^ - id: {re.escape(registered[-1])}\n.*?^ status: )\w+" + registry_text, replacements = re.subn(entry_pattern, r"\1disabled", registry_text, count=1) + self.assertEqual(replacements, 1) + registry.write_text(registry_text, encoding="utf-8") + drifted = self.run_cli( + "verify-subagents", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json", check=False + ) + self.assertEqual(drifted.returncode, 2) + self.assertTrue(json.loads(drifted.stdout)["catalog_drift"]) + + def test_verify_subagents_requires_distinct_adapter_challenge_per_native_success(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + self.seed_subagent_catalog(project, "T001", run_id, reuse_native_challenge=True) + + result = self.run_cli( + "verify-subagents", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json", check=False + ) + self.assertEqual(result.returncode, 2) + payload = json.loads(result.stdout) + self.assertEqual(set(payload["native_shortfalls"].values()), {1}) + self.assertGreaterEqual(payload["ignored_reused_challenge_count"], 6) + + def test_verify_subagents_rejects_pre_snapshot_native_disable_and_incomplete_snapshot(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + registry = project / ".agent-os" / "tool-registry.yaml" + text = registry.read_text(encoding="utf-8") + text, replacements = re.subn( + r"(?ms)(^ - id: codex-explorer\n.*?^ status: )\w+", r"\1disabled", text, count=1 + ) + self.assertEqual(replacements, 1) + registry.write_text(text, encoding="utf-8") + cli.build_subagent_adapter(project, "codex-default", task_id="T001", run_id=run_id) + + weakened = cli.verify_subagent_catalog(project, "T001", run_id) + self.assertEqual(weakened["status"], "failed") + self.assertTrue(weakened["catalog_drift"]) + + snapshot_path = project / ".agent-os" / "runs" / run_id / "subagent-catalog.json" + snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) + snapshot.pop("catalog_sha256") + snapshot_path.write_text(json.dumps(snapshot), encoding="utf-8") + incomplete = cli.verify_subagent_catalog(project, "T001", run_id) + self.assertEqual(incomplete["catalog_snapshot_error"], "catalog_snapshot_missing_hash") + + + def test_verify_subagents_rejects_tampered_snapshot_metadata(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + self.seed_subagent_catalog(project, "T001", run_id) + snapshot_path = project / ".agent-os" / "runs" / run_id / "subagent-catalog.json" + snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) + snapshot.pop("evidence_model") + snapshot["captured_at"] = "0" + snapshot_path.write_text(json.dumps(snapshot), encoding="utf-8") + malformed = cli.verify_subagent_catalog(project, "T001", run_id) + self.assertNotEqual(malformed["catalog_snapshot_error"], "") + + def test_verify_subagents_rejects_duplicate_challenge_tokens(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + self.seed_subagent_catalog(project, "T001", run_id) + adapter = cli.build_subagent_adapter(project, "codex-explorer", task_id="T001", run_id=run_id) + cli.record_capability_event( + project, "T001", run_id, kind="subagent", capability_id="codex-explorer", purpose="Ambiguous challenge.", + evidence=( + "SUBAGENT_SMOKE_OK id=codex-explorer nonce=AMBIGUOUS; cleanup=completed; role_contract=passed; " + f"adapter_challenge={adapter['runtime_challenge']}; adapter_challenge=other" + ), + ) + payload = cli.verify_subagent_catalog(project, "T001", run_id) + self.assertIn("duplicate_or_malformed_adapter_challenge", {item["reason"] for item in payload["invalid_events"]}) + + def test_verify_subagents_rejects_empty_catalog_and_missing_adapter_evidence(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + + with patch.object(cli, "load_tool_registry", return_value=[]): + empty = cli.verify_subagent_catalog(project, "T001", run_id) + self.assertEqual(empty["status"], "failed") + self.assertTrue(empty["catalog_empty"]) + + registered = [ + entry["id"] + for entry in cli.load_tool_registry(project) + if entry.get("kind") == "subagent" + and entry.get("status") in cli.ACTIVE_TOOL_STATUSES + and cli.is_runtime_callable_subagent(entry) + ] + cli.ensure_subagent_catalog_snapshot( + project / ".agent-os" / "runs" / run_id, + "T001", + run_id, + cli.active_runtime_subagent_catalog(project), + ) + native_ids = {item["id"] for item in cli.CODEX_NATIVE_SUBAGENTS} + for subagent_id in registered: + attempts = 3 if subagent_id in native_ids else 1 + for attempt in range(1, attempts + 1): + cli.record_capability_event( + project, + "T001", + run_id, + kind="subagent", + capability_id=subagent_id, + purpose="Strict smoke without adapter evidence.", + evidence=f"SUBAGENT_SMOKE_OK id={subagent_id} nonce=NO-ADAPTER-{attempt}; cleanup=completed; role_contract=passed", + ) + missing_adapter = cli.verify_subagent_catalog(project, "T001", run_id) + self.assertEqual(missing_adapter["status"], "failed") + self.assertEqual(missing_adapter["missing_adapter_role_ids"], sorted(registered)) + + def test_init_project_includes_runtime_native_and_maestro_adapters(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + result = self.run_cli("tool-registry", "--project-root", str(project), "--json") + payload = json.loads(result.stdout) + entries = {item["id"]: item for item in payload["entries"]} + for subagent_id in ["codex-default", "codex-explorer", "codex-worker", "maestro-architect"]: + self.assertIn(subagent_id, entries) + self.assertEqual(entries[subagent_id]["runtime_tool"], "multi_agent_v1.spawn_agent") + self.assertEqual(entries[subagent_id]["execution_mode"], "ask") def test_init_project_preserves_existing_agents_md(self): with tempfile.TemporaryDirectory() as tmp: @@ -817,6 +1208,138 @@ def test_check_route_write_enforces_route_allowed_outputs(self): guarded_payload = json.loads(guarded.stdout) self.assertEqual(guarded_payload["decision"], "human_gate_required") + def test_check_route_write_denies_external_paths_by_default(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + + external_target = str(Path.home() / ".local" / "bin" / "agy") + blocked = self.run_cli( + "check-route-write", + "--project-root", + str(project), + "--task-id", + "T001", + "--path", + external_target, + "--json", + check=False, + ) + self.assertEqual(blocked.returncode, 2) + payload = json.loads(blocked.stdout) + self.assertEqual(payload["decision"], "deny") + self.assertFalse(payload["inside_project"]) + + def test_check_route_write_allows_external_paths_only_with_local_overlay_and_route_flag(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + + tasks_path = project / ".agent-os" / "tasks.yaml" + tasks_path.write_text( + tasks_path.read_text(encoding="utf-8").replace("type: initialization", "type: migration_task", 1), + encoding="utf-8", + ) + + router_path = project / ".agent-os" / "workflows" / "router.yaml" + router_path.write_text( + router_path.read_text(encoding="utf-8") + + "\n" + + " migration_task:\n" + + " route_order:\n" + + " - doctor --project-root .\n" + + " - route-task --project-root . --task-id <task-id>\n" + + " - check-route-write --project-root . --task-id <task-id> --path <planned-path>\n" + + " eval_profile: migration_task\n" + + " human_gate: explicit_approval\n" + + " allow_external_controlled: true\n" + + " allowed_outputs:\n" + + " - .knowledgeos-local/\n", + encoding="utf-8", + ) + + local_policy = project / ".knowledgeos-local" / "write-policy.local.yaml" + local_policy.parent.mkdir(parents=True, exist_ok=True) + external_target = str(Path.home() / ".local" / "bin" / "agy") + local_policy.write_text( + "external_controlled:\n" + f" - {external_target}\n" + "external_require_receipt_for:\n" + f" - {external_target}\n", + encoding="utf-8", + ) + + allowed = self.run_cli( + "check-route-write", + "--project-root", + str(project), + "--task-id", + "T001", + "--path", + external_target, + "--json", + ) + payload = json.loads(allowed.stdout) + self.assertEqual(payload["decision"], "allow") + self.assertEqual(payload["route_status"], "allowed_by_external_route") + self.assertFalse(payload["inside_project"]) + self.assertTrue(payload["receipt_required"]) + + def test_check_route_write_blocks_external_paths_when_route_lacks_external_flag(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + + tasks_path = project / ".agent-os" / "tasks.yaml" + tasks_path.write_text( + tasks_path.read_text(encoding="utf-8").replace("type: initialization", "type: migration_task", 1), + encoding="utf-8", + ) + + router_path = project / ".agent-os" / "workflows" / "router.yaml" + router_path.write_text( + router_path.read_text(encoding="utf-8") + + "\n" + + " migration_task:\n" + + " route_order:\n" + + " - doctor --project-root .\n" + + " - route-task --project-root . --task-id <task-id>\n" + + " - check-route-write --project-root . --task-id <task-id> --path <planned-path>\n" + + " eval_profile: migration_task\n" + + " human_gate: explicit_approval\n" + + " allowed_outputs:\n" + + " - .knowledgeos-local/\n", + encoding="utf-8", + ) + + local_policy = project / ".knowledgeos-local" / "write-policy.local.yaml" + local_policy.parent.mkdir(parents=True, exist_ok=True) + external_target = str(Path.home() / ".local" / "bin" / "agy") + local_policy.write_text( + "external_controlled:\n" + f" - {external_target}\n", + encoding="utf-8", + ) + + blocked = self.run_cli( + "check-route-write", + "--project-root", + str(project), + "--task-id", + "T001", + "--path", + external_target, + "--json", + check=False, + ) + self.assertEqual(blocked.returncode, 2) + payload = json.loads(blocked.stdout) + self.assertEqual(payload["decision"], "route_output_denied") + self.assertIn("does not allow external controlled writes", payload["reason"]) + def test_archive_management_route_allows_archive_root_check(self): result = self.run_cli( "check-route-write", @@ -1209,9 +1732,14 @@ def test_complete_task_requires_passed_eval_and_updates_status(self): payload = json.loads(completed.stdout) self.assertEqual(payload["status"], "completed") self.assertEqual(payload["sync_status"], "PENDING") + self.assertEqual(payload["flow_marker"], "FLOW_OK") + self.assertIn("Goal:", payload["flow_mermaid"]) + self.assertIn("Health check:", payload["flow_mermaid"]) + self.assertTrue((run_dir / "mission-flow.md").exists()) self.assertIn("status: completed", (run_dir / "run.yaml").read_text(encoding="utf-8")) self.assertIn("status: completed", (project / ".agent-os" / "tasks.yaml").read_text(encoding="utf-8")) self.assertIn("Guarded task complete", (project / ".agent-os" / "receipts" / "latest.md").read_text(encoding="utf-8")) + self.assertIn("Mission Flow Marker: FLOW_OK", (run_dir / "receipt.md").read_text(encoding="utf-8")) def test_complete_task_requires_lifecycle_phases(self): with tempfile.TemporaryDirectory() as tmp: @@ -1689,19 +2217,16 @@ def test_trace_step_records_public_operational_trace_marker(self): self.assertEqual(payload["trace_marker"], "TRACE_OK") self.assertIn("TRACE_OK step=route_guard", payload["marker"]) - def test_artifact_assert_records_effect_marker_for_real_file_checks(self): + def test_decision_event_records_queryable_public_decision_tree_marker(self): with tempfile.TemporaryDirectory() as tmp: project = Path(tmp) / "ExampleProject" project.mkdir() self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") run_id = json.loads(started.stdout)["run_id"] - report = project / "docs" / "report.md" - report.parent.mkdir() - report.write_text("# Report\n\nverified side effect\n", encoding="utf-8") - result = self.run_cli( - "artifact-assert", + root = self.run_cli( + "decision-event", "--project-root", str(project), "--task-id", @@ -1709,20 +2234,426 @@ def test_artifact_assert_records_effect_marker_for_real_file_checks(self): "--run-id", run_id, "--kind", - "file_contains", - "--path", - "docs/report.md", - "--expect", - "verified side effect", + "branch_opened", + "--status", + "planned", + "--title", + "Compare analysis paths", + "--summary", + "Open alternative analysis routes before execution.", + "--reason", + "Research tasks can branch before a stable plan is chosen.", + "--option", + "full rerun", + "--option", + "targeted rerun", + "--evidence", + "user request", + "--json", ) - self.assertIn("EFFECT_OK kind=file_contains target=docs/report.md", result.stdout) - run_dir = project / ".agent-os" / "runs" / run_id - self.assertIn('"event_type": "artifact-assert"', (run_dir / "command-events.ndjson").read_text(encoding="utf-8")) - self.assertIn('"kind": "file_contains"', (run_dir / "effect-assertions.ndjson").read_text(encoding="utf-8")) + root_payload = json.loads(root.stdout) + self.assertEqual(root_payload["decision_marker"], "DECISION_OK") + self.assertTrue(root_payload["decision_id"].startswith("DEC-")) - def test_artifact_assert_does_not_record_failed_assertion(self): - with tempfile.TemporaryDirectory() as tmp: - project = Path(tmp) / "ExampleProject" + child = self.run_cli( + "decision-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--parent-id", + root_payload["decision_id"], + "--kind", + "branch_selected", + "--status", + "selected", + "--title", + "Use targeted rerun", + "--summary", + "Select the smaller rerun path for faster validation.", + "--reason", + "The targeted path proves the changed artifact without repeating expensive work.", + "--chosen", + "targeted rerun", + "--evidence", + "plan review", + "--json", + ) + child_payload = json.loads(child.stdout) + self.assertEqual(child_payload["record"]["parent_id"], root_payload["decision_id"]) + + queried = self.run_cli( + "decision-query", + "--project-root", + str(project), + "--run-id", + run_id, + "--parent-id", + root_payload["decision_id"], + "--json", + ) + query_payload = json.loads(queried.stdout) + self.assertEqual(query_payload["count"], 1) + self.assertEqual(query_payload["events"][0]["decision_id"], child_payload["decision_id"]) + + run_dir = project / ".agent-os" / "runs" / run_id + self.assertIn('"event_type": "decision-event"', (run_dir / "command-events.ndjson").read_text(encoding="utf-8")) + self.assertIn('"kind": "branch_selected"', (run_dir / "decision-events.ndjson").read_text(encoding="utf-8")) + + def test_thread_plan_ledger_is_chat_level_append_only_and_readable(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + + started = self.run_cli( + "thread-plan", + "start", + "--project-root", + str(project), + "--title", + "长期维护鸟类声景基金申请计划", + "--spec-id", + "SPEC-TEST", + "--json", + ) + start_payload = json.loads(started.stdout) + self.assertEqual(start_payload["thread_plan_marker"], "THREAD_PLAN_OK") + thread_id = start_payload["thread_id"] + thread_dir = project / ".agent-os" / "threads" / thread_id + ledger = thread_dir / "thread-plan.ndjson" + current = json.loads((project / ".agent-os" / "threads" / "current.json").read_text(encoding="utf-8")) + self.assertEqual(current["thread_id"], thread_id) + first_lines = ledger.read_text(encoding="utf-8").splitlines() + self.assertEqual(len(first_lines), 1) + self.assertIn("总体计划", first_lines[0]) + + self.run_cli( + "thread-plan", + "append", + "--project-root", + str(project), + "--thread-id", + thread_id, + "--kind", + "branch", + "--text", + "Plan A:先稳定 OS 计划记录,再做可视化;Plan B:直接做复杂 dashboard,暂缓。", + ) + self.run_cli( + "thread-plan", + "append", + "--project-root", + str(project), + "--thread-id", + thread_id, + "--kind", + "phase", + "--text", + "Phase A:把聊天级计划记录清楚;Phase B:再把多个任务串起来。", + ) + after_lines = ledger.read_text(encoding="utf-8").splitlines() + self.assertEqual(after_lines[0], first_lines[0]) + self.assertEqual(len(after_lines), 3) + + run = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(run.stdout)["run_id"] + linked = self.run_cli( + "thread-plan", + "link-run", + "--project-root", + str(project), + "--thread-id", + thread_id, + "--task-id", + "T001", + "--run-id", + run_id, + "--json", + ) + self.assertEqual(json.loads(linked.stdout)["status"], "linked") + + current_result = self.run_cli("thread-plan", "current", "--project-root", str(project), "--json") + self.assertEqual(json.loads(current_result.stdout)["current"]["thread_id"], thread_id) + + markdown = self.run_cli( + "thread-plan", + "render", + "--project-root", + str(project), + "--thread-id", + thread_id, + "--format", + "markdown", + "--json", + ) + md_payload = json.loads(markdown.stdout) + md_text = Path(md_payload["output"]).read_text(encoding="utf-8") + self.assertIn("Plan A / Plan B", md_text) + self.assertIn("Phase A / Phase B", md_text) + self.assertIn("当前工作线", md_text) + self.assertIn(run_id, md_text) + + mermaid = self.run_cli( + "thread-plan", + "render", + "--project-root", + str(project), + "--thread-id", + thread_id, + "--format", + "mermaid", + ) + self.assertIn("THREAD_PLAN_OK action=render", mermaid.stdout) + self.assertIn("flowchart LR", mermaid.stdout) + self.assertIn("Plan A", mermaid.stdout) + + html = self.run_cli( + "thread-plan", + "render", + "--project-root", + str(project), + "--thread-id", + thread_id, + "--format", + "html", + "--json", + ) + html_payload = json.loads(html.stdout) + html_text = Path(html_payload["output"]).read_text(encoding="utf-8") + manifest = json.loads(Path(html_payload["manifest"]).read_text(encoding="utf-8")) + source_sha = hashlib.sha256(ledger.read_bytes()).hexdigest() + self.assertEqual(manifest["kind"], "thread-plan") + self.assertEqual(manifest["source_sha256"], source_sha) + self.assertIn(source_sha, html_text) + self.assertIn("HTML is presentation, not source of truth.", html_text) + self.assertEqual(html_text.lower().count("<h1"), 2) + self.assertNotIn("<script", html_text.lower()) + self.assertNotIn("https://", html_text) + + old_lines = ledger.read_text(encoding="utf-8").splitlines() + second = self.run_cli( + "thread-plan", + "start", + "--project-root", + str(project), + "--title", + "另一个聊天计划", + "--json", + ) + second_thread_id = json.loads(second.stdout)["thread_id"] + self.assertNotEqual(second_thread_id, thread_id) + self.assertEqual(ledger.read_text(encoding="utf-8").splitlines(), old_lines) + second_current = json.loads((project / ".agent-os" / "threads" / "current.json").read_text(encoding="utf-8")) + self.assertEqual(second_current["thread_id"], second_thread_id) + + def test_verify_decisions_detects_orphans_and_requires_explanations(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + + invalid = self.run_cli( + "decision-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "branch_abandoned", + "--status", + "abandoned", + "--title", + "Abandon unexplained path", + "--summary", + "This should fail because reason is required.", + "--evidence", + "test", + check=False, + ) + self.assertEqual(invalid.returncode, 1) + self.assertIn("--reason is required", invalid.stderr) + + valid = self.run_cli( + "decision-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "final_decision", + "--status", + "executed", + "--title", + "Finish linear path", + "--summary", + "Record a simple final decision.", + "--reason", + "No branch was needed.", + "--evidence", + "test", + "--json", + ) + self.assertIn("DECISION_OK", json.loads(valid.stdout)["marker"]) + passed = self.run_cli("verify-decisions", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json") + self.assertEqual(json.loads(passed.stdout)["status"], "passed") + + run_dir = project / ".agent-os" / "runs" / run_id + forged = { + "decision_id": "DEC-ORPHAN", + "parent_id": "DEC-MISSING", + "task_id": "T001", + "run_id": run_id, + "kind": "branch_selected", + "status": "selected", + "title": "Forged orphan", + "summary": "This node has no parent.", + "reason": "test", + "options": [], + "chosen": "", + "evidence": "manual write", + "timestamp": "2026-05-29T00:00:00+00:00", + } + with (run_dir / "decision-events.ndjson").open("a", encoding="utf-8") as handle: + handle.write(json.dumps(forged, ensure_ascii=False, sort_keys=True) + "\n") + + failed = self.run_cli("verify-decisions", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json", check=False) + self.assertEqual(failed.returncode, 2) + self.assertIn("decision_orphan_parent", failed.stdout) + + def test_complete_task_enforces_decision_verification_when_policy_enforces(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + created = self.run_cli( + "create-task", + "--project-root", + str(project), + "--title", + "Decision gated completion", + "--type", + "report_task", + "--output", + "docs/report.md", + "--acceptance", + "report exists", + "--json", + ) + task_id = json.loads(created.stdout)["task_id"] + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", task_id, "--json") + run_id = json.loads(started.stdout)["run_id"] + self.log_required_phases(project, task_id, run_id) + self.write_plan_context(project, task_id, run_id) + report = project / "docs" / "report.md" + report.parent.mkdir() + report.write_text("report\n", encoding="utf-8") + self.run_cli("eval-task", "--project-root", str(project), "--task-id", task_id, "--run-id", run_id) + self.run_cli("artifact-assert", "--project-root", str(project), "--task-id", task_id, "--run-id", run_id, "--kind", "file_exists", "--path", "docs/report.md") + (project / ".agent-os" / "decision-policy.yaml").write_text( + "decision_policy:\n strictness: enforce\n", + encoding="utf-8", + ) + + blocked = self.run_cli( + "complete-task", + "--project-root", + str(project), + "--task-id", + task_id, + "--run-id", + run_id, + "--summary", + "Should fail before decision proof.", + "--allow-pending-postflight", + "temporary project", + check=False, + ) + self.assertEqual(blocked.returncode, 1) + self.assertIn("decision verification failed", blocked.stderr) + + self.run_cli( + "decision-event", + "--project-root", + str(project), + "--task-id", + task_id, + "--run-id", + run_id, + "--kind", + "final_decision", + "--status", + "executed", + "--title", + "Complete simple report", + "--summary", + "No branch was needed for this deterministic report task.", + "--reason", + "The task had a single declared output.", + "--evidence", + "docs/report.md", + ) + completed = self.run_cli( + "complete-task", + "--project-root", + str(project), + "--task-id", + task_id, + "--run-id", + run_id, + "--summary", + "Completed after decision proof.", + "--allow-pending-postflight", + "temporary project", + "--json", + ) + payload = json.loads(completed.stdout) + self.assertEqual(payload["decision_status"], "passed") + receipt = (project / ".agent-os" / "receipts" / "latest.md").read_text(encoding="utf-8") + self.assertIn("Decision Verification Status: passed", receipt) + + def test_artifact_assert_records_effect_marker_for_real_file_checks(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + report = project / "docs" / "report.md" + report.parent.mkdir() + report.write_text("# Report\n\nverified side effect\n", encoding="utf-8") + + result = self.run_cli( + "artifact-assert", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "file_contains", + "--path", + "docs/report.md", + "--expect", + "verified side effect", + ) + self.assertIn("EFFECT_OK kind=file_contains target=docs/report.md", result.stdout) + run_dir = project / ".agent-os" / "runs" / run_id + self.assertIn('"event_type": "artifact-assert"', (run_dir / "command-events.ndjson").read_text(encoding="utf-8")) + self.assertIn('"kind": "file_contains"', (run_dir / "effect-assertions.ndjson").read_text(encoding="utf-8")) + + def test_artifact_assert_does_not_record_failed_assertion(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" project.mkdir() self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") @@ -2474,42 +3405,183 @@ def test_render_html_generates_receipt_and_handoff_sidecars_with_source_hash(sel self.assertTrue(Path(handoff_payload["manifest"]).exists()) self.assertIn("status: ready", (project / ".agent-os" / "tasks.yaml").read_text(encoding="utf-8")) - def test_render_html_rich_report_is_self_contained_composable_and_stale_detectable(self): + def test_render_html_decision_map_sidecar_from_decision_ledger(self): with tempfile.TemporaryDirectory() as tmp: project = Path(tmp) / "ExampleProject" project.mkdir() - self.run_cli( - "init-project", - "--root", - str(ROOT), - "--project-root", - str(project), - "--name", - "Example", - "--global-root", - str(ROOT / "global-agent-fabric"), - "--capability-root", - str(ROOT / "capability-layer"), - ) - drafts = project / "reports" / "drafts" - drafts.mkdir(parents=True) - report_a = drafts / "alpha.md" - report_b = drafts / "beta.md" - report_a.write_text( - "# Alpha Report\n\n## Findings\n\n- Evidence lane one\n\n```text\nTRACE_OK\n```\n\n<script>alert('x')</script>\n", - encoding="utf-8", - ) - report_b.write_text("# Beta Report\n\n## Findings\n\nHuman handoff content.\n", encoding="utf-8") - - rendered_a = self.run_cli( - "render-html", + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + root = self.run_cli( + "decision-event", "--project-root", str(project), - "--input", - "reports/drafts/alpha.md", + "--task-id", + "T001", + "--run-id", + run_id, "--kind", - "rich-report", - "--output", + "branch_opened", + "--status", + "planned", + "--title", + "Open model strategy", + "--summary", + "Compare model paths.", + "--reason", + "Research modeling can require alternatives.", + "--option", + "fast model", + "--option", + "full model", + "--evidence", + "plan", + "--json", + ) + root_id = json.loads(root.stdout)["decision_id"] + self.run_cli( + "decision-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--parent-id", + root_id, + "--kind", + "branch_abandoned", + "--status", + "abandoned", + "--title", + "Skip full model", + "--summary", + "Full model is deferred for this run.", + "--reason", + "The quick validation branch is enough for the current task.", + "--evidence", + "runtime budget", + ) + + rendered = self.run_cli( + "render-html", + "--project-root", + str(project), + "--run-id", + run_id, + "--kind", + "decision-map", + "--json", + ) + payload = json.loads(rendered.stdout) + html = Path(payload["output"]).read_text(encoding="utf-8") + manifest = json.loads(Path(payload["manifest"]).read_text(encoding="utf-8")) + source_sha = hashlib.sha256((project / ".agent-os" / "runs" / run_id / "decision-events.ndjson").read_bytes()).hexdigest() + self.assertIn(source_sha, html) + self.assertIn("Open model strategy", html) + self.assertIn("Skip full model", html) + self.assertIn("HTML is presentation, not source of truth.", html) + self.assertEqual(manifest["kind"], "decision-map") + self.assertEqual(manifest["source_sha256"], source_sha) + + def test_flow_summary_and_mission_flow_html_are_readable_sidecars(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + self.write_plan_context(project, "T001", run_id) + self.log_required_phases(project, "T001", run_id) + self.run_cli( + "capability-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "shell", + "--id", + "unit-test", + "--purpose", + "Validate mission flow readability.", + ) + + plain = self.run_cli("flow-summary", "--project-root", str(project), "--run-id", run_id) + self.assertIn("FLOW_OK run=", plain.stdout) + self.assertIn("```mermaid", plain.stdout) + self.assertIn("Goal:", plain.stdout) + self.assertIn("Health check:", plain.stdout) + self.assertIn("Safe writes:", plain.stdout) + self.assertNotIn("lifecycle_status", plain.stdout) + + as_json = self.run_cli("flow-summary", "--project-root", str(project), "--run-id", run_id, "--json") + payload = json.loads(as_json.stdout) + self.assertEqual(payload["flow_marker"], "FLOW_OK") + self.assertIn("Task and plan:", payload["mermaid"]) + source = project / ".agent-os" / "runs" / run_id / "mission-flow.md" + self.assertTrue(source.exists()) + + rendered = self.run_cli( + "render-html", + "--project-root", + str(project), + "--run-id", + run_id, + "--kind", + "mission-flow", + "--json", + ) + html_payload = json.loads(rendered.stdout) + html = Path(html_payload["output"]).read_text(encoding="utf-8") + manifest = json.loads(Path(html_payload["manifest"]).read_text(encoding="utf-8")) + source_sha = hashlib.sha256(source.read_bytes()).hexdigest() + self.assertEqual(html_payload["flow_marker"], "FLOW_OK") + self.assertEqual(manifest["kind"], "mission-flow") + self.assertEqual(manifest["source_sha256"], source_sha) + self.assertIn(source_sha, html) + self.assertIn("Mission Flow", html) + self.assertIn("Safe Writes", html) + self.assertIn("HTML is presentation, not source of truth.", html) + + def test_render_html_rich_report_is_self_contained_composable_and_stale_detectable(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli( + "init-project", + "--root", + str(ROOT), + "--project-root", + str(project), + "--name", + "Example", + "--global-root", + str(ROOT / "global-agent-fabric"), + "--capability-root", + str(ROOT / "capability-layer"), + ) + drafts = project / "reports" / "drafts" + drafts.mkdir(parents=True) + report_a = drafts / "alpha.md" + report_b = drafts / "beta.md" + report_a.write_text( + "# Alpha Report\n\n## Findings\n\n- Evidence lane one\n\n```text\nTRACE_OK\n```\n\n<script>alert('x')</script>\n", + encoding="utf-8", + ) + report_b.write_text("# Beta Report\n\n## Findings\n\nHuman handoff content.\n", encoding="utf-8") + + rendered_a = self.run_cli( + "render-html", + "--project-root", + str(project), + "--input", + "reports/drafts/alpha.md", + "--kind", + "rich-report", + "--output", "reports/drafts/alpha.html", "--theme", 'bad"></style><script>x</script>', @@ -2581,6 +3653,138 @@ def test_render_html_rich_report_is_self_contained_composable_and_stale_detectab self.assertIn("status: ok", doctor.stdout) self.assertIn("status: ready", (project / ".agent-os" / "tasks.yaml").read_text(encoding="utf-8")) + def test_render_html_presentation_modes_keep_metadata_but_loosen_layout(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + drafts = project / "reports" / "drafts" + drafts.mkdir(parents=True) + report = drafts / "layout.md" + report.write_text("# Layout Report\n\n## Finding\n\n- Evidence over template lock-in.\n", encoding="utf-8") + source_sha = hashlib.sha256(report.read_bytes()).hexdigest() + + default_render = self.run_cli( + "render-html", + "--project-root", + str(project), + "--input", + "reports/drafts/layout.md", + "--kind", + "rich-report", + "--output", + "reports/drafts/default.html", + "--json", + ) + default_payload = json.loads(default_render.stdout) + default_html = Path(default_payload["output"]).read_text(encoding="utf-8") + default_manifest = json.loads(Path(default_payload["manifest"]).read_text(encoding="utf-8")) + self.assertEqual(default_payload["presentation"], "default") + self.assertEqual(default_manifest["presentation"], "default") + self.assertIn("kos-hero", default_html) + self.assertIn("kos-panel", default_html) + self.assertIn(source_sha, default_html) + + minimal_render = self.run_cli( + "render-html", + "--project-root", + str(project), + "--input", + "reports/drafts/layout.md", + "--kind", + "rich-report", + "--output", + "reports/drafts/minimal.html", + "--presentation", + "minimal", + "--json", + ) + minimal_payload = json.loads(minimal_render.stdout) + minimal_html = Path(minimal_payload["output"]).read_text(encoding="utf-8") + minimal_manifest = json.loads(Path(minimal_payload["manifest"]).read_text(encoding="utf-8")) + self.assertEqual(minimal_payload["presentation"], "minimal") + self.assertEqual(minimal_manifest["presentation"], "minimal") + self.assertTrue(minimal_manifest["html_required_metadata"]) + self.assertIn(source_sha, minimal_html) + self.assertIn("HTML is presentation, not source of truth.", minimal_html) + self.assertNotIn("kos-hero", minimal_html) + self.assertNotIn("kos-panel", minimal_html) + self.assertNotIn("<script", minimal_html.lower()) + self.assertNotIn("<link", minimal_html.lower()) + self.assertNotIn('src="http', minimal_html.lower()) + self.assertNotIn('href="http', minimal_html.lower()) + + bare_render = self.run_cli( + "render-html", + "--project-root", + str(project), + "--input", + "reports/drafts/layout.md", + "--kind", + "rich-report", + "--output", + "reports/drafts/bare.html", + "--presentation", + "bare", + "--json", + ) + bare_payload = json.loads(bare_render.stdout) + bare_html = Path(bare_payload["output"]).read_text(encoding="utf-8") + self.assertEqual(bare_payload["presentation"], "bare") + self.assertIn("<!doctype html>", bare_html.lower()) + self.assertIn(source_sha, bare_html) + self.assertIn("presentation=bare", bare_html) + self.assertNotIn("kos-hero", bare_html) + self.assertNotIn("kos-panel", bare_html) + + fragment_render = self.run_cli( + "render-html", + "--project-root", + str(project), + "--input", + "reports/drafts/layout.md", + "--kind", + "rich-report", + "--output", + "reports/drafts/fragment.html", + "--presentation", + "fragment", + "--json", + ) + fragment_payload = json.loads(fragment_render.stdout) + fragment_html = Path(fragment_payload["fragment"]).read_text(encoding="utf-8") + fragment_manifest = json.loads(Path(fragment_payload["manifest"]).read_text(encoding="utf-8")) + self.assertEqual(fragment_payload["presentation"], "fragment") + self.assertEqual(fragment_payload["output"], "") + self.assertFalse((project / "reports" / "drafts" / "fragment.html").exists()) + self.assertEqual(fragment_manifest["presentation"], "fragment") + self.assertEqual(fragment_manifest["output"], "") + self.assertIn(source_sha, fragment_html) + self.assertIn("HTML is presentation, not source of truth.", fragment_html) + + project_yaml = project / ".agent-os" / "project.yaml" + project_yaml.write_text( + project_yaml.read_text(encoding="utf-8") + + "\nreporting:\n html_sidecars: true\n html_source_of_truth: false\n html_presentation_default: minimal\n html_required_metadata: true\n", + encoding="utf-8", + ) + policy_render = self.run_cli( + "render-html", + "--project-root", + str(project), + "--input", + "reports/drafts/layout.md", + "--kind", + "rich-report", + "--output", + "reports/drafts/policy-default.html", + "--json", + ) + policy_payload = json.loads(policy_render.stdout) + policy_html = Path(policy_payload["output"]).read_text(encoding="utf-8") + self.assertEqual(policy_payload["presentation"], "minimal") + self.assertNotIn("kos-hero", policy_html) + def test_doctor_passes_initialized_project(self): with tempfile.TemporaryDirectory() as tmp: project = Path(tmp) / "ExampleProject" @@ -2608,8 +3812,47 @@ def test_doctor_passes_initialized_project(self): self.assertTrue(any(item["label"] == "phase_keys" for item in payload)) self.assertTrue(any(item["label"] == "workflow_router" for item in payload)) self.assertTrue(any(item["label"] == "tool_registry" for item in payload)) + self.assertTrue(any(item["label"] == "decision_policy" for item in payload)) self.assertTrue(any(item["label"] == "effect_policy" for item in payload)) + def test_decision_policy_defaults_to_warn_and_requires_reason_when_disabled(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + runtime = Path(tmp) / "KnowledgeOSRuntime" + self.run_cli("init-os", "--root", str(ROOT), "--os-root", str(runtime), "--json") + self.run_cli( + "init-project", + "--root", + str(ROOT), + "--project-root", + str(project), + "--name", + "Example", + "--global-root", + str(runtime / "global-agent-fabric"), + "--capability-root", + str(runtime / "capability-layer"), + ) + policy = project / ".agent-os" / "decision-policy.yaml" + self.assertTrue(policy.exists()) + policy.unlink() + missing = self.run_cli("doctor", "--root", str(ROOT), "--project-root", str(project), "--summary") + self.assertIn("status: ok", missing.stdout) + + policy.write_text("decision_policy:\n strictness: off\n downgrade_reason:\n", encoding="utf-8") + disabled = self.run_cli("doctor", "--root", str(ROOT), "--project-root", str(project), "--summary", check=False) + self.assertEqual(disabled.returncode, 1) + self.assertIn("decision_policy", disabled.stdout) + self.assertIn("strictness=off requires downgrade_reason", disabled.stdout) + + policy.write_text( + "decision_policy:\n strictness: off\n downgrade_reason: temporary exploration mode\n", + encoding="utf-8", + ) + reasoned = self.run_cli("doctor", "--root", str(ROOT), "--project-root", str(project), "--summary") + self.assertIn("status: ok", reasoned.stdout) + def test_effect_policy_defaults_to_observe_and_requires_reason_when_disabled(self): with tempfile.TemporaryDirectory() as tmp: project = Path(tmp) / "ExampleProject" @@ -2901,7 +4144,10 @@ def test_tool_registry_reports_configured_tools(self): self.assertGreaterEqual(payload["counts"].get("mcp", 0), 1) self.assertGreaterEqual(payload["counts"].get("skill", 0), 1) self.assertGreaterEqual(payload["counts"].get("orchestrator", 0), 1) - self.assertGreaterEqual(payload["counts"].get("subagent", 0), 40) + self.assertGreaterEqual(payload["counts"].get("subagent", 0), 43) + self.assertIn("codex-default", result.stdout) + self.assertIn("codex-explorer", result.stdout) + self.assertIn("codex-worker", result.stdout) self.assertIn("maestro-mcp", result.stdout) self.assertIn("maestro-architect", result.stdout) self.assertIn("maestro-coder", result.stdout) @@ -2913,16 +4159,355 @@ def test_dispatch_surfaces_maestro_specialist_subagents(self): result = self.run_cli("dispatch-task", "--project-root", str(ROOT), "--task-id", "KOS-T009", "--json") payload = json.loads(result.stdout) self.assertEqual(payload["status"], "dispatch_ready") + self.assertEqual(payload["dispatch_marker"], "AGENT_DISPATCH_PLAN") + self.assertIn("AGENT_DISPATCH_PLAN", payload["marker"]) + self.assertGreaterEqual(payload["dispatch_summary"]["planned_agents"], 1) + self.assertGreaterEqual(payload["dispatch_summary"]["runtime_callable_agents"], 1) stages = {step["stage"]: step for step in payload["steps"]} self.assertIn("subagent", stages) + self.assertLessEqual(len(stages["subagent"]["tools"]), 3) subagent_ids = {tool["id"] for tool in stages["subagent"]["tools"]} + self.assertIn("codex-default", subagent_ids) self.assertIn("maestro-architect", subagent_ids) self.assertIn("maestro-coder", subagent_ids) - self.assertIn("maestro-security-engineer", subagent_ids) + for tool in stages["subagent"]["tools"]: + self.assertEqual(tool.get("runtime_tool"), "multi_agent_v1.spawn_agent") + self.assertTrue(tool.get("runtime_callable")) orchestrator_ids = {tool["id"] for tool in stages["orchestrator"]["tools"]} self.assertIn("maestro", orchestrator_ids) self.assertNotIn("agent-orchestrator", orchestrator_ids) + def test_dispatch_task_plain_output_shows_agent_dispatch_plan(self): + result = self.run_cli("dispatch-task", "--project-root", str(ROOT), "--task-id", "KOS-T009") + self.assertIn("AGENT_DISPATCH_PLAN", result.stdout) + self.assertIn("planned_agents:", result.stdout) + self.assertIn("maestro", result.stdout) + + def test_dispatch_report_summarizes_actual_capability_events(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + self.run_cli("dispatch-task", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json") + self.run_cli( + "capability-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "orchestrator", + "--id", + "maestro", + "--purpose", + "Coordinate explicit dispatch reporting test.", + ) + self.run_cli( + "capability-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "shell", + "--id", + "unit-test", + "--purpose", + "Run targeted dispatch report test.", + ) + self.run_cli( + "capability-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "github", + "--id", + "github-connector", + "--purpose", + "Inspect mounted GitHub capability reporting.", + ) + self.run_cli( + "capability-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "browser", + "--id", + "browser-plugin", + "--purpose", + "Browser capability was considered but not needed for this CLI test.", + "--status", + "skipped", + "--evidence", + "no UI target", + ) + self.run_cli( + "capability-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "subagent", + "--id", + "branch-builder", + "--purpose", + "Required branch planning was skipped because this test has a single deterministic path.", + "--status", + "skipped", + "--evidence", + "single-path test", + ) + + plain = self.run_cli("dispatch-report", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id) + self.assertIn("AGENT_DISPATCH_OK agents=1 capabilities=3", plain.stdout) + self.assertIn("Full Capability Dispatch Report", plain.stdout) + self.assertIn("orchestrator: maestro", plain.stdout) + self.assertIn("github: github-connector", plain.stdout) + self.assertIn("browser: used=0, skipped=1", plain.stdout) + self.assertIn("skipped_or_not_needed:", plain.stdout) + self.assertIn("gaps:", plain.stdout) + + as_json = self.run_cli("dispatch-report", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json") + payload = json.loads(as_json.stdout) + self.assertEqual(payload["dispatch_report_marker"], "AGENT_DISPATCH_OK") + self.assertTrue(payload["full_capability_report"]) + self.assertEqual(payload["agent_count"], 1) + self.assertEqual(payload["capability_count"], 3) + self.assertEqual(payload["skipped_count"], 2) + self.assertEqual(payload["counts_by_kind"]["github"], 1) + self.assertEqual(payload["counts_by_kind"]["browser"], 0) + self.assertEqual(payload["skipped_by_kind"]["browser"], 1) + self.assertEqual(payload["skipped_by_kind"]["subagent"], 1) + self.assertIn("capability_events", payload["evidence"]) + self.assertIn("command_events", payload["evidence"]) + self.assertEqual(payload["gaps"], ["none"]) + self.assertTrue(Path(payload["report"]).exists()) + report = Path(payload["report"]).read_text(encoding="utf-8") + self.assertIn("# Full Capability Dispatch Report", report) + self.assertIn("Meaning: AGENT_DISPATCH_OK summarizes all recorded external and mounted capabilities", report) + self.assertIn("## Skipped Or Not Needed", report) + + def test_dispatch_report_records_runtime_subagent_gaps(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + self.run_cli("dispatch-task", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json") + self.run_cli( + "capability-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "orchestrator", + "--id", + "maestro", + "--purpose", + "Satisfy required orchestrator dispatch stage.", + ) + self.run_cli( + "capability-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "subagent", + "--id", + "codex-explorer", + "--purpose", + "Live runtime smoke did not return before timeout.", + "--status", + "timed_out", + "--evidence", + "spawn_agent returned an id; wait_agent timed out; close_agent was aborted", + ) + result = self.run_cli("dispatch-report", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json") + payload = json.loads(result.stdout) + self.assertEqual(payload["agent_count"], 1) + self.assertEqual(payload["skipped_agent_count"], 1) + self.assertEqual(payload["runtime_gap_count"], 1) + self.assertEqual(payload["runtime_gaps"][0]["id"], "codex-explorer") + self.assertIn("runtime subagent gaps: codex-explorer=timed_out", payload["gaps"]) + + def test_dispatch_report_resolves_late_results_and_keeps_policy_blocks_out_of_runtime_gaps(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + self.run_cli("dispatch-task", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json") + cli.record_capability_event( + project, + "T001", + run_id, + kind="orchestrator", + capability_id="maestro", + purpose="Satisfy the required orchestrator stage for this dispatch-report fixture.", + ) + purpose = "Read-only explorer review." + timeout_event = cli.record_capability_event( + project, + "T001", + run_id, + kind="subagent", + capability_id="codex-explorer", + purpose=purpose, + status="timed_out", + evidence="wait window elapsed", + ) + cli.record_capability_event( + project, + "T001", + run_id, + kind="subagent", + capability_id="codex-explorer", + purpose=purpose, + status="completed", + evidence="late result received; recovered_from=timed_out", + recovers_event_id=timeout_event["capability_event_id"], + ) + cli.record_capability_event( + project, + "T001", + run_id, + kind="subagent", + capability_id="codex-worker", + purpose="Stopped at a safety policy gate.", + status="blocked", + evidence="policy decision, not a runtime failure", + ) + + result = self.run_cli("dispatch-report", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json") + payload = json.loads(result.stdout) + self.assertEqual(payload["runtime_gap_count"], 0) + self.assertEqual(payload["resolved_runtime_gap_count"], 1) + self.assertEqual(payload["resolved_runtime_gaps"][0]["id"], "codex-explorer") + self.assertEqual(payload["gaps"], ["none"]) + self.assertEqual(payload["skipped_agent_count"], 2) + + def test_dispatch_report_does_not_treat_failed_event_as_timeout_recovery(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + purpose = "Bounded review." + cli.record_capability_event(project, "T001", run_id, kind="subagent", capability_id="codex-explorer", purpose=purpose, status="timed_out", evidence="wait elapsed") + cli.record_capability_event(project, "T001", run_id, kind="subagent", capability_id="codex-explorer", purpose=purpose, status="failed", evidence="recovered_from=timed_out") + + payload = cli.build_dispatch_report(project, "T001", run_id) + self.assertEqual(payload["runtime_gap_count"], 2) + self.assertEqual(payload["resolved_runtime_gap_count"], 0) + + def test_dispatch_report_treats_failed_subagent_as_runtime_gap(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + cli.record_capability_event( + project, "T001", run_id, kind="subagent", capability_id="codex-explorer", purpose="Failed runtime.", status="failed", evidence="runtime returned failure" + ) + payload = cli.build_dispatch_report(project, "T001", run_id) + self.assertEqual(payload["agent_count"], 0) + self.assertEqual(payload["runtime_gap_count"], 1) + + def test_dispatch_report_recovery_is_one_to_one_by_event_id(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + first = cli.record_capability_event(project, "T001", run_id, kind="subagent", capability_id="codex-explorer", purpose="Same purpose.", status="timed_out", evidence="wait elapsed") + cli.record_capability_event(project, "T001", run_id, kind="subagent", capability_id="codex-explorer", purpose="Same purpose.", status="timed_out", evidence="second wait elapsed") + cli.record_capability_event( + project, "T001", run_id, kind="subagent", capability_id="codex-explorer", purpose="Same purpose.", status="completed", + evidence="late result", recovers_event_id=first["capability_event_id"], + ) + + payload = cli.build_dispatch_report(project, "T001", run_id) + self.assertEqual(payload["runtime_gap_count"], 1) + self.assertEqual(payload["resolved_runtime_gap_count"], 1) + + def test_complete_task_returns_agent_dispatch_summary(self): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "ExampleProject" + project.mkdir() + self.run_cli("init-project", "--root", str(ROOT), "--project-root", str(project), "--name", "Example") + started = self.run_cli("run-task", "--project-root", str(project), "--task-id", "T001", "--json") + run_id = json.loads(started.stdout)["run_id"] + self.write_plan_context(project, "T001", run_id) + self.run_cli("dispatch-task", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id, "--json") + self.run_cli( + "capability-event", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--kind", + "orchestrator", + "--id", + "maestro", + "--purpose", + "Coordinate completion dispatch summary test.", + ) + self.log_required_phases(project, "T001", run_id) + self.run_cli("eval-task", "--project-root", str(project), "--task-id", "T001", "--run-id", run_id) + + completed = self.run_cli( + "complete-task", + "--project-root", + str(project), + "--task-id", + "T001", + "--run-id", + run_id, + "--summary", + "Completed with explicit agent dispatch summary.", + "--allow-pending-postflight", + "temp project has no executable shared-fabric hook", + "--json", + ) + payload = json.loads(completed.stdout) + self.assertEqual(payload["agent_dispatch_status"], "AGENT_DISPATCH_OK") + self.assertIn("AGENT_DISPATCH_OK", payload["agent_dispatch_marker"]) + self.assertGreaterEqual(payload["agents_invoked"], 1) + self.assertTrue(Path(payload["dispatch_report"]).exists()) + receipt = (project / ".agent-os" / "runs" / run_id / "receipt.md").read_text(encoding="utf-8") + self.assertIn("Agent Dispatch Marker:", receipt) + self.assertIn("AGENT_DISPATCH_OK", receipt) + def test_tool_registry_rejects_inline_secret_markers(self): with tempfile.TemporaryDirectory() as tmp: project = Path(tmp) / "ExampleProject" @@ -2959,21 +4544,37 @@ def test_agent_guide_outputs_operational_checklist(self): self.assertIn("check-route-write", result.stdout) self.assertIn("create-spec", result.stdout) self.assertIn("align-spec", result.stdout) + self.assertIn("thread-plan", result.stdout) + self.assertIn("THREAD_PLAN_OK", result.stdout) + self.assertIn("KOS_DECISION", result.stdout) + self.assertIn("full capability dispatch report", result.stdout) self.assertIn("context-pack", result.stdout) self.assertIn("plan-task", result.stdout) + self.assertIn("AGENT_DISPATCH_PLAN", result.stdout) self.assertIn("verify-context", result.stdout) self.assertIn("trace-step", result.stdout) + self.assertIn("KOS_DECISION", result.stdout) self.assertIn("TRACE_OK", result.stdout) self.assertIn("phase-task", result.stdout) self.assertIn("CHECKPOINT_OK", result.stdout) self.assertIn("capability-event", result.stdout) self.assertIn("CAPABILITY_OK", result.stdout) + self.assertIn("dispatch-report", result.stdout) + self.assertIn("AGENT_DISPATCH_OK", result.stdout) + self.assertIn("decision-event", result.stdout) + self.assertIn("DECISION_OK", result.stdout) + self.assertIn("verify-decisions", result.stdout) + self.assertIn("DECISION_VERIFY_OK", result.stdout) self.assertIn("artifact-assert", result.stdout) self.assertIn("EFFECT_OK", result.stdout) self.assertIn("verify-lifecycle", result.stdout) self.assertIn("verify-effects", result.stdout) self.assertIn("EFFECT_VERIFY_OK", result.stdout) self.assertIn("complete-task", result.stdout) + self.assertIn("flow-summary", result.stdout) + self.assertIn("FLOW_OK", result.stdout) + self.assertIn("thread-plan", result.stdout) + self.assertIn("THREAD_PLAN_OK", result.stdout) self.assertIn("archive-legacy-project", result.stdout) def test_startup_prompt_outputs_trace_step_contract(self): @@ -2986,12 +4587,29 @@ def test_startup_prompt_outputs_trace_step_contract(self): self.assertIn("TRACE_OK", result.stdout) self.assertIn("phase-task", result.stdout) self.assertIn("CHECKPOINT_OK", result.stdout) + self.assertIn("AGENT_DISPATCH_PLAN", result.stdout) self.assertIn("capability-event", result.stdout) self.assertIn("CAPABILITY_OK", result.stdout) + self.assertIn("dispatch-report", result.stdout) + self.assertIn("AGENT_DISPATCH_OK", result.stdout) self.assertIn("artifact-assert", result.stdout) self.assertIn("EFFECT_OK", result.stdout) self.assertIn("verify-effects", result.stdout) self.assertIn("EFFECT_VERIFY_OK", result.stdout) + self.assertIn("flow-summary", result.stdout) + self.assertIn("FLOW_OK", result.stdout) + + def test_static_prompt_contracts_require_dispatch_report_markers(self): + template = (ROOT / "templates" / "project-control-plane" / "AGENTS.md").read_text(encoding="utf-8") + guide = (ROOT / "docs" / "agent-guide.md").read_text(encoding="utf-8") + startup = (ROOT / ".agent-os" / "startup-prompt.md").read_text(encoding="utf-8") + for content in (template, guide, startup): + self.assertIn("KOS_DECISION", content) + self.assertIn("AGENT_DISPATCH_PLAN", content) + self.assertIn("dispatch-report", content) + self.assertIn("AGENT_DISPATCH_OK", content) + self.assertIn("full capability", content.lower()) + self.assertIn("complete-task", content) def test_dispatch_task_prioritizes_branch_builder_and_consultation(self): result = self.run_cli("dispatch-task", "--project-root", str(ROOT), "--task-id", "KOS-T009", "--json")