From 89e37f429481751e07899f8f9938e5270300043c Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 21:18:11 -0400 Subject: [PATCH 01/48] docs: clarify Windows compile-parity requirements for CI checkpoints --- .../kickoff_prompts/CP1-ci-checkpoint.md | 3 +- .../kickoff_prompts/CP2-ci-checkpoint.md | 3 +- .../plan.md | 1 + .../pre-planning/ci_checkpoint_plan.md | 2 ++ .../session_log.md | 36 +++++++++++++++++++ .../tasks.json | 4 +++ 6 files changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md index 98872b01f..259a73218 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md @@ -16,7 +16,8 @@ Do not edit planning docs inside the worktree. ## Requirements - Run the advisory CI audit if available, then dispatch compile parity for the checkpoint checkout SHA. - Record run ids, URLs, and any required Linux/macOS/Windows parity follow-ups in `session_log.md`. -- Keep the task scoped to compile parity. `pre-planning/ci_checkpoint_plan.md` currently leaves feature smoke disabled for this checkpoint. +- Keep the task scoped to compile parity. `pre-planning/ci_checkpoint_plan.md` leaves feature smoke disabled for this checkpoint, including Windows. +- Do not dispatch or require `scripts/windows/wsl-smoke.ps1` as part of this checkpoint task. ## End Checklist 1. Confirm compile parity is green or record the blocking failures precisely. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md index 2b0142458..cb9b76714 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md @@ -16,7 +16,8 @@ Do not edit planning docs inside the worktree. ## Requirements - Run the advisory CI audit if available, then dispatch compile parity for the checkpoint checkout SHA. - Record run ids, URLs, and any required Linux/macOS/Windows parity follow-ups in `session_log.md`. -- Keep the task scoped to compile parity. `pre-planning/ci_checkpoint_plan.md` currently leaves feature smoke disabled for this checkpoint. +- Keep the task scoped to compile parity. `pre-planning/ci_checkpoint_plan.md` leaves feature smoke disabled for this checkpoint, including Windows. +- Do not dispatch or require `scripts/windows/wsl-smoke.ps1` as part of this checkpoint task. ## End Checklist 1. Confirm compile parity is green or record the blocking failures precisely. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md index b406a4d0a..cd470d330 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md @@ -46,6 +46,7 @@ - `tasks.json` `meta.checkpoint_boundaries` stays `["AHCSITC2", "AHCSITC3"]`. - `tasks.json` `meta.ci_parity_platforms_required` stays `["linux", "macos", "windows"]`. - `tasks.json` `meta.behavior_platforms_required` stays `[]` until a later execution lane adds feature smoke scripts and explicitly widens the behavior-platform requirement. +- Windows stays inside the checkpoint compile-parity set only. Neither `CP1-ci-checkpoint` nor `CP2-ci-checkpoint` requires Windows feature smoke. - `AHCSITC0` and `AHCSITC1` stay normal schema-v4 slices with a single final `*-integ` task. - `AHCSITC2` and `AHCSITC3` use the full boundary model: `*-integ-core`, `*-integ-linux`, `*-integ-macos`, `*-integ-windows`, and final `*-integ`. - The platform-fix tasks for this pack are parity-only follow-up tasks. They do not imply required feature-smoke coverage. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md index 9d44c0c66..ad94b19fc 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md @@ -73,6 +73,7 @@ What risk is reduced by running cross-platform CI here: - Compile parity catches cross-platform build and type drift across `crates/shell`, `crates/agent-api-*`, `crates/common`, and `crates/trace` before the final validation slice starts. - `ci_testing = quick` exercises the high-risk contract and telemetry path without paying full-suite cost before the parity and compatibility closure slice lands. - `feature_smoke = false` in this first pass because the pack defines no feature-local `smoke/` surface and `spec_manifest.md` assigns deterministic validation ownership to `manual_testing_playbook.md`. +- Windows remains part of the compile-parity checkpoint set only. The Windows WSL warm or smoke flow is not a CI checkpoint requirement for `CP1`. Checkpoint-size justification: - This group contains 3 slices, which stays within the machine-readable checkpoint range of 1 to 8 slices. @@ -93,6 +94,7 @@ What risk is reduced by running cross-platform CI here: - Compile parity confirms the final compatibility and parity edits did not reopen cross-platform shell or trace regressions. - `ci_testing = full` provides the last cross-platform confidence gate before planning promotes this pack into execution. - `feature_smoke = false` remains correct in this first pass because the pack still has no feature-local smoke surface. Full planning updates this gate if a smoke workflow becomes part of the accepted validation contract. +- Windows remains part of the compile-parity checkpoint set only. The Windows WSL warm or smoke flow is not a CI checkpoint requirement for `CP2`. Checkpoint-size justification: - This group contains 1 slice, which stays within the machine-readable checkpoint range of 1 to 8 slices. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index b323e5922..449dbf45c 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -77,3 +77,39 @@ - `NONE` - Next steps: - The pack is execution-ready on the owned planning surfaces. + +## START — 2026-04-24T21:05:00Z — planning — checkpoint scope clarification +- Goal: clarify that Windows remains compile parity only at `CP1` and `CP2` and is not a feature-smoke requirement for CI checkpoints. +- Inputs read: + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/platform-parity-spec.md` +- Commands planned: + - `python3 docs/project_management/system/scripts/planning/validate_tasks_json.py --feature-dir "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible"` + - `python3 docs/project_management/system/scripts/planning/validate_ci_checkpoint_plan.py --feature-dir "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible"` + - `make planning-micro-lint FEATURE_DIR="docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" OWNED_PATHS="plan.md tasks.json session_log.md kickoff_prompts/CP1-ci-checkpoint.md kickoff_prompts/CP2-ci-checkpoint.md pre-planning/ci_checkpoint_plan.md"` + +## END — 2026-04-24T21:05:00Z — planning — checkpoint scope clarification +- Summary of changes: + - Updated `plan.md` to state explicitly that Windows stays in the CI compile-parity set only and is not a checkpoint feature-smoke requirement. + - Updated `pre-planning/ci_checkpoint_plan.md` so both `CP1` and `CP2` call out that Windows WSL warm or smoke flows are outside checkpoint scope. + - Updated checkpoint kickoff prompts and `tasks.json` acceptance criteria or checklists so operators do not dispatch Windows feature smoke during checkpoint execution. +- Files created or modified: + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md` +- Validation commands run (with results): + - `python3 docs/project_management/system/scripts/planning/validate_tasks_json.py --feature-dir "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible"` → `0` → `PASS` + - `python3 docs/project_management/system/scripts/planning/validate_ci_checkpoint_plan.py --feature-dir "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible"` → `0` → `PASS` + - `make planning-micro-lint FEATURE_DIR="docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" OWNED_PATHS="plan.md tasks.json session_log.md kickoff_prompts/CP1-ci-checkpoint.md kickoff_prompts/CP2-ci-checkpoint.md pre-planning/ci_checkpoint_plan.md"` → `0` → `PASS` +- Blockers: + - `NONE` +- Next steps: + - Use the checkpoint tasks as compile-parity-only gates unless a later planning change explicitly introduces a feature-local smoke surface. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index ddbf6ab86..4b71b1021 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -524,6 +524,7 @@ "acceptance_criteria": [ "The checkpoint audit trail is recorded in session_log.md.", "Compile parity is green for the checkpoint checkout SHA.", + "Windows remains a compile-parity checkpoint lane only; no Windows feature-smoke run is required.", "The task only validates the checkpoint boundary defined by pre-planning/ci_checkpoint_plan.md." ], "start_checklist": [ @@ -534,6 +535,7 @@ "end_checklist": [ "Dispatch compile parity for the checkpoint checkout SHA and record run ids/URLs.", "Record any checkpoint blockers or required platform follow-ups in session_log.md.", + "Do not dispatch or require Windows feature smoke for this checkpoint.", "Set status to completed; add END entry; commit docs." ], "worktree": null, @@ -929,6 +931,7 @@ "acceptance_criteria": [ "The checkpoint audit trail is recorded in session_log.md.", "Compile parity is green for the checkpoint checkout SHA.", + "Windows remains a compile-parity checkpoint lane only; no Windows feature-smoke run is required.", "The task only validates the checkpoint boundary defined by pre-planning/ci_checkpoint_plan.md." ], "start_checklist": [ @@ -939,6 +942,7 @@ "end_checklist": [ "Dispatch compile parity for the checkpoint checkout SHA and record run ids/URLs.", "Record any checkpoint blockers or required platform follow-ups in session_log.md.", + "Do not dispatch or require Windows feature smoke for this checkpoint.", "Set status to completed; add END entry; commit docs." ], "worktree": null, From 12a3690910869c89f6c9eccd079a3b66441054d9 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 21:19:27 -0400 Subject: [PATCH 02/48] docs: start AHCSITC0 code+test --- .../session_log.md | 8 + .../tasks.json | 1218 ++++++++--------- 2 files changed, 617 insertions(+), 609 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 449dbf45c..c658bb1e2 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -113,3 +113,11 @@ - `NONE` - Next steps: - Use the checkpoint tasks as compile-parity-only gates unless a later planning change explicitly introduces a feature-local smoke surface. + +## START — 2026-04-25T01:19:27Z — code — AHCSITC0-code +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC0"` + +## START — 2026-04-25T01:19:27Z — test — AHCSITC0-test +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC0"` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 4b71b1021..cabe7e8ef 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -1,49 +1,30 @@ { "meta": { - "schema_version": 4, - "slice_spec_version": 2, - "feature": "agent-hub-core-successor-identity-tuple-compatible", - "cross_platform": true, - "execution_gates": false, + "adr_paths": [ + "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" + ], "automation": { "enabled": true, "orchestration_branch": "feat/agent-hub-core-successor-identity-tuple-compatible" }, "behavior_platforms_required": [], + "checkpoint_boundaries": [ + "AHCSITC2", + "AHCSITC3" + ], "ci_parity_platforms_required": [ "linux", "macos", "windows" ], - "checkpoint_boundaries": [ - "AHCSITC2", - "AHCSITC3" - ], - "adr_paths": [ - "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" - ] + "cross_platform": true, + "execution_gates": false, + "feature": "agent-hub-core-successor-identity-tuple-compatible", + "schema_version": 4, + "slice_spec_version": 2 }, "tasks": [ { - "id": "AHCSITC0-code", - "name": "AHCSITC0 slice (code)", - "type": "code", - "phase": "AHCSITC0", - "status": "pending", - "description": "Implement the AHCSITC0 operator contract and identity presentation behaviors.", - "references": [ - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC0/AHCSITC0-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", - "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" - ], - "acceptance_criteria": [ - "Implements the behaviors required by ac_ids (see slices/AHCSITC0/AHCSITC0-spec.md)." - ], "ac_ids": [ "AC-AHCSITC0-01", "AC-AHCSITC0-02", @@ -52,37 +33,26 @@ "AC-AHCSITC0-05", "AC-AHCSITC0-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC0\"" + "acceptance_criteria": [ + "Implements the behaviors required by ac_ids (see slices/AHCSITC0/AHCSITC0-spec.md)." + ], + "concurrent_with": [ + "AHCSITC0-test" ], + "depends_on": [], + "description": "Implement the AHCSITC0 operator contract and identity presentation behaviors.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC0-code\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-code", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-code", + "id": "AHCSITC0-code", "integration_task": "AHCSITC0-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC0/kickoff_prompts/AHCSITC0-code.md", - "depends_on": [], - "concurrent_with": [ - "AHCSITC0-test" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-code", - "required_make_targets": [ - "triad-code-checks" - ] - }, - { - "id": "AHCSITC0-test", - "name": "AHCSITC0 slice (test)", - "type": "test", + "name": "AHCSITC0 slice (code)", "phase": "AHCSITC0", - "status": "pending", - "description": "Add tests that enforce the AHCSITC0 operator contract and identity presentation behaviors.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -93,9 +63,20 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC0/AHCSITC0-spec.md)." + "required_make_targets": [ + "triad-code-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC0\"" ], + "status": "in_progress", + "type": "code", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-code" + }, + { "ac_ids": [ "AC-AHCSITC0-01", "AC-AHCSITC0-02", @@ -104,37 +85,26 @@ "AC-AHCSITC0-05", "AC-AHCSITC0-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC0\"" + "acceptance_criteria": [ + "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC0/AHCSITC0-spec.md)." + ], + "concurrent_with": [ + "AHCSITC0-code" ], + "depends_on": [], + "description": "Add tests that enforce the AHCSITC0 operator contract and identity presentation behaviors.", "end_checklist": [ "cargo fmt", "Run the targeted tests you add or touch.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC0-test\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-test", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-test", + "id": "AHCSITC0-test", "integration_task": "AHCSITC0-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC0/kickoff_prompts/AHCSITC0-test.md", - "depends_on": [], - "concurrent_with": [ - "AHCSITC0-code" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-test", - "required_make_targets": [ - "triad-test-checks" - ] - }, - { - "id": "AHCSITC0-integ", - "name": "AHCSITC0 slice (integration final)", - "type": "integration", + "name": "AHCSITC0 slice (test)", "phase": "AHCSITC0", - "status": "pending", - "description": "Finalize AHCSITC0 after the code and test branches merge cleanly.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -143,11 +113,22 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", - "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" + "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Slice is green under make integ-checks and implements the behaviors required by ac_ids." + "required_make_targets": [ + "triad-test-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC0\"" ], + "status": "in_progress", + "type": "test", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-test" + }, + { "ac_ids": [ "AC-AHCSITC0-01", "AC-AHCSITC0-02", @@ -156,12 +137,15 @@ "AC-AHCSITC0-05", "AC-AHCSITC0-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC0-integ\"" + "acceptance_criteria": [ + "Slice is green under make integ-checks and implements the behaviors required by ac_ids." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC0-code", + "AHCSITC0-test" ], + "description": "Finalize AHCSITC0 after the code and test branches merge cleanly.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", @@ -170,40 +154,37 @@ "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC0-integ\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-integ", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-integ", + "id": "AHCSITC0-integ", "integration_task": "AHCSITC0-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC0/kickoff_prompts/AHCSITC0-integ.md", - "depends_on": [ - "AHCSITC0-code", - "AHCSITC0-test" - ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-integ", - "required_make_targets": [ - "integ-checks" - ], - "merge_to_orchestration": true - }, - { - "id": "AHCSITC1-code", - "name": "AHCSITC1 slice (code)", - "type": "code", - "phase": "AHCSITC1", - "status": "pending", - "description": "Implement the AHCSITC1 session protocol and placement behaviors.", + "merge_to_orchestration": true, + "name": "AHCSITC0 slice (integration final)", + "phase": "AHCSITC0", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/AHCSITC1-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC0/AHCSITC0-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", - "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", + "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "acceptance_criteria": [ - "Implements the behaviors required by ac_ids (see slices/AHCSITC1/AHCSITC1-spec.md)." + "required_make_targets": [ + "integ-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC0-integ\"" ], + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-integ" + }, + { "ac_ids": [ "AC-AHCSITC1-01", "AC-AHCSITC1-02", @@ -212,39 +193,28 @@ "AC-AHCSITC1-05", "AC-AHCSITC1-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC1-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" + "acceptance_criteria": [ + "Implements the behaviors required by ac_ids (see slices/AHCSITC1/AHCSITC1-spec.md)." + ], + "concurrent_with": [ + "AHCSITC1-test" + ], + "depends_on": [ + "AHCSITC0-integ" ], + "description": "Implement the AHCSITC1 session protocol and placement behaviors.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC1-code\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-code", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-code", + "id": "AHCSITC1-code", "integration_task": "AHCSITC1-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/kickoff_prompts/AHCSITC1-code.md", - "depends_on": [ - "AHCSITC0-integ" - ], - "concurrent_with": [ - "AHCSITC1-test" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-code", - "required_make_targets": [ - "triad-code-checks" - ] - }, - { - "id": "AHCSITC1-test", - "name": "AHCSITC1 slice (test)", - "type": "test", + "name": "AHCSITC1 slice (code)", "phase": "AHCSITC1", - "status": "pending", - "description": "Add tests that enforce the AHCSITC1 session protocol and placement behaviors.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -255,9 +225,20 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC1/AHCSITC1-spec.md)." + "required_make_targets": [ + "triad-code-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC1-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" ], + "status": "pending", + "type": "code", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-code" + }, + { "ac_ids": [ "AC-AHCSITC1-01", "AC-AHCSITC1-02", @@ -266,52 +247,52 @@ "AC-AHCSITC1-05", "AC-AHCSITC1-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC1-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" + "acceptance_criteria": [ + "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC1/AHCSITC1-spec.md)." + ], + "concurrent_with": [ + "AHCSITC1-code" + ], + "depends_on": [ + "AHCSITC0-integ" ], + "description": "Add tests that enforce the AHCSITC1 session protocol and placement behaviors.", "end_checklist": [ "cargo fmt", "Run the targeted tests you add or touch.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC1-test\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-test", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-test", + "id": "AHCSITC1-test", "integration_task": "AHCSITC1-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/kickoff_prompts/AHCSITC1-test.md", - "depends_on": [ - "AHCSITC0-integ" + "name": "AHCSITC1 slice (test)", + "phase": "AHCSITC1", + "references": [ + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/AHCSITC1-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", + "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "concurrent_with": [ - "AHCSITC1-code" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-test", "required_make_targets": [ "triad-test-checks" - ] - }, - { - "id": "AHCSITC1-integ", - "name": "AHCSITC1 slice (integration final)", - "type": "integration", - "phase": "AHCSITC1", - "status": "pending", - "description": "Finalize AHCSITC1 after the code and test branches merge cleanly.", - "references": [ - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/AHCSITC1-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", - "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "acceptance_criteria": [ - "Slice is green under make integ-checks and implements the behaviors required by ac_ids." + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC1-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" ], + "status": "pending", + "type": "test", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-test" + }, + { "ac_ids": [ "AC-AHCSITC1-01", "AC-AHCSITC1-02", @@ -320,12 +301,15 @@ "AC-AHCSITC1-05", "AC-AHCSITC1-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC1-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC1-integ\"" + "acceptance_criteria": [ + "Slice is green under make integ-checks and implements the behaviors required by ac_ids." ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC1-code", + "AHCSITC1-test" + ], + "description": "Finalize AHCSITC1 after the code and test branches merge cleanly.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", @@ -334,40 +318,37 @@ "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC1-integ\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-integ", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-integ", + "id": "AHCSITC1-integ", "integration_task": "AHCSITC1-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/kickoff_prompts/AHCSITC1-integ.md", - "depends_on": [ - "AHCSITC1-code", - "AHCSITC1-test" - ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-integ", - "required_make_targets": [ - "integ-checks" - ], - "merge_to_orchestration": true - }, - { - "id": "AHCSITC2-code", - "name": "AHCSITC2 slice (code)", - "type": "code", - "phase": "AHCSITC2", - "status": "pending", - "description": "Implement the AHCSITC2 fail-closed policy and telemetry publication behaviors.", + "merge_to_orchestration": true, + "name": "AHCSITC1 slice (integration final)", + "phase": "AHCSITC1", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/AHCSITC2-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/AHCSITC1-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", - "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", + "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "acceptance_criteria": [ - "Implements the behaviors required by ac_ids (see slices/AHCSITC2/AHCSITC2-spec.md)." + "required_make_targets": [ + "integ-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC1-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC1-integ\"" ], + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-integ" + }, + { "ac_ids": [ "AC-AHCSITC2-01", "AC-AHCSITC2-02", @@ -376,39 +357,28 @@ "AC-AHCSITC2-05", "AC-AHCSITC2-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" + "acceptance_criteria": [ + "Implements the behaviors required by ac_ids (see slices/AHCSITC2/AHCSITC2-spec.md)." + ], + "concurrent_with": [ + "AHCSITC2-test" + ], + "depends_on": [ + "AHCSITC1-integ" ], + "description": "Implement the AHCSITC2 fail-closed policy and telemetry publication behaviors.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-code\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-code", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-code", + "id": "AHCSITC2-code", "integration_task": "AHCSITC2-integ-core", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-code.md", - "depends_on": [ - "AHCSITC1-integ" - ], - "concurrent_with": [ - "AHCSITC2-test" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-code", - "required_make_targets": [ - "triad-code-checks" - ] - }, - { - "id": "AHCSITC2-test", - "name": "AHCSITC2 slice (test)", - "type": "test", + "name": "AHCSITC2 slice (code)", "phase": "AHCSITC2", - "status": "pending", - "description": "Add tests that enforce the AHCSITC2 fail-closed policy and telemetry publication behaviors.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -419,9 +389,20 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC2/AHCSITC2-spec.md)." + "required_make_targets": [ + "triad-code-checks" ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" + ], + "status": "pending", + "type": "code", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-code" + }, + { "ac_ids": [ "AC-AHCSITC2-01", "AC-AHCSITC2-02", @@ -430,39 +411,28 @@ "AC-AHCSITC2-05", "AC-AHCSITC2-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" + "acceptance_criteria": [ + "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC2/AHCSITC2-spec.md)." + ], + "concurrent_with": [ + "AHCSITC2-code" + ], + "depends_on": [ + "AHCSITC1-integ" ], + "description": "Add tests that enforce the AHCSITC2 fail-closed policy and telemetry publication behaviors.", "end_checklist": [ "cargo fmt", "Run the targeted tests you add or touch.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-test\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-test", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-test", + "id": "AHCSITC2-test", "integration_task": "AHCSITC2-integ-core", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-test.md", - "depends_on": [ - "AHCSITC1-integ" - ], - "concurrent_with": [ - "AHCSITC2-code" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-test", - "required_make_targets": [ - "triad-test-checks" - ] - }, - { - "id": "AHCSITC2-integ-core", - "name": "AHCSITC2 slice (integration core)", - "type": "integration", + "name": "AHCSITC2 slice (test)", "phase": "AHCSITC2", - "status": "pending", - "description": "Integrate AHCSITC2 implementation and make it green on the primary development platform before CP1.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -470,20 +440,33 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/AHCSITC2-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", - "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", + "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Local integration gates are green for AHCSITC2 (fmt, clippy, tests, integ-checks).", - "Core integration branch is ready for checkpoint validation (`CP1-ci-checkpoint`)." + "required_make_targets": [ + "triad-test-checks" ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-core\"" + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" + ], + "status": "pending", + "type": "test", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-test" + }, + { + "acceptance_criteria": [ + "Local integration gates are green for AHCSITC2 (fmt, clippy, tests, integ-checks).", + "Core integration branch is ready for checkpoint validation (`CP1-ci-checkpoint`)." ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC2-code", + "AHCSITC2-test" + ], + "description": "Integrate AHCSITC2 implementation and make it green on the primary development platform before CP1.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", @@ -493,114 +476,100 @@ "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ-core\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-core", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-core", + "id": "AHCSITC2-integ-core", "integration_task": "AHCSITC2-integ-core", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ-core.md", - "depends_on": [ - "AHCSITC2-code", - "AHCSITC2-test" + "merge_to_orchestration": false, + "name": "AHCSITC2 slice (integration core)", + "phase": "AHCSITC2", + "references": [ + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/AHCSITC2-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", + "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-core", "required_make_targets": [ "integ-checks" ], - "merge_to_orchestration": false + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-core\"" + ], + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-core" }, { - "id": "CP1-ci-checkpoint", - "name": "CP1 CI checkpoint (cross-platform parity)", - "type": "ops", - "phase": "CP1", - "status": "pending", - "description": "Run the cross-platform compile-parity gate for the checkpoint group ending at AHCSITC2.", - "references": [ - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", - "docs/project_management/system/standards/ci/PLANNING_CI_CHECKPOINT_STANDARD.md" - ], "acceptance_criteria": [ "The checkpoint audit trail is recorded in session_log.md.", "Compile parity is green for the checkpoint checkout SHA.", "Windows remains a compile-parity checkpoint lane only; no Windows feature-smoke run is required.", "The task only validates the checkpoint boundary defined by pre-planning/ci_checkpoint_plan.md." ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read pre-planning/ci_checkpoint_plan.md, tasks.json, session_log.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs." + "concurrent_with": [], + "depends_on": [ + "AHCSITC2-integ-core" ], + "description": "Run the cross-platform compile-parity gate for the checkpoint group ending at AHCSITC2.", "end_checklist": [ "Dispatch compile parity for the checkpoint checkout SHA and record run ids/URLs.", "Record any checkpoint blockers or required platform follow-ups in session_log.md.", "Do not dispatch or require Windows feature smoke for this checkpoint.", "Set status to completed; add END entry; commit docs." ], - "worktree": null, + "id": "CP1-ci-checkpoint", "integration_task": null, "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md", - "depends_on": [ - "AHCSITC2-integ-core" - ], - "concurrent_with": [] - }, - { - "id": "AHCSITC2-integ-linux", - "name": "AHCSITC2 slice (integration Linux parity)", - "type": "integration", - "phase": "AHCSITC2", - "status": "pending", - "description": "Linux platform-fix integration task for AHCSITC2 parity failures.", + "name": "CP1 CI checkpoint (cross-platform parity)", + "phase": "CP1", "references": [ - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/AHCSITC2-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", + "docs/project_management/system/standards/ci/PLANNING_CI_CHECKPOINT_STANDARD.md" ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read pre-planning/ci_checkpoint_plan.md, tasks.json, session_log.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs." + ], + "status": "pending", + "type": "ops", + "worktree": null + }, + { "acceptance_criteria": [ "Linux compile and test parity is green for AHCSITC2." ], - "start_checklist": [ - "Run on a Linux host if possible.", - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-linux\"" + "concurrent_with": [], + "depends_on": [ + "AHCSITC2-integ-core", + "CP1-ci-checkpoint" ], + "description": "Linux platform-fix integration task for AHCSITC2 parity failures.", "end_checklist": [ "Run Linux-local parity gates (fmt, clippy, relevant tests) and capture outputs.", "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ-linux\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-linux", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-linux", + "id": "AHCSITC2-integ-linux", "integration_task": "AHCSITC2-integ-linux", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ-linux.md", - "depends_on": [ - "AHCSITC2-integ-core", - "CP1-ci-checkpoint" - ], - "concurrent_with": [], - "platform": "linux", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-linux", - "required_make_targets": [ - "triad-code-checks" - ], - "merge_to_orchestration": false - }, - { - "id": "AHCSITC2-integ-macos", - "name": "AHCSITC2 slice (integration macOS parity)", - "type": "integration", + "merge_to_orchestration": false, + "name": "AHCSITC2 slice (integration Linux parity)", "phase": "AHCSITC2", - "status": "pending", - "description": "macOS platform-fix integration task for AHCSITC2 parity failures.", + "platform": "linux", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -611,44 +580,44 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "macOS compile and test parity is green for AHCSITC2." + "required_make_targets": [ + "triad-code-checks" ], "start_checklist": [ - "Run on a macOS host if possible.", + "Run on a Linux host if possible.", "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-macos\"" + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-linux\"" ], + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-linux" + }, + { + "acceptance_criteria": [ + "macOS compile and test parity is green for AHCSITC2." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC2-integ-core", + "CP1-ci-checkpoint" + ], + "description": "macOS platform-fix integration task for AHCSITC2 parity failures.", "end_checklist": [ "Run macOS-local parity gates (fmt, clippy, relevant tests) and capture outputs.", "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ-macos\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-macos", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-macos", + "id": "AHCSITC2-integ-macos", "integration_task": "AHCSITC2-integ-macos", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ-macos.md", - "depends_on": [ - "AHCSITC2-integ-core", - "CP1-ci-checkpoint" - ], - "concurrent_with": [], - "platform": "macos", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-macos", - "required_make_targets": [ - "triad-code-checks" - ], - "merge_to_orchestration": false - }, - { - "id": "AHCSITC2-integ-windows", - "name": "AHCSITC2 slice (integration Windows parity)", - "type": "integration", + "merge_to_orchestration": false, + "name": "AHCSITC2 slice (integration macOS parity)", "phase": "AHCSITC2", - "status": "pending", - "description": "Windows platform-fix integration task for AHCSITC2 parity failures.", + "platform": "macos", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -659,44 +628,44 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "Windows compile and test parity is green for AHCSITC2." + "required_make_targets": [ + "triad-code-checks" ], "start_checklist": [ - "Run on a Windows host if possible.", + "Run on a macOS host if possible.", "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-windows\"" + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-macos\"" + ], + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-macos" + }, + { + "acceptance_criteria": [ + "Windows compile and test parity is green for AHCSITC2." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC2-integ-core", + "CP1-ci-checkpoint" ], + "description": "Windows platform-fix integration task for AHCSITC2 parity failures.", "end_checklist": [ "Run Windows-local parity gates (fmt, clippy, relevant tests) and capture outputs.", "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ-windows\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-windows", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-windows", + "id": "AHCSITC2-integ-windows", "integration_task": "AHCSITC2-integ-windows", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ-windows.md", - "depends_on": [ - "AHCSITC2-integ-core", - "CP1-ci-checkpoint" - ], - "concurrent_with": [], - "platform": "windows", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-windows", - "required_make_targets": [ - "triad-code-checks" - ], - "merge_to_orchestration": false - }, - { - "id": "AHCSITC2-integ", - "name": "AHCSITC2 slice (integration final)", - "type": "integration", + "merge_to_orchestration": false, + "name": "AHCSITC2 slice (integration Windows parity)", "phase": "AHCSITC2", - "status": "pending", - "description": "Finalize AHCSITC2 across required CI parity platforms.", + "platform": "windows", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -705,14 +674,23 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", - "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "All required CI parity runs for AHCSITC2 are green.", - "Any platform-specific follow-ups are merged and reflected in session_log.md.", - "The final merged state implements the behaviors required by ac_ids." + "required_make_targets": [ + "triad-code-checks" + ], + "start_checklist": [ + "Run on a Windows host if possible.", + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-windows\"" ], + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-windows" + }, + { "ac_ids": [ "AC-AHCSITC2-01", "AC-AHCSITC2-02", @@ -721,12 +699,20 @@ "AC-AHCSITC2-05", "AC-AHCSITC2-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ\"" + "acceptance_criteria": [ + "All required CI parity runs for AHCSITC2 are green.", + "Any platform-specific follow-ups are merged and reflected in session_log.md.", + "The final merged state implements the behaviors required by ac_ids." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC2-integ-core", + "CP1-ci-checkpoint", + "AHCSITC2-integ-linux", + "AHCSITC2-integ-macos", + "AHCSITC2-integ-windows" ], + "description": "Finalize AHCSITC2 across required CI parity platforms.", "end_checklist": [ "Merge platform-fix branches (if any) and resolve conflicts.", "cargo fmt", @@ -736,43 +722,38 @@ "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ", + "id": "AHCSITC2-integ", "integration_task": "AHCSITC2-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ.md", - "depends_on": [ - "AHCSITC2-integ-core", - "CP1-ci-checkpoint", - "AHCSITC2-integ-linux", - "AHCSITC2-integ-macos", - "AHCSITC2-integ-windows" - ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ", - "required_make_targets": [ - "integ-checks" - ], - "merge_to_orchestration": true - }, - { - "id": "AHCSITC3-code", - "name": "AHCSITC3 slice (code)", - "type": "code", - "phase": "AHCSITC3", - "status": "pending", - "description": "Implement the AHCSITC3 platform parity, compatibility, and validation behaviors.", + "merge_to_orchestration": true, + "name": "AHCSITC2 slice (integration final)", + "phase": "AHCSITC2", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/AHCSITC3-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/platform-parity-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/compatibility-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/AHCSITC2-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", - "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" + "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "acceptance_criteria": [ - "Implements the behaviors required by ac_ids (see slices/AHCSITC3/AHCSITC3-spec.md)." + "required_make_targets": [ + "integ-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ\"" ], + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ" + }, + { "ac_ids": [ "AC-AHCSITC3-01", "AC-AHCSITC3-02", @@ -781,40 +762,29 @@ "AC-AHCSITC3-05", "AC-AHCSITC3-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC3-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" + "acceptance_criteria": [ + "Implements the behaviors required by ac_ids (see slices/AHCSITC3/AHCSITC3-spec.md)." + ], + "concurrent_with": [ + "AHCSITC3-test" + ], + "depends_on": [ + "CP1-ci-checkpoint", + "AHCSITC2-integ" ], + "description": "Implement the AHCSITC3 platform parity, compatibility, and validation behaviors.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-code\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-code", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-code", + "id": "AHCSITC3-code", "integration_task": "AHCSITC3-integ-core", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-code.md", - "depends_on": [ - "CP1-ci-checkpoint", - "AHCSITC2-integ" - ], - "concurrent_with": [ - "AHCSITC3-test" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-code", - "required_make_targets": [ - "triad-code-checks" - ] - }, - { - "id": "AHCSITC3-test", - "name": "AHCSITC3 slice (test)", - "type": "test", + "name": "AHCSITC3 slice (code)", "phase": "AHCSITC3", - "status": "pending", - "description": "Add tests that enforce the AHCSITC3 platform parity, compatibility, and validation behaviors.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -825,9 +795,20 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC3/AHCSITC3-spec.md)." + "required_make_targets": [ + "triad-code-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC3-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" ], + "status": "pending", + "type": "code", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-code" + }, + { "ac_ids": [ "AC-AHCSITC3-01", "AC-AHCSITC3-02", @@ -836,40 +817,29 @@ "AC-AHCSITC3-05", "AC-AHCSITC3-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC3-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" + "acceptance_criteria": [ + "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC3/AHCSITC3-spec.md)." + ], + "concurrent_with": [ + "AHCSITC3-code" + ], + "depends_on": [ + "CP1-ci-checkpoint", + "AHCSITC2-integ" ], + "description": "Add tests that enforce the AHCSITC3 platform parity, compatibility, and validation behaviors.", "end_checklist": [ "cargo fmt", "Run the targeted tests you add or touch.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-test\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-test", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-test", + "id": "AHCSITC3-test", "integration_task": "AHCSITC3-integ-core", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-test.md", - "depends_on": [ - "CP1-ci-checkpoint", - "AHCSITC2-integ" - ], - "concurrent_with": [ - "AHCSITC3-code" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-test", - "required_make_targets": [ - "triad-test-checks" - ] - }, - { - "id": "AHCSITC3-integ-core", - "name": "AHCSITC3 slice (integration core)", - "type": "integration", + "name": "AHCSITC3 slice (test)", "phase": "AHCSITC3", - "status": "pending", - "description": "Integrate AHCSITC3 implementation and make it green on the primary development platform before CP2.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -877,20 +847,33 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/AHCSITC3-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/platform-parity-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/compatibility-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", - "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" + "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Local integration gates are green for AHCSITC3 (fmt, clippy, tests, integ-checks).", - "Core integration branch is ready for checkpoint validation (`CP2-ci-checkpoint`)." + "required_make_targets": [ + "triad-test-checks" ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Read plan.md, tasks.json, session_log.md, AHCSITC3-spec.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-core\"" + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" + ], + "status": "pending", + "type": "test", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-test" + }, + { + "acceptance_criteria": [ + "Local integration gates are green for AHCSITC3 (fmt, clippy, tests, integ-checks).", + "Core integration branch is ready for checkpoint validation (`CP2-ci-checkpoint`)." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC3-code", + "AHCSITC3-test" ], + "description": "Integrate AHCSITC3 implementation and make it green on the primary development platform before CP2.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", @@ -900,66 +883,100 @@ "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-core\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-core", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-core", + "id": "AHCSITC3-integ-core", "integration_task": "AHCSITC3-integ-core", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-core.md", - "depends_on": [ - "AHCSITC3-code", - "AHCSITC3-test" + "merge_to_orchestration": false, + "name": "AHCSITC3 slice (integration core)", + "phase": "AHCSITC3", + "references": [ + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/AHCSITC3-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/platform-parity-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/compatibility-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", + "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-core", "required_make_targets": [ "integ-checks" ], - "merge_to_orchestration": false - }, - { - "id": "CP2-ci-checkpoint", - "name": "CP2 CI checkpoint (cross-platform parity)", - "type": "ops", - "phase": "CP2", - "status": "pending", - "description": "Run the cross-platform compile-parity gate for the checkpoint group ending at AHCSITC3.", - "references": [ - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", - "docs/project_management/system/standards/ci/PLANNING_CI_CHECKPOINT_STANDARD.md" + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC3-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-core\"" ], + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-core" + }, + { "acceptance_criteria": [ "The checkpoint audit trail is recorded in session_log.md.", "Compile parity is green for the checkpoint checkout SHA.", "Windows remains a compile-parity checkpoint lane only; no Windows feature-smoke run is required.", "The task only validates the checkpoint boundary defined by pre-planning/ci_checkpoint_plan.md." ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read pre-planning/ci_checkpoint_plan.md, tasks.json, session_log.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs." + "concurrent_with": [], + "depends_on": [ + "AHCSITC3-integ-core" ], + "description": "Run the cross-platform compile-parity gate for the checkpoint group ending at AHCSITC3.", "end_checklist": [ "Dispatch compile parity for the checkpoint checkout SHA and record run ids/URLs.", "Record any checkpoint blockers or required platform follow-ups in session_log.md.", "Do not dispatch or require Windows feature smoke for this checkpoint.", "Set status to completed; add END entry; commit docs." ], - "worktree": null, + "id": "CP2-ci-checkpoint", "integration_task": null, "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md", - "depends_on": [ - "AHCSITC3-integ-core" + "name": "CP2 CI checkpoint (cross-platform parity)", + "phase": "CP2", + "references": [ + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", + "docs/project_management/system/standards/ci/PLANNING_CI_CHECKPOINT_STANDARD.md" ], - "concurrent_with": [] + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read pre-planning/ci_checkpoint_plan.md, tasks.json, session_log.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs." + ], + "status": "pending", + "type": "ops", + "worktree": null }, { + "acceptance_criteria": [ + "Linux compile and test parity is green for AHCSITC3." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC3-integ-core", + "CP2-ci-checkpoint" + ], + "description": "Linux platform-fix integration task for AHCSITC3 parity failures.", + "end_checklist": [ + "Run Linux-local parity gates (fmt, clippy, relevant tests) and capture outputs.", + "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-linux\"", + "Update tasks.json and session_log.md on the orchestration branch." + ], + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-linux", "id": "AHCSITC3-integ-linux", + "integration_task": "AHCSITC3-integ-linux", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-linux.md", + "merge_to_orchestration": false, "name": "AHCSITC3 slice (integration Linux parity)", - "type": "integration", "phase": "AHCSITC3", - "status": "pending", - "description": "Linux platform-fix integration task for AHCSITC3 parity failures.", + "platform": "linux", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -970,8 +987,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "Linux compile and test parity is green for AHCSITC3." + "required_make_targets": [ + "triad-code-checks" ], "start_checklist": [ "Run on a Linux host if possible.", @@ -980,34 +997,34 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-linux\"" ], - "end_checklist": [ - "Run Linux-local parity gates (fmt, clippy, relevant tests) and capture outputs.", - "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-linux\"", - "Update tasks.json and session_log.md on the orchestration branch." + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-linux" + }, + { + "acceptance_criteria": [ + "macOS compile and test parity is green for AHCSITC3." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-linux", - "integration_task": "AHCSITC3-integ-linux", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-linux.md", + "concurrent_with": [], "depends_on": [ "AHCSITC3-integ-core", "CP2-ci-checkpoint" ], - "concurrent_with": [], - "platform": "linux", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-linux", - "required_make_targets": [ - "triad-code-checks" + "description": "macOS platform-fix integration task for AHCSITC3 parity failures.", + "end_checklist": [ + "Run macOS-local parity gates (fmt, clippy, relevant tests) and capture outputs.", + "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-macos\"", + "Update tasks.json and session_log.md on the orchestration branch." ], - "merge_to_orchestration": false - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-macos", "id": "AHCSITC3-integ-macos", + "integration_task": "AHCSITC3-integ-macos", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-macos.md", + "merge_to_orchestration": false, "name": "AHCSITC3 slice (integration macOS parity)", - "type": "integration", "phase": "AHCSITC3", - "status": "pending", - "description": "macOS platform-fix integration task for AHCSITC3 parity failures.", + "platform": "macos", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -1018,8 +1035,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "macOS compile and test parity is green for AHCSITC3." + "required_make_targets": [ + "triad-code-checks" ], "start_checklist": [ "Run on a macOS host if possible.", @@ -1028,34 +1045,34 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-macos\"" ], - "end_checklist": [ - "Run macOS-local parity gates (fmt, clippy, relevant tests) and capture outputs.", - "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-macos\"", - "Update tasks.json and session_log.md on the orchestration branch." + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-macos" + }, + { + "acceptance_criteria": [ + "Windows compile and test parity is green for AHCSITC3." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-macos", - "integration_task": "AHCSITC3-integ-macos", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-macos.md", + "concurrent_with": [], "depends_on": [ "AHCSITC3-integ-core", "CP2-ci-checkpoint" ], - "concurrent_with": [], - "platform": "macos", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-macos", - "required_make_targets": [ - "triad-code-checks" + "description": "Windows platform-fix integration task for AHCSITC3 parity failures.", + "end_checklist": [ + "Run Windows-local parity gates (fmt, clippy, relevant tests) and capture outputs.", + "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-windows\"", + "Update tasks.json and session_log.md on the orchestration branch." ], - "merge_to_orchestration": false - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-windows", "id": "AHCSITC3-integ-windows", + "integration_task": "AHCSITC3-integ-windows", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-windows.md", + "merge_to_orchestration": false, "name": "AHCSITC3 slice (integration Windows parity)", - "type": "integration", "phase": "AHCSITC3", - "status": "pending", - "description": "Windows platform-fix integration task for AHCSITC3 parity failures.", + "platform": "windows", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -1066,8 +1083,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "Windows compile and test parity is green for AHCSITC3." + "required_make_targets": [ + "triad-code-checks" ], "start_checklist": [ "Run on a Windows host if possible.", @@ -1076,34 +1093,49 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-windows\"" ], - "end_checklist": [ - "Run Windows-local parity gates (fmt, clippy, relevant tests) and capture outputs.", - "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-windows\"", - "Update tasks.json and session_log.md on the orchestration branch." + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-windows" + }, + { + "ac_ids": [ + "AC-AHCSITC3-01", + "AC-AHCSITC3-02", + "AC-AHCSITC3-03", + "AC-AHCSITC3-04", + "AC-AHCSITC3-05", + "AC-AHCSITC3-06" ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-windows", - "integration_task": "AHCSITC3-integ-windows", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-windows.md", + "acceptance_criteria": [ + "All required CI parity runs for AHCSITC3 are green.", + "Any platform-specific follow-ups are merged and reflected in session_log.md.", + "The final merged state implements the behaviors required by ac_ids." + ], + "concurrent_with": [], "depends_on": [ "AHCSITC3-integ-core", - "CP2-ci-checkpoint" + "CP2-ci-checkpoint", + "AHCSITC3-integ-linux", + "AHCSITC3-integ-macos", + "AHCSITC3-integ-windows" ], - "concurrent_with": [], - "platform": "windows", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-windows", - "required_make_targets": [ - "triad-code-checks" + "description": "Finalize AHCSITC3 across required CI parity platforms.", + "end_checklist": [ + "Merge platform-fix branches (if any) and resolve conflicts.", + "cargo fmt", + "cargo clippy --workspace --all-targets -- -D warnings", + "Run relevant tests.", + "make integ-checks", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ\"", + "Update tasks.json and session_log.md on the orchestration branch." ], - "merge_to_orchestration": false - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ", "id": "AHCSITC3-integ", + "integration_task": "AHCSITC3-integ", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ.md", + "merge_to_orchestration": true, "name": "AHCSITC3 slice (integration final)", - "type": "integration", "phase": "AHCSITC3", - "status": "pending", - "description": "Finalize AHCSITC3 across required CI parity platforms.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -1115,18 +1147,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "acceptance_criteria": [ - "All required CI parity runs for AHCSITC3 are green.", - "Any platform-specific follow-ups are merged and reflected in session_log.md.", - "The final merged state implements the behaviors required by ac_ids." - ], - "ac_ids": [ - "AC-AHCSITC3-01", - "AC-AHCSITC3-02", - "AC-AHCSITC3-03", - "AC-AHCSITC3-04", - "AC-AHCSITC3-05", - "AC-AHCSITC3-06" + "required_make_targets": [ + "integ-checks" ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", @@ -1134,39 +1156,31 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ\"" ], - "end_checklist": [ - "Merge platform-fix branches (if any) and resolve conflicts.", - "cargo fmt", - "cargo clippy --workspace --all-targets -- -D warnings", - "Run relevant tests.", - "make integ-checks", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ\"", - "Update tasks.json and session_log.md on the orchestration branch." + "status": "pending", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ" + }, + { + "acceptance_criteria": [ + "Feature directory docs remain consistent after cleanup.", + "All temporary worktrees are removed.", + "The cleanup summary is recorded in session_log.md." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ", - "integration_task": "AHCSITC3-integ", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ.md", + "concurrent_with": [], "depends_on": [ - "AHCSITC3-integ-core", - "CP2-ci-checkpoint", - "AHCSITC3-integ-linux", - "AHCSITC3-integ-macos", - "AHCSITC3-integ-windows" + "AHCSITC3-integ" ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ", - "required_make_targets": [ - "integ-checks" + "description": "Close out the feature task graph and remove retained worktrees.", + "end_checklist": [ + "Run: make triad-feature-cleanup FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" DRY_RUN=1 REMOVE_WORKTREES=1 PRUNE_LOCAL=1", + "Then run: make triad-feature-cleanup FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" REMOVE_WORKTREES=1 PRUNE_LOCAL=1", + "Set status to completed; add END entry with the cleanup summary; commit docs." ], - "merge_to_orchestration": true - }, - { "id": "FZ-feature-cleanup", + "integration_task": null, + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/FZ-feature-cleanup.md", "name": "Feature cleanup (worktrees + branches)", - "type": "ops", "phase": "FZ", - "status": "pending", - "description": "Close out the feature task graph and remove retained worktrees.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -1175,28 +1189,14 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/system/standards/triad/TASK_TRIADS_AND_FEATURE_SETUP.md" ], - "acceptance_criteria": [ - "Feature directory docs remain consistent after cleanup.", - "All temporary worktrees are removed.", - "The cleanup summary is recorded in session_log.md." - ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Confirm all tasks are completed and merged as intended.", "Set status to in_progress; add START entry; commit docs." ], - "end_checklist": [ - "Run: make triad-feature-cleanup FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" DRY_RUN=1 REMOVE_WORKTREES=1 PRUNE_LOCAL=1", - "Then run: make triad-feature-cleanup FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" REMOVE_WORKTREES=1 PRUNE_LOCAL=1", - "Set status to completed; add END entry with the cleanup summary; commit docs." - ], - "worktree": null, - "integration_task": null, - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/FZ-feature-cleanup.md", - "depends_on": [ - "AHCSITC3-integ" - ], - "concurrent_with": [] + "status": "pending", + "type": "ops", + "worktree": null } ] } From c8d7c9488066305762d8d4c000c3f3835c0c3207 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 21:27:03 -0400 Subject: [PATCH 03/48] test: add AHCSITC0 successor contract coverage --- .../agent_successor_contract_ahcsitc0.rs | 462 ++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 crates/shell/tests/agent_successor_contract_ahcsitc0.rs diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs new file mode 100644 index 000000000..e1a4a4943 --- /dev/null +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -0,0 +1,462 @@ +#![cfg(unix)] + +mod common; + +use common::{substrate_shell_driver, temp_dir}; +use serde_json::Value; +use std::fs; +use std::path::PathBuf; +use std::process::Output; +use tempfile::TempDir; + +struct AgentSuccessorFixture { + _temp: TempDir, + home: PathBuf, + substrate_home: PathBuf, + workspace_root: PathBuf, +} + +impl AgentSuccessorFixture { + fn new() -> Self { + let temp = temp_dir("substrate-agent-successor-"); + let home = temp.path().join("home"); + fs::create_dir_all(&home).expect("failed to create HOME fixture"); + let substrate_home = temp.path().join("substrate-home"); + fs::create_dir_all(&substrate_home).expect("failed to create SUBSTRATE_HOME fixture"); + let workspace_root = temp.path().join("workspace"); + fs::create_dir_all(&workspace_root).expect("failed to create workspace root"); + Self { + _temp: temp, + home, + substrate_home, + workspace_root, + } + } + + fn command(&self) -> assert_cmd::Command { + let mut cmd = substrate_shell_driver(); + cmd.env("HOME", &self.home) + .env("USERPROFILE", &self.home) + .env("SUBSTRATE_HOME", &self.substrate_home); + cmd + } + + fn init_workspace(&self) { + let output = self + .command() + .arg("workspace") + .arg("init") + .arg(&self.workspace_root) + .arg("--force") + .output() + .expect("failed to run workspace init"); + assert!( + output.status.success(), + "workspace init should succeed: {output:?}" + ); + } + + fn write_global_config_patch(&self, contents: &str) { + fs::write(self.substrate_home.join("config.yaml"), contents) + .expect("failed to write config.yaml"); + } + + fn write_global_policy_patch(&self, contents: &str) { + fs::write(self.substrate_home.join("policy.yaml"), contents) + .expect("failed to write policy.yaml"); + } + + fn write_agent_file(&self, file_name: &str, contents: &str) { + let agents_dir = self.substrate_home.join("agents"); + fs::create_dir_all(&agents_dir).expect("failed to create agents directory"); + fs::write(agents_dir.join(file_name), contents).expect("failed to write agent file"); + } + + fn run(&self, args: &[&str]) -> Output { + self.command() + .current_dir(&self.workspace_root) + .args(args) + .output() + .expect("failed to run substrate command") + } + + fn seed_inventory_for_list_and_status_contracts(&self) { + self.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + self.write_global_policy_patch( + r#"agents: + allowed_backends: + - cli:claude_code + - cli:codex +"#, + ); + self.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + self.write_agent_file( + "codex.yaml", + &cli_agent_file("codex", "world", true, false, true), + ); + } + + fn seed_inventory_for_doctor_contract(&self) { + self.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + self.write_global_policy_patch( + r#"agents: + allowed_backends: + - cli:claude_code + - cli:helper +"#, + ); + self.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + self.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + } +} + +fn cli_agent_file( + agent_id: &str, + scope: &str, + llm: bool, + mcp_client: bool, + enabled: bool, +) -> String { + format!( + "version: 1\nid: {agent_id}\nconfig:\n kind: cli\n enabled: {enabled}\n execution:\n scope: {scope}\n cli:\n binary: {agent_id}\n mode: persistent\n capabilities:\n llm: {llm}\n mcp_client: {mcp_client}\n" + ) +} + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .expect("repo root should exist") +} + +fn parse_json_output(output: &Output) -> Value { + serde_json::from_slice(&output.stdout).expect("stdout should be valid JSON") +} + +#[test] +fn plural_agents_namespace_keeps_validate_as_the_only_compatibility_leaf() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_agent_file( + "codex.yaml", + &cli_agent_file("codex", "world", true, false, true), + ); + + let validate = fixture.run(&["agents", "validate"]); + assert!( + validate.status.success(), + "substrate agents validate must remain supported: {validate:?}" + ); + + for alias in [ + ["agents", "list"], + ["agents", "status"], + ["agents", "doctor"], + ] { + let output = fixture.run(&alias); + assert_eq!( + output.status.code(), + Some(2), + "`substrate {}` must remain an invalid plural alias: {output:?}", + alias.join(" ") + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("unrecognized subcommand") + || stderr.contains("unexpected argument") + || stderr.contains("Usage:"), + "plural alias failure should be a CLI usage error\nstderr: {stderr}" + ); + } +} + +#[test] +fn agent_list_json_locks_backend_id_derivation_role_and_omission_rules() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + + let output = fixture.run(&["agent", "list", "--json"]); + assert!( + output.status.success(), + "substrate agent list --json should succeed for a valid successor fixture: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!( + json.get("disabled").and_then(Value::as_bool), + Some(false), + "list output must report agents enabled: {json}" + ); + assert_eq!( + json.pointer("/scope_filter").and_then(Value::as_str), + Some("any"), + "default list scope filter must be `any`: {json}" + ); + assert!( + json.pointer("/role_filter") + .is_some_and(serde_json::Value::is_null), + "list output must publish a null role filter when --role is absent: {json}" + ); + + let agents = json["agents"] + .as_array() + .expect("agents should be an array"); + assert_eq!( + agents.len(), + 2, + "fixture should produce exactly two agents: {json}" + ); + + let orchestrator = agents + .iter() + .find(|agent| agent.pointer("/agent_id").and_then(Value::as_str) == Some("claude_code")) + .expect("orchestrator row should exist"); + assert_eq!( + orchestrator.pointer("/backend_id").and_then(Value::as_str), + Some("cli:claude_code"), + "backend_id must be derived as :: {orchestrator}" + ); + assert_eq!( + orchestrator.pointer("/role").and_then(Value::as_str), + Some("orchestrator"), + "role must remain a separate field from backend_id: {orchestrator}" + ); + assert_eq!( + orchestrator + .pointer("/execution/scope") + .and_then(Value::as_str), + Some("host") + ); + assert_eq!( + orchestrator + .pointer("/capabilities_summary/llm") + .and_then(Value::as_bool), + Some(true) + ); + assert_eq!( + orchestrator + .pointer("/capabilities_summary/mcp_client") + .and_then(Value::as_bool), + Some(true) + ); + assert_eq!( + orchestrator.pointer("/protocol").and_then(Value::as_str), + Some("uaa.agent.session") + ); + + let member = agents + .iter() + .find(|agent| agent.pointer("/agent_id").and_then(Value::as_str) == Some("codex")) + .expect("member row should exist"); + assert_eq!( + member.pointer("/backend_id").and_then(Value::as_str), + Some("cli:codex"), + "member backend_id must be derived as :: {member}" + ); + assert!( + member + .pointer("/role") + .is_some_and(serde_json::Value::is_null), + "non-orchestrator rows must keep role separate and unassigned: {member}" + ); + assert_eq!( + member.pointer("/execution/scope").and_then(Value::as_str), + Some("world") + ); + + for agent in agents { + for forbidden in ["provider", "auth_authority", "world_id", "world_generation"] { + assert!( + agent.get(forbidden).is_none(), + "agent list rows must omit `{forbidden}`: {agent}" + ); + } + } +} + +#[test] +fn agent_status_json_uses_locked_top_level_field_names() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "substrate agent status --json should succeed for a valid successor fixture: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!( + json.get("disabled").and_then(Value::as_bool), + Some(false), + "status output must report agents enabled: {json}" + ); + assert_eq!( + json.pointer("/scope_filter").and_then(Value::as_str), + Some("any"), + "default status scope filter must be `any`: {json}" + ); + assert!( + json.pointer("/role_filter") + .is_some_and(serde_json::Value::is_null), + "status output must publish a null role filter when --role is absent: {json}" + ); + assert_eq!( + json.pointer("/orchestrator_agent_id") + .and_then(Value::as_str), + Some("claude_code"), + "status output must report orchestrator_agent_id as the selected inventory id: {json}" + ); + assert!( + json.get("sessions").is_some_and(Value::is_array), + "status output must expose `sessions` as an array: {json}" + ); + assert!( + json.get("nested_llm_records").is_some_and(Value::is_array), + "status output must expose `nested_llm_records` as an array: {json}" + ); +} + +#[test] +fn agent_doctor_json_locks_field_names_omissions_and_check_order() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_doctor_contract(); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert!( + output.status.success(), + "substrate agent doctor --json should succeed for a valid host-only successor fixture: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!( + json.get("healthy").and_then(Value::as_bool), + Some(true), + "doctor should report a healthy control plane for the host-only fixture: {json}" + ); + assert_eq!( + json.get("fail_closed").and_then(Value::as_bool), + Some(false), + "healthy doctor output must not mark fail_closed: {json}" + ); + assert_eq!( + json.pointer("/orchestrator/agent_id") + .and_then(Value::as_str), + Some("claude_code") + ); + assert_eq!( + json.pointer("/orchestrator/backend_id") + .and_then(Value::as_str), + Some("cli:claude_code"), + "doctor orchestrator summary must publish the derived backend_id: {json}" + ); + assert_eq!( + json.pointer("/orchestrator/execution/scope") + .and_then(Value::as_str), + Some("host"), + "doctor orchestrator summary must preserve execution.scope: {json}" + ); + assert!( + json.pointer("/orchestrator/provider").is_none(), + "doctor orchestrator summary must omit provider: {json}" + ); + assert!( + json.pointer("/orchestrator/auth_authority").is_none(), + "doctor orchestrator summary must omit auth_authority: {json}" + ); + + let checks = json["checks"] + .as_array() + .expect("checks should be an array"); + let observed: Vec<&str> = checks + .iter() + .map(|check| { + check + .pointer("/check") + .and_then(Value::as_str) + .expect("check id should be a string") + }) + .collect(); + assert_eq!( + observed, + vec![ + "inventory_scan", + "orchestrator_selection", + "policy_allowlist", + "world_boundary", + ], + "doctor checks must stay in the contract-locked order: {json}" + ); + + let statuses: Vec<&str> = checks + .iter() + .map(|check| { + check + .pointer("/status") + .and_then(Value::as_str) + .expect("check status should be a string") + }) + .collect(); + assert_eq!( + statuses, + vec!["pass", "pass", "pass", "not_applicable"], + "host-only doctor fixture should report a not_applicable world boundary after the three required passes: {json}" + ); +} + +#[test] +fn docs_usage_and_repo_boundary_match_the_successor_contract() { + let usage = fs::read_to_string(repo_root().join("docs/USAGE.md")) + .expect("docs/USAGE.md should be readable"); + assert!( + usage.contains("substrate agent list"), + "docs/USAGE.md must document the canonical singular list command" + ); + assert!( + usage.contains("substrate agent status"), + "docs/USAGE.md must document the canonical singular status command" + ); + assert!( + usage.contains("substrate agent doctor"), + "docs/USAGE.md must document the canonical singular doctor command" + ); + for forbidden in [ + "substrate agents list", + "substrate agents status", + "substrate agents doctor", + ] { + assert!( + !usage.contains(forbidden), + "docs/USAGE.md must not advertise plural successor aliases: {forbidden}" + ); + } + + assert!( + !repo_root().join("crates/agent-hub").exists(), + "AHCSITC0 must not introduce a new crates/agent-hub package" + ); +} From d7d13845a891e03ec3e5d171442d1dbeac781679 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 21:31:34 -0400 Subject: [PATCH 04/48] docs: allow AHCSITC0 task touches --- .../pre-planning/impact_map.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md index 718d229a5..0a59b8608 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md @@ -39,7 +39,8 @@ Strict packs (`tasks.json` → `meta.slice_spec_version >= 2`) requirements: - To compute pack-derived Work Lift v1 from this Touch Set: `make pm-lift-pack PACK="docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible"` (strict packs only). ### Create -- None +- `crates/common/src/agent_identity.rs` +- `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` ### Edit - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md` @@ -80,12 +81,15 @@ Strict packs (`tasks.json` → `meta.slice_spec_version >= 2`) requirements: - `crates/shell/src/execution/agent_events.rs` - `crates/shell/src/execution/routing/telemetry.rs` - `crates/common/src/agent_events.rs` +- `crates/common/src/lib.rs` - `crates/common/tests/agent_hub_event_envelope_schema.rs` - `crates/trace/src/span.rs` - `crates/trace/src/tests.rs` - `crates/agent-api-types/src/lib.rs` - `crates/agent-api-client/src/lib.rs` - `crates/agent-api-core/src/lib.rs` +- `crates/shell/src/execution/invocation/plan.rs` +- `crates/shell/src/execution/mod.rs` - `crates/shell/tests/agents_validate.rs` - `crates/shell/tests/agent_hub_trace_persistence.rs` - `crates/shell/tests/repl_world_first_routing_v1.rs` From e8801afac6f568a3855c158d6547cebcb18663b4 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 21:35:21 -0400 Subject: [PATCH 05/48] task: AHCSITC0-code --- crates/common/src/agent_identity.rs | 3 + crates/common/src/lib.rs | 2 + crates/shell/src/execution/agent_inventory.rs | 56 +- crates/shell/src/execution/agents_cmd.rs | 1028 ++++++++++++++++- crates/shell/src/execution/cli.rs | 61 + crates/shell/src/execution/invocation/plan.rs | 12 +- crates/shell/src/execution/mod.rs | 1 + docs/USAGE.md | 23 + 8 files changed, 1162 insertions(+), 24 deletions(-) create mode 100644 crates/common/src/agent_identity.rs diff --git a/crates/common/src/agent_identity.rs b/crates/common/src/agent_identity.rs new file mode 100644 index 000000000..36909175a --- /dev/null +++ b/crates/common/src/agent_identity.rs @@ -0,0 +1,3 @@ +pub fn derive_agent_backend_id(kind: &str, agent_id: &str) -> String { + format!("{}:{}", kind.trim(), agent_id.trim()) +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 5a3086ab0..02515de34 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; pub mod agent_events; +pub mod agent_identity; pub mod fs_diff; pub mod identity; pub mod manager_manifest; @@ -12,6 +13,7 @@ pub mod settings; pub mod world_exec_guard; pub use agent_events::{AgentEvent, AgentEventKind}; +pub use agent_identity::derive_agent_backend_id; pub use fs_diff::FsDiff; pub use identity::{ validate_identity_tuple_and_placement_posture, IdentityTuple, PlacementExecution, diff --git a/crates/shell/src/execution/agent_inventory.rs b/crates/shell/src/execution/agent_inventory.rs index dc66d4372..8c42539ce 100644 --- a/crates/shell/src/execution/agent_inventory.rs +++ b/crates/shell/src/execution/agent_inventory.rs @@ -7,6 +7,7 @@ use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; use substrate_broker::{validate_backend_id, Policy, WorldFsDenyEnforcement}; +use substrate_common::derive_agent_backend_id; use substrate_common::paths as substrate_paths; #[derive(Debug, Clone, Deserialize)] @@ -42,6 +43,15 @@ pub(crate) enum AgentConfigKind { Api, } +impl AgentConfigKind { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Cli => "cli", + Self::Api => "api", + } + } +} + #[derive(Debug, Clone, Default, Deserialize)] #[serde(default, deny_unknown_fields)] pub(crate) struct AgentExecutionConfigV1 { @@ -84,6 +94,35 @@ pub(crate) struct AgentInventoryEntryV1 { pub file: AgentFileV1, } +impl AgentFileV1 { + pub(crate) fn derived_backend_id(&self) -> String { + derive_agent_backend_id(self.config.kind.as_str(), &self.id) + } + + pub(crate) fn effective_scope( + &self, + effective_config: &crate::execution::config_model::SubstrateConfig, + ) -> crate::execution::config_model::AgentExecutionScope { + self.config + .execution + .scope + .unwrap_or(effective_config.agents.defaults.execution.scope) + } +} + +impl AgentInventoryEntryV1 { + pub(crate) fn derived_backend_id(&self) -> String { + self.file.derived_backend_id() + } + + pub(crate) fn effective_scope( + &self, + effective_config: &crate::execution::config_model::SubstrateConfig, + ) -> crate::execution::config_model::AgentExecutionScope { + self.file.effective_scope(effective_config) + } +} + fn default_true() -> bool { true } @@ -159,14 +198,8 @@ pub(crate) fn resolve_gateway_backend_inventory_entry( )) })?; - let derived_backend_id = format!( - "{}:{}", - agent_kind_as_backend_kind(entry.file.config.kind), - entry.file.id - ); - if derived_backend_id != trimmed_backend_id - || backend_kind != agent_kind_as_backend_kind(entry.file.config.kind) - { + let derived_backend_id = entry.derived_backend_id(); + if derived_backend_id != trimmed_backend_id || backend_kind != entry.file.config.kind.as_str() { return Err(config_model::user_error(format!( "gateway backend '{}' does not match effective inventory item '{}' in {}", trimmed_backend_id, @@ -609,10 +642,3 @@ fn validate_overlay_subset( } Ok(()) } - -fn agent_kind_as_backend_kind(kind: AgentConfigKind) -> &'static str { - match kind { - AgentConfigKind::Cli => "cli", - AgentConfigKind::Api => "api", - } -} diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 7512ef0f9..e4c54c92b 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -1,9 +1,67 @@ -use crate::execution::agent_inventory::{discover_agent_files, validate_agent_file}; -use crate::execution::cli::{AgentsAction, AgentsCmd, Cli}; -use crate::execution::config_model; -use anyhow::Result; +use crate::execution::agent_inventory::{ + discover_agent_files, load_effective_agent_inventory, validate_agent_file, + AgentInventoryEntryV1, +}; +use crate::execution::cli::{ + AgentAction, AgentCmd, AgentDoctorArgs, AgentScopeArg, AgentViewArgs, AgentsAction, AgentsCmd, + Cli, +}; +use crate::execution::config_model::{ + self, AgentExecutionScope, CliConfigOverrides, SubstrateConfig, +}; +use anyhow::{Context, Result}; +use serde::Serialize; +use std::collections::{BTreeMap, BTreeSet}; use std::env; -use std::path::PathBuf; +use std::fs::File; +use std::io::{self, BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use substrate_broker::Policy; +use substrate_common::paths as substrate_paths; +use substrate_common::{AgentEvent, PlacementExecution}; + +const PURE_AGENT_PROTOCOL: &str = "uaa.agent.session"; +const PURE_AGENT_ROUTER: &str = "agent_hub"; +const NESTED_ROUTER: &str = "substrate_gateway"; +const ORCHESTRATOR_ROLE: &str = "orchestrator"; + +pub(crate) fn handle_agent_command(cmd: &AgentCmd, cli: &Cli) -> i32 { + match &cmd.action { + AgentAction::List(args) => match run_list(args, cli) { + Ok(()) => 0, + Err(err) if config_model::is_user_error(&err) => { + eprintln!("{err}"); + 2 + } + Err(err) => { + eprintln!("{err:#}"); + 1 + } + }, + AgentAction::Status(args) => match run_status(args, cli) { + Ok(()) => 0, + Err(err) if config_model::is_user_error(&err) => { + eprintln!("{err}"); + 2 + } + Err(err) => { + eprintln!("{err:#}"); + 1 + } + }, + AgentAction::Doctor(args) => match run_doctor(args, cli) { + Ok(code) => code, + Err(err) if config_model::is_user_error(&err) => { + eprintln!("{err}"); + 2 + } + Err(err) => { + eprintln!("{err:#}"); + 1 + } + }, + } +} pub(crate) fn handle_agents_command(cmd: &AgentsCmd, _cli: &Cli) -> i32 { let result = match &cmd.action { @@ -35,3 +93,963 @@ fn run_validate() -> Result<()> { Ok(()) } + +fn run_list(args: &AgentViewArgs, cli: &Cli) -> Result<()> { + let context = resolve_command_context(cli)?; + let report = build_list_report(&context, args); + render_list_report(&report, args.json)?; + Ok(()) +} + +fn run_status(args: &AgentViewArgs, cli: &Cli) -> Result<()> { + let context = resolve_command_context(cli)?; + let report = build_status_report(&context, args)?; + render_status_report(&report, args.json)?; + Ok(()) +} + +fn run_doctor(args: &AgentDoctorArgs, cli: &Cli) -> Result { + let report = build_doctor_report(cli)?; + let exit_code = doctor_exit_code(&report); + render_doctor_report(&report, args.json)?; + Ok(exit_code) +} + +struct AgentCommandContext { + effective_config: SubstrateConfig, + base_policy: Policy, + inventory: BTreeMap, +} + +fn resolve_command_context(cli: &Cli) -> Result { + let cwd = current_dir(); + let effective_config = resolve_effective_config(&cwd, cli)?; + let (base_policy, _) = substrate_broker::resolve_effective_policy_with_explain(&cwd, false) + .map_err(|err| config_model::user_error(err.to_string()))?; + let inventory = load_effective_agent_inventory(&cwd, &base_policy)?; + + Ok(AgentCommandContext { + effective_config, + base_policy, + inventory, + }) +} + +fn current_dir() -> PathBuf { + env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) +} + +fn cli_world_enabled(cli: &Cli) -> Option { + if cli.world { + Some(true) + } else if cli.no_world { + Some(false) + } else { + None + } +} + +fn resolve_effective_config(cwd: &Path, cli: &Cli) -> Result { + config_model::resolve_effective_config( + cwd, + &CliConfigOverrides { + world_enabled: cli_world_enabled(cli), + ..Default::default() + }, + ) +} + +#[derive(Clone, Serialize)] +struct ExecutionScopeJson<'a> { + scope: &'a str, +} + +#[derive(Serialize)] +struct CapabilitiesSummaryJson { + llm: bool, + mcp_client: bool, +} + +#[derive(Serialize)] +struct EligibilityJson<'a> { + state: &'a str, + reason: Option, +} + +#[derive(Serialize)] +struct ListAgentJson<'a> { + agent_id: String, + backend_id: String, + kind: &'a str, + execution: ExecutionScopeJson<'a>, + role: Option<&'a str>, + capabilities_summary: CapabilitiesSummaryJson, + eligibility: EligibilityJson<'a>, + protocol: &'a str, +} + +#[derive(Serialize)] +struct ListReportJson<'a> { + disabled: bool, + scope_filter: &'a str, + role_filter: Option<&'a str>, + agents: Vec>, +} + +fn build_list_report<'a>( + context: &'a AgentCommandContext, + args: &'a AgentViewArgs, +) -> ListReportJson<'a> { + let role_filter = normalized_role_filter(args.role.as_deref()); + let agents = if context.effective_config.agents.enabled { + context + .inventory + .values() + .filter_map(|entry| { + let scope = entry.effective_scope(&context.effective_config); + let role = role_for_entry(&entry.file.id, &context.effective_config); + if !matches_scope(scope, args.scope) || !matches_role(role, role_filter) { + return None; + } + + let backend_id = entry.derived_backend_id(); + let eligibility_reason = + eligibility_reason(entry, &context.effective_config, &context.base_policy); + let eligibility = if let Some(reason) = eligibility_reason { + EligibilityJson { + state: "denied", + reason: Some(reason), + } + } else { + EligibilityJson { + state: "allowed", + reason: None, + } + }; + + Some(ListAgentJson { + agent_id: entry.file.id.clone(), + backend_id, + kind: entry.file.config.kind.as_str(), + execution: ExecutionScopeJson { + scope: scope.as_str(), + }, + role, + capabilities_summary: CapabilitiesSummaryJson { + llm: entry.file.config.capabilities.llm, + mcp_client: entry.file.config.capabilities.mcp_client, + }, + eligibility, + protocol: PURE_AGENT_PROTOCOL, + }) + }) + .collect() + } else { + Vec::new() + }; + + ListReportJson { + disabled: !context.effective_config.agents.enabled, + scope_filter: args.scope.as_str(), + role_filter, + agents, + } +} + +fn render_list_report(report: &ListReportJson<'_>, json_mode: bool) -> Result<()> { + if json_mode { + println!("{}", serde_json::to_string_pretty(report)?); + return Ok(()); + } + + println!("disabled: {}", report.disabled); + println!( + "scope_filter: {}{}", + report.scope_filter, + report + .role_filter + .map(|role| format!(", role_filter: {role}")) + .unwrap_or_default() + ); + println!( + "agent_id\tbackend_id\tkind\texecution.scope\trole\tcapabilities\teligibility\tprotocol" + ); + + for agent in &report.agents { + let capabilities = capabilities_label(&agent.capabilities_summary); + let eligibility = if let Some(reason) = &agent.eligibility.reason { + format!("{}: {}", agent.eligibility.state, reason) + } else { + agent.eligibility.state.to_string() + }; + println!( + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + agent.agent_id, + agent.backend_id, + agent.kind, + agent.execution.scope, + agent.role.unwrap_or(""), + capabilities, + eligibility, + agent.protocol + ); + } + + Ok(()) +} + +#[derive(Clone, Serialize)] +struct StatusSessionJson { + orchestration_session_id: String, + agent_id: String, + backend_id: String, + client: String, + router: String, + protocol: String, + execution: ExecutionScopeJson<'static>, + role: Option, + last_event_at: String, + #[serde(skip_serializing_if = "Option::is_none")] + world_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + world_generation: Option, +} + +#[derive(Clone, Serialize)] +struct NestedParentJson { + orchestration_session_id: String, + agent_id: String, +} + +#[derive(Clone, Serialize)] +struct NestedLlmRecordJson { + parent: NestedParentJson, + backend_id: String, + client: String, + router: String, + provider: String, + auth_authority: String, + protocol: String, +} + +#[derive(Serialize)] +struct StatusReportJson<'a> { + disabled: bool, + scope_filter: &'a str, + role_filter: Option<&'a str>, + orchestrator_agent_id: String, + sessions: Vec, + nested_llm_records: Vec, +} + +#[derive(Clone)] +struct SessionProjection { + session: StatusSessionJson, +} + +#[derive(Clone)] +struct NestedProjection { + record: NestedLlmRecordJson, + sort_key: (String, String, String, String, String, String), +} + +fn build_status_report<'a>( + context: &'a AgentCommandContext, + args: &'a AgentViewArgs, +) -> Result> { + let role_filter = normalized_role_filter(args.role.as_deref()); + let orchestrator_agent_id = context + .effective_config + .agents + .hub + .orchestrator_agent_id + .clone(); + + if !context.effective_config.agents.enabled { + return Ok(StatusReportJson { + disabled: true, + scope_filter: args.scope.as_str(), + role_filter, + orchestrator_agent_id, + sessions: Vec::new(), + nested_llm_records: Vec::new(), + }); + } + + let events = read_trace_agent_events()?; + let mut sessions = BTreeMap::<(String, String), SessionProjection>::new(); + let mut nested = + BTreeMap::<(String, String, String, String, String, String), NestedProjection>::new(); + + for event in events { + let Some(entry) = context.inventory.get(&event.agent_id) else { + continue; + }; + let role = role_for_event(&event, &entry.file.id, &context.effective_config); + let scope = scope_for_event(&event, entry, &context.effective_config); + + if let Some(session_key) = pure_session_key(&event) { + let mut world_id = None; + let mut world_generation = None; + if scope == AgentExecutionScope::World { + let maybe_world_id = event.world_id.clone(); + let maybe_world_generation = event + .data + .get("world_generation") + .and_then(serde_json::Value::as_u64); + if maybe_world_id.is_some() && maybe_world_generation.is_some() { + world_id = maybe_world_id; + world_generation = maybe_world_generation; + } + } + + let projection = SessionProjection { + session: StatusSessionJson { + orchestration_session_id: session_key.0.clone(), + agent_id: entry.file.id.clone(), + backend_id: entry.derived_backend_id(), + client: entry.file.id.clone(), + router: PURE_AGENT_ROUTER.to_string(), + protocol: PURE_AGENT_PROTOCOL.to_string(), + execution: ExecutionScopeJson { + scope: scope.as_str(), + }, + role: role.map(ToOwned::to_owned), + last_event_at: event.ts.to_rfc3339(), + world_id, + world_generation, + }, + }; + + sessions.insert(session_key, projection); + } + + if let Some(projection) = nested_projection(&event, entry) { + nested.insert(projection.sort_key.clone(), projection); + } + } + + let filtered_sessions: Vec = sessions + .into_values() + .filter(|projection| { + matches_scope( + scope_from_label(projection.session.execution.scope), + args.scope, + ) && matches_role(projection.session.role.as_deref(), role_filter) + }) + .map(|projection| projection.session) + .collect(); + + let allowed_parents: BTreeSet<(String, String)> = filtered_sessions + .iter() + .map(|session| { + ( + session.orchestration_session_id.clone(), + session.agent_id.clone(), + ) + }) + .collect(); + + let filtered_nested: Vec = nested + .into_values() + .filter(|projection| { + allowed_parents.contains(&( + projection.record.parent.orchestration_session_id.clone(), + projection.record.parent.agent_id.clone(), + )) + }) + .map(|projection| projection.record) + .collect(); + + Ok(StatusReportJson { + disabled: false, + scope_filter: args.scope.as_str(), + role_filter, + orchestrator_agent_id, + sessions: filtered_sessions, + nested_llm_records: filtered_nested, + }) +} + +fn render_status_report(report: &StatusReportJson<'_>, json_mode: bool) -> Result<()> { + if json_mode { + println!("{}", serde_json::to_string_pretty(report)?); + return Ok(()); + } + + println!("disabled: {}", report.disabled); + println!("orchestrator_agent_id: {}", report.orchestrator_agent_id); + println!("scope_filter: {}", report.scope_filter); + if let Some(role_filter) = report.role_filter { + println!("role_filter: {role_filter}"); + } + + println!(); + println!("orchestrator"); + println!(" agent_id: {}", report.orchestrator_agent_id); + + println!(); + println!("sessions"); + for session in &report.sessions { + let mut fields = vec![ + format!( + "orchestration_session_id={}", + session.orchestration_session_id + ), + format!("agent_id={}", session.agent_id), + format!("backend_id={}", session.backend_id), + format!("client={}", session.client), + format!("router={}", session.router), + format!("protocol={}", session.protocol), + format!("execution.scope={}", session.execution.scope), + format!("role={}", session.role.as_deref().unwrap_or("")), + format!("last_event_at={}", session.last_event_at), + ]; + if let (Some(world_id), Some(world_generation)) = + (session.world_id.as_deref(), session.world_generation) + { + fields.push(format!("world_id={world_id}")); + fields.push(format!("world_generation={world_generation}")); + } + println!(" {}", fields.join(" | ")); + } + + if !report.nested_llm_records.is_empty() { + println!(); + println!("nested_llm_records"); + for record in &report.nested_llm_records { + println!( + " parent.orchestration_session_id={} | parent.agent_id={} | backend_id={} | client={} | router={} | provider={} | auth_authority={} | protocol={}", + record.parent.orchestration_session_id, + record.parent.agent_id, + record.backend_id, + record.client, + record.router, + record.provider, + record.auth_authority, + record.protocol + ); + } + } + + Ok(()) +} + +fn read_trace_agent_events() -> Result> { + let trace_path = trace_log_path()?; + let file = match File::open(&trace_path) { + Ok(file) => file, + Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(err) => { + return Err(err) + .with_context(|| format!("failed to read trace log at {}", trace_path.display())) + } + }; + + let reader = BufReader::new(file); + let mut events = Vec::new(); + for line in reader.lines() { + let line = match line { + Ok(line) => line, + Err(_) => continue, + }; + if line.trim().is_empty() { + continue; + } + let value: serde_json::Value = match serde_json::from_str(&line) { + Ok(value) => value, + Err(_) => continue, + }; + if value.get("event_type").and_then(serde_json::Value::as_str) != Some("agent_event") { + continue; + } + let event: AgentEvent = match serde_json::from_value(value) { + Ok(event) => event, + Err(_) => continue, + }; + events.push(event); + } + + Ok(events) +} + +fn trace_log_path() -> Result { + if let Ok(path) = env::var("SHIM_TRACE_LOG") { + return Ok(PathBuf::from(path)); + } + + Ok(substrate_paths::substrate_home()?.join("trace.jsonl")) +} + +fn pure_session_key(event: &AgentEvent) -> Option<(String, String)> { + let tuple = event.identity_tuple.as_ref()?; + if tuple.router != PURE_AGENT_ROUTER || tuple.protocol != PURE_AGENT_PROTOCOL { + return None; + } + Some(( + event.orchestration_session_id.clone(), + event.agent_id.clone(), + )) +} + +fn nested_projection( + event: &AgentEvent, + entry: &AgentInventoryEntryV1, +) -> Option { + let tuple = event.identity_tuple.as_ref()?; + if tuple.router != NESTED_ROUTER { + return None; + } + let provider = tuple.provider.clone()?; + let auth_authority = tuple.auth_authority.clone()?; + let sort_key = ( + event.orchestration_session_id.clone(), + event.agent_id.clone(), + entry.derived_backend_id(), + provider.clone(), + auth_authority.clone(), + tuple.protocol.clone(), + ); + + Some(NestedProjection { + record: NestedLlmRecordJson { + parent: NestedParentJson { + orchestration_session_id: event.orchestration_session_id.clone(), + agent_id: event.agent_id.clone(), + }, + backend_id: entry.derived_backend_id(), + client: event.agent_id.clone(), + router: NESTED_ROUTER.to_string(), + provider, + auth_authority, + protocol: tuple.protocol.clone(), + }, + sort_key, + }) +} + +fn scope_for_event( + event: &AgentEvent, + entry: &AgentInventoryEntryV1, + effective_config: &SubstrateConfig, +) -> AgentExecutionScope { + match event + .placement_posture + .as_ref() + .map(|posture| posture.execution) + { + Some(PlacementExecution::HostOnly) => AgentExecutionScope::Host, + Some(PlacementExecution::InWorld) => AgentExecutionScope::World, + None => entry.effective_scope(effective_config), + } +} + +fn role_for_entry<'a>(agent_id: &str, effective_config: &'a SubstrateConfig) -> Option<&'a str> { + if effective_config.agents.hub.orchestrator_agent_id == agent_id { + Some(ORCHESTRATOR_ROLE) + } else { + None + } +} + +fn role_for_event<'a>( + event: &'a AgentEvent, + agent_id: &str, + effective_config: &'a SubstrateConfig, +) -> Option<&'a str> { + match event.role.as_deref() { + Some(ORCHESTRATOR_ROLE) => Some(ORCHESTRATOR_ROLE), + _ => role_for_entry(agent_id, effective_config), + } +} + +fn eligibility_reason( + entry: &AgentInventoryEntryV1, + effective_config: &SubstrateConfig, + base_policy: &Policy, +) -> Option { + if !entry.file.config.enabled { + return Some("agent is disabled in the effective inventory".to_string()); + } + + let backend_id = entry.derived_backend_id(); + if !backend_allowed(base_policy, &backend_id) { + return Some(format!( + "{backend_id} is not allowlisted by effective policy agents.allowed_backends" + )); + } + + let _ = effective_config; + None +} + +fn backend_allowed(policy: &Policy, backend_id: &str) -> bool { + policy + .agents_allowed_backends + .iter() + .any(|allowed| allowed == backend_id) +} + +fn enabled_world_member_exists( + inventory: &BTreeMap, + effective_config: &SubstrateConfig, +) -> bool { + inventory.values().any(|entry| { + entry.file.config.enabled + && entry.effective_scope(effective_config) == AgentExecutionScope::World + }) +} + +#[derive(Serialize)] +struct DoctorOrchestratorJson<'a> { + agent_id: String, + backend_id: String, + execution: ExecutionScopeJson<'a>, +} + +#[derive(Clone, Serialize)] +struct DoctorCheckJson { + check: String, + status: String, + reason: Option, +} + +#[derive(Serialize)] +struct DoctorReportJson<'a> { + healthy: bool, + fail_closed: bool, + #[serde(skip_serializing_if = "Option::is_none")] + orchestrator: Option>, + checks: Vec, +} + +fn build_doctor_report(cli: &Cli) -> Result> { + let cwd = current_dir(); + let effective_config = match resolve_effective_config(&cwd, cli) { + Ok(config) => config, + Err(err) => { + if config_model::is_user_error(&err) { + return Ok(failed_doctor_report( + "inventory_scan", + err.to_string(), + None, + )); + } + return Err(err); + } + }; + + let (base_policy, _) = + match substrate_broker::resolve_effective_policy_with_explain(&cwd, false) + .map_err(|err| config_model::user_error(err.to_string())) + { + Ok(value) => value, + Err(err) => { + if config_model::is_user_error(&err) { + return Ok(failed_doctor_report( + "inventory_scan", + err.to_string(), + None, + )); + } + return Err(err); + } + }; + + let inventory = match load_effective_agent_inventory(&cwd, &base_policy) { + Ok(inventory) => inventory, + Err(err) => { + if config_model::is_user_error(&err) { + return Ok(failed_doctor_report( + "inventory_scan", + err.to_string(), + None, + )); + } + return Err(err); + } + }; + + let mut checks = vec![DoctorCheckJson { + check: "inventory_scan".to_string(), + status: "pass".to_string(), + reason: None, + }]; + + let orchestrator = match validate_orchestrator_selection(&effective_config, &inventory) { + Ok(entry) => { + checks.push(DoctorCheckJson { + check: "orchestrator_selection".to_string(), + status: "pass".to_string(), + reason: None, + }); + DoctorOrchestratorJson { + agent_id: entry.file.id.clone(), + backend_id: entry.derived_backend_id(), + execution: ExecutionScopeJson { scope: "host" }, + } + } + Err(reason) => { + checks.push(DoctorCheckJson { + check: "orchestrator_selection".to_string(), + status: "fail".to_string(), + reason: Some(reason), + }); + return Ok(DoctorReportJson { + healthy: false, + fail_closed: true, + orchestrator: None, + checks, + }); + } + }; + + if let Some(reason) = policy_allowlist_failure(&effective_config, &inventory, &base_policy) { + checks.push(DoctorCheckJson { + check: "policy_allowlist".to_string(), + status: "fail".to_string(), + reason: Some(reason), + }); + return Ok(DoctorReportJson { + healthy: false, + fail_closed: true, + orchestrator: Some(orchestrator), + checks, + }); + } + checks.push(DoctorCheckJson { + check: "policy_allowlist".to_string(), + status: "pass".to_string(), + reason: None, + }); + + if enabled_world_member_exists(&inventory, &effective_config) { + if effective_config.world.enabled { + checks.push(DoctorCheckJson { + check: "world_boundary".to_string(), + status: "pass".to_string(), + reason: None, + }); + } else { + checks.push(DoctorCheckJson { + check: "world_boundary".to_string(), + status: "fail".to_string(), + reason: Some( + "world-scoped member posture requires world isolation but world is disabled" + .to_string(), + ), + }); + return Ok(DoctorReportJson { + healthy: false, + fail_closed: true, + orchestrator: Some(orchestrator), + checks, + }); + } + } else { + checks.push(DoctorCheckJson { + check: "world_boundary".to_string(), + status: "not_applicable".to_string(), + reason: None, + }); + } + + Ok(DoctorReportJson { + healthy: true, + fail_closed: false, + orchestrator: Some(orchestrator), + checks, + }) +} + +fn failed_doctor_report( + check: &str, + reason: String, + orchestrator: Option>, +) -> DoctorReportJson<'static> { + DoctorReportJson { + healthy: false, + fail_closed: true, + orchestrator, + checks: vec![DoctorCheckJson { + check: check.to_string(), + status: "fail".to_string(), + reason: Some(reason), + }], + } +} + +fn validate_orchestrator_selection<'a>( + effective_config: &SubstrateConfig, + inventory: &'a BTreeMap, +) -> std::result::Result<&'a AgentInventoryEntryV1, String> { + if !effective_config.agents.enabled { + return Err("agents are disabled by effective config".to_string()); + } + + let orchestrator_agent_id = effective_config.agents.hub.orchestrator_agent_id.trim(); + if orchestrator_agent_id.is_empty() { + return Err("agents.hub.orchestrator_agent_id must select an orchestrator".to_string()); + } + + let entry = inventory.get(orchestrator_agent_id).ok_or_else(|| { + format!( + "agents.hub.orchestrator_agent_id '{}' is not present in the effective agent inventory", + orchestrator_agent_id + ) + })?; + + if !entry.file.config.enabled { + return Err(format!( + "selected orchestrator '{}' is disabled in the effective inventory", + orchestrator_agent_id + )); + } + + if entry.effective_scope(effective_config) != AgentExecutionScope::Host { + return Err(format!( + "selected orchestrator '{}' must resolve to execution.scope=host", + orchestrator_agent_id + )); + } + + Ok(entry) +} + +fn policy_allowlist_failure( + effective_config: &SubstrateConfig, + inventory: &BTreeMap, + base_policy: &Policy, +) -> Option { + let orchestrator = validate_orchestrator_selection(effective_config, inventory).ok()?; + let orchestrator_backend_id = orchestrator.derived_backend_id(); + if !backend_allowed(base_policy, &orchestrator_backend_id) { + return Some(format!( + "selected orchestrator backend '{}' is not allowlisted by effective policy agents.allowed_backends", + orchestrator_backend_id + )); + } + + for entry in inventory.values() { + if !entry.file.config.enabled + || entry.effective_scope(effective_config) != AgentExecutionScope::World + { + continue; + } + let backend_id = entry.derived_backend_id(); + if !backend_allowed(base_policy, &backend_id) { + return Some(format!( + "required world-scoped member backend '{}' is not allowlisted by effective policy agents.allowed_backends", + backend_id + )); + } + } + + None +} + +fn doctor_exit_code(report: &DoctorReportJson<'_>) -> i32 { + if report.healthy { + return 0; + } + + let Some(failed_check) = report + .checks + .iter() + .find(|check| check.status == "fail") + .map(|check| check.check.as_str()) + else { + return 1; + }; + + match failed_check { + "policy_allowlist" => 5, + "world_boundary" => 3, + "inventory_scan" | "orchestrator_selection" => 2, + _ => 1, + } +} + +fn render_doctor_report(report: &DoctorReportJson<'_>, json_mode: bool) -> Result<()> { + if json_mode { + println!("{}", serde_json::to_string_pretty(report)?); + return Ok(()); + } + + println!( + "{}", + if report.healthy { + "healthy" + } else { + "fail_closed" + } + ); + if let Some(orchestrator) = &report.orchestrator { + println!("orchestrator"); + println!(" agent_id: {}", orchestrator.agent_id); + println!(" backend_id: {}", orchestrator.backend_id); + println!(" execution.scope: {}", orchestrator.execution.scope); + } + println!("checks"); + for check in &report.checks { + match check.reason.as_deref() { + Some(reason) if check.status == "fail" => { + println!(" {}: fail: {}", check.check, reason); + } + _ => println!(" {}: {}", check.check, check.status), + } + } + + Ok(()) +} + +fn matches_scope(scope: AgentExecutionScope, filter: AgentScopeArg) -> bool { + match filter { + AgentScopeArg::Any => true, + AgentScopeArg::Host => scope == AgentExecutionScope::Host, + AgentScopeArg::World => scope == AgentExecutionScope::World, + } +} + +fn scope_from_label(value: &str) -> AgentExecutionScope { + match value { + "host" => AgentExecutionScope::Host, + "world" => AgentExecutionScope::World, + _ => AgentExecutionScope::World, + } +} + +fn matches_role(role: Option<&str>, role_filter: Option<&str>) -> bool { + match role_filter { + Some(filter) => role == Some(filter), + None => true, + } +} + +fn normalized_role_filter(role: Option<&str>) -> Option<&str> { + role.map(str::trim).filter(|role| !role.is_empty()) +} + +fn capabilities_label(summary: &CapabilitiesSummaryJson) -> String { + let mut parts = Vec::new(); + if summary.llm { + parts.push("llm"); + } + if summary.mcp_client { + parts.push("mcp_client"); + } + if parts.is_empty() { + "none".to_string() + } else { + parts.join(",") + } +} + +impl AgentExecutionScope { + fn as_str(self) -> &'static str { + match self { + Self::Host => "host", + Self::World => "world", + } + } +} diff --git a/crates/shell/src/execution/cli.rs b/crates/shell/src/execution/cli.rs index caeac3906..afda96d99 100644 --- a/crates/shell/src/execution/cli.rs +++ b/crates/shell/src/execution/cli.rs @@ -158,6 +158,7 @@ pub enum SubCommands { Config(ConfigCmd), Policy(PolicyCmd), Workspace(WorkspaceCmd), + Agent(AgentCmd), Agents(AgentsCmd), Shim(ShimCmd), Health(HealthCmd), @@ -395,6 +396,66 @@ pub struct PolicySetArgs { pub updates: Vec, } +#[derive(Args, Debug)] +pub struct AgentCmd { + #[command(subcommand)] + pub action: AgentAction, +} + +#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq)] +#[value(rename_all = "snake_case")] +pub enum AgentScopeArg { + Host, + World, + Any, +} + +impl AgentScopeArg { + pub fn as_str(self) -> &'static str { + match self { + Self::Host => "host", + Self::World => "world", + Self::Any => "any", + } + } +} + +impl Default for AgentScopeArg { + fn default() -> Self { + Self::Any + } +} + +#[derive(Args, Debug, Default)] +pub struct AgentViewArgs { + /// Emit JSON instead of human-readable output + #[arg(long)] + pub json: bool, + /// Filter rows by execution scope + #[arg(long, value_name = "host|world|any", default_value = "any")] + pub scope: AgentScopeArg, + /// Filter rows by role label + #[arg(long, value_name = "ROLE")] + pub role: Option, +} + +#[derive(Args, Debug, Default)] +pub struct AgentDoctorArgs { + /// Emit JSON instead of human-readable output + #[arg(long)] + pub json: bool, +} + +#[derive(Subcommand, Debug)] +pub enum AgentAction { + /// List the effective agent inventory + List(AgentViewArgs), + /// Show the current pure-agent and nested gateway-backed status view + Status(AgentViewArgs), + /// Validate deterministic startability of the agent control plane + Doctor(AgentDoctorArgs), +} + #[derive(Args, Debug)] pub struct WorkspaceCmd { #[command(subcommand)] diff --git a/crates/shell/src/execution/invocation/plan.rs b/crates/shell/src/execution/invocation/plan.rs index bf04d650e..c49ed2677 100644 --- a/crates/shell/src/execution/invocation/plan.rs +++ b/crates/shell/src/execution/invocation/plan.rs @@ -6,10 +6,10 @@ use crate::execution::shim_deploy::{DeploymentStatus, ShimDeployer}; #[cfg(target_os = "linux")] use crate::execution::socket_activation; use crate::execution::{ - export_runtime_config_env, handle_agents_command, handle_config_command, handle_graph_command, - handle_health_command, handle_host_command, handle_policy_command, handle_replay_command, - handle_shim_command, handle_trace_command, handle_workspace_command, handle_world_command, - update_world_env, + export_runtime_config_env, handle_agent_command, handle_agents_command, handle_config_command, + handle_graph_command, handle_health_command, handle_host_command, handle_policy_command, + handle_replay_command, handle_shim_command, handle_trace_command, handle_workspace_command, + handle_world_command, update_world_env, }; use anyhow::{Context, Result}; use clap::Parser; @@ -499,6 +499,10 @@ impl ShellConfig { let code = handle_workspace_command(workspace_cmd, &cli); std::process::exit(code); } + SubCommands::Agent(agent_cmd) => { + let code = handle_agent_command(agent_cmd, &cli); + std::process::exit(code); + } SubCommands::Agents(agents_cmd) => { let code = handle_agents_command(agents_cmd, &cli); std::process::exit(code); diff --git a/crates/shell/src/execution/mod.rs b/crates/shell/src/execution/mod.rs index 83c7d1f1c..9a7ead4de 100644 --- a/crates/shell/src/execution/mod.rs +++ b/crates/shell/src/execution/mod.rs @@ -30,6 +30,7 @@ mod value_parse; mod workspace; mod workspace_cmd; +pub(crate) use agents_cmd::handle_agent_command; pub(crate) use agents_cmd::handle_agents_command; pub(crate) use auto_sync::run_auto_sync_if_enabled; pub use cli::*; diff --git a/docs/USAGE.md b/docs/USAGE.md index 1ce7e6b48..b0d01d57e 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -59,6 +59,29 @@ The shell includes cross-platform built-ins handled without spawning a child pro through the builtin handler when running non-interactively (`substrate -c`, scripts, etc.). +### Agent Commands + +The successor agent control-plane surface lives under the singular +`substrate agent` namespace: + +```bash +substrate agent list +substrate agent list --json +substrate agent status --scope world +substrate agent doctor --json +``` + +`substrate agents validate` remains available as the inventory-validation +compatibility leaf. It does not alias `substrate agent list`, `status`, or +`doctor`. + +Operator-visible identity rules on these surfaces: +- `backend_id` always renders as `:`. +- Pure-agent list rows omit `provider`, `auth_authority`, `world_id`, and `world_generation`. +- Pure-agent status rows omit `provider` and `auth_authority`. +- `world_id` and `world_generation` render only for world-scoped pure-agent session rows. +- Nested gateway-backed status rows stay separate from pure-agent rows and are the only rows that publish `provider` and `auth_authority`. + ## PTY Support Substrate automatically uses PTY for interactive commands: From fcbdb8c2a4f3e569be883b886c96e800074e5290 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 21:35:24 -0400 Subject: [PATCH 06/48] docs: finish AHCSITC0 code+test --- .../session_log.md | 8 ++++++++ .../tasks.json | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index c658bb1e2..1599de632 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -121,3 +121,11 @@ ## START — 2026-04-25T01:19:27Z — test — AHCSITC0-test - Dispatch: - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC0"` + +## END — 2026-04-25T01:35:24Z — code — AHCSITC0-code +- HEAD: `e8801afac6f568a3855c158d6547cebcb18663b4` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC0/code/last_message.md` + +## END — 2026-04-25T01:35:24Z — test — AHCSITC0-test +- HEAD: `c8d7c9488066305762d8d4c000c3f3835c0c3207` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC0/test/last_message.md` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index cabe7e8ef..685542a03 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -72,7 +72,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC0\"" ], - "status": "in_progress", + "status": "completed", "type": "code", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-code" }, @@ -124,7 +124,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC0\"" ], - "status": "in_progress", + "status": "completed", "type": "test", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-test" }, From e9e45522fd07d88f21445ecaf4f793bd2ce4c5c8 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 21:35:24 -0400 Subject: [PATCH 07/48] docs: start AHCSITC0-integ --- .../session_log.md | 4 ++++ .../tasks.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 1599de632..6aa535568 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -129,3 +129,7 @@ ## END — 2026-04-25T01:35:24Z — test — AHCSITC0-test - HEAD: `c8d7c9488066305762d8d4c000c3f3835c0c3207` - Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC0/test/last_message.md` + +## START — 2026-04-25T01:35:24Z — integration — AHCSITC0-integ +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC0-integ" LAUNCH_CODEX=1` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 685542a03..70fb59b28 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -180,7 +180,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC0-integ\"" ], - "status": "pending", + "status": "in_progress", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-integ" }, From 60ce0818f76ede1b002726c2daa2715258a1d0d0 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 21:45:19 -0400 Subject: [PATCH 08/48] test: align gateway unavailable contract --- crates/shell/tests/world_gateway.rs | 37 +++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/shell/tests/world_gateway.rs b/crates/shell/tests/world_gateway.rs index 17dd6e77a..5dd8c3dde 100644 --- a/crates/shell/tests/world_gateway.rs +++ b/crates/shell/tests/world_gateway.rs @@ -735,11 +735,17 @@ fn assert_gateway_unavailable(args: &[&str], expected_fragment: &str) { fn assert_gateway_unavailable_json(args: &[&str]) { let mut cmd = substrate_shell_driver(); - cmd.args(args) + let assert = cmd + .args(args) .assert() .code(4) - .stdout("{\"status\":\"unavailable\"}\n") .stderr(predicate::str::is_empty()); + let stdout = + String::from_utf8(assert.get_output().stdout.clone()).expect("gateway status stdout utf8"); + let parsed: JsonValue = + serde_json::from_str(stdout.trim()).expect("parse gateway unavailable json"); + assert_eq!(parsed.pointer("/status"), Some(&json!("unavailable"))); + assert_eq!(parsed.pointer("/client_wiring"), None); } fn gateway_socket_fixture() -> (TempDir, AgentSocket, std::path::PathBuf) { @@ -1215,14 +1221,37 @@ fn world_gateway_disabled_state_skips_typed_runtime_bootstrap() { let (_temp, _socket, socket_path) = gateway_socket_fixture(); let mut cmd = substrate_shell_driver(); - cmd.env("SUBSTRATE_WORLD_ENABLED", "0") + let assert = cmd + .env("SUBSTRATE_WORLD_ENABLED", "0") .env("SUBSTRATE_WORLD", "disabled") .env("SUBSTRATE_WORLD_SOCKET", &socket_path) .args(["world", "gateway", "status", "--json"]) .assert() .code(4) - .stdout("{\"status\":\"unavailable\"}\n") .stderr(predicate::str::is_empty()); + + let stdout = + String::from_utf8(assert.get_output().stdout.clone()).expect("gateway status stdout utf8"); + let parsed: JsonValue = serde_json::from_str(stdout.trim()).expect("parse gateway status json"); + + assert_eq!(parsed.pointer("/status"), Some(&json!("unavailable"))); + assert_eq!(parsed.pointer("/client_wiring"), None); + assert_eq!( + parsed.pointer("/identity_tuple/router"), + Some(&json!("substrate_gateway")) + ); + assert_eq!( + parsed.pointer("/identity_tuple/provider"), + Some(&json!("openai")) + ); + assert_eq!( + parsed.pointer("/identity_tuple/auth_authority"), + Some(&json!("codex_subscription")) + ); + assert_eq!( + parsed.pointer("/placement_posture/execution"), + Some(&json!("in_world")) + ); } #[test] From 49a2e5141ce055427c561e40b3b6db117eacaf21 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 21:48:07 -0400 Subject: [PATCH 09/48] docs: allow AHCSITC0 integration touch --- .../pre-planning/impact_map.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md index 0a59b8608..da1d2919b 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md @@ -93,6 +93,7 @@ Strict packs (`tasks.json` → `meta.slice_spec_version >= 2`) requirements: - `crates/shell/tests/agents_validate.rs` - `crates/shell/tests/agent_hub_trace_persistence.rs` - `crates/shell/tests/repl_world_first_routing_v1.rs` +- `crates/shell/tests/world_gateway.rs` ### Deprecate - `docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md` From af1be750a3de93b55397d5247a99f40888939d50 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 21:49:57 -0400 Subject: [PATCH 10/48] docs: finish AHCSITC0-integ --- .../session_log.md | 4 ++++ .../tasks.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 6aa535568..5e10984e2 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -133,3 +133,7 @@ ## START — 2026-04-25T01:35:24Z — integration — AHCSITC0-integ - Dispatch: - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC0-integ" LAUNCH_CODEX=1` + +## END — 2026-04-25T01:49:57Z — integration — AHCSITC0-integ +- HEAD: `60ce0818f76ede1b002726c2daa2715258a1d0d0` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC0/integ/last_message.md` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 70fb59b28..025e73988 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -180,7 +180,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC0-integ\"" ], - "status": "in_progress", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-integ" }, From c21ae91091bddf69b67f71680316df9ce0c3a650 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 23:28:54 -0400 Subject: [PATCH 11/48] docs: start AHCSITC1 code+test --- .../session_log.md | 8 ++++++++ .../tasks.json | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 5e10984e2..85ae4970f 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -137,3 +137,11 @@ ## END — 2026-04-25T01:49:57Z — integration — AHCSITC0-integ - HEAD: `60ce0818f76ede1b002726c2daa2715258a1d0d0` - Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC0/integ/last_message.md` + +## START — 2026-04-25T03:28:54Z — code — AHCSITC1-code +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC1"` + +## START — 2026-04-25T03:28:54Z — test — AHCSITC1-test +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC1"` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 025e73988..585be0ee4 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -234,7 +234,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" ], - "status": "pending", + "status": "in_progress", "type": "code", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-code" }, @@ -288,7 +288,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" ], - "status": "pending", + "status": "in_progress", "type": "test", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-test" }, From 7cb003048cb4b455a649ece83d04cb15bc01e539 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 23:38:04 -0400 Subject: [PATCH 12/48] docs: finish AHCSITC1 code+test --- .../session_log.md | 8 ++++++++ .../tasks.json | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 85ae4970f..6af83d3a2 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -145,3 +145,11 @@ ## START — 2026-04-25T03:28:54Z — test — AHCSITC1-test - Dispatch: - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC1"` + +## END — 2026-04-25T03:38:04Z — code — AHCSITC1-code +- HEAD: `c21ae91091bddf69b67f71680316df9ce0c3a650` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC1/code/last_message.md` + +## END — 2026-04-25T03:38:04Z — test — AHCSITC1-test +- HEAD: `c21ae91091bddf69b67f71680316df9ce0c3a650` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC1/test/last_message.md` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 585be0ee4..11ced0ac2 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -234,7 +234,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" ], - "status": "in_progress", + "status": "completed", "type": "code", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-code" }, @@ -288,7 +288,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" ], - "status": "in_progress", + "status": "completed", "type": "test", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-test" }, From 45864b834aae0275c4cb060be8d46ddc46093d8b Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 23:38:04 -0400 Subject: [PATCH 13/48] docs: start AHCSITC1-integ --- .../session_log.md | 4 ++++ .../tasks.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 6af83d3a2..879db35ea 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -153,3 +153,7 @@ ## END — 2026-04-25T03:38:04Z — test — AHCSITC1-test - HEAD: `c21ae91091bddf69b67f71680316df9ce0c3a650` - Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC1/test/last_message.md` + +## START — 2026-04-25T03:38:04Z — integration — AHCSITC1-integ +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC1-integ" LAUNCH_CODEX=1` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 11ced0ac2..160f74399 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -344,7 +344,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC1-integ\"" ], - "status": "pending", + "status": "in_progress", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-integ" }, From a1b10b1802bacc8fb96ad776703212c1e2f842cd Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 23:46:35 -0400 Subject: [PATCH 14/48] docs: finish AHCSITC1-integ --- .../session_log.md | 4 ++++ .../tasks.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 879db35ea..217e358dd 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -157,3 +157,7 @@ ## START — 2026-04-25T03:38:04Z — integration — AHCSITC1-integ - Dispatch: - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC1-integ" LAUNCH_CODEX=1` + +## END — 2026-04-25T03:46:35Z — integration — AHCSITC1-integ +- HEAD: `45864b834aae0275c4cb060be8d46ddc46093d8b` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC1/integ/last_message.md` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 160f74399..b7a3dcbb6 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -344,7 +344,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC1-integ\"" ], - "status": "in_progress", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-integ" }, From a0277704b95b69120a44232571b45433b7f11a8e Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 24 Apr 2026 23:50:28 -0400 Subject: [PATCH 15/48] docs: start AHCSITC2 code+test --- .../session_log.md | 8 ++++++++ .../tasks.json | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 217e358dd..e54c0ac86 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -161,3 +161,11 @@ ## END — 2026-04-25T03:46:35Z — integration — AHCSITC1-integ - HEAD: `45864b834aae0275c4cb060be8d46ddc46093d8b` - Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC1/integ/last_message.md` + +## START — 2026-04-25T03:50:28Z — code — AHCSITC2-code +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC2"` + +## START — 2026-04-25T03:50:28Z — test — AHCSITC2-test +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC2"` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index b7a3dcbb6..9fa0a64ad 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -398,7 +398,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" ], - "status": "pending", + "status": "in_progress", "type": "code", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-code" }, @@ -452,7 +452,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" ], - "status": "pending", + "status": "in_progress", "type": "test", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-test" }, From 66c56b74362470d7b0891a7692cf1fca1b28b263 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:00:37 -0400 Subject: [PATCH 16/48] feat: lock agent event telemetry fields --- crates/common/src/agent_events.rs | 71 ++++++++++++++++++- crates/shell/src/execution/agents_cmd.rs | 10 +-- .../shell/src/execution/routing/telemetry.rs | 2 +- crates/shell/src/repl/async_repl.rs | 6 +- crates/world-agent/src/service.rs | 1 + 5 files changed, 83 insertions(+), 7 deletions(-) diff --git a/crates/common/src/agent_events.rs b/crates/common/src/agent_events.rs index c015fbe5e..e31dacfbf 100644 --- a/crates/common/src/agent_events.rs +++ b/crates/common/src/agent_events.rs @@ -1,15 +1,20 @@ use std::fmt; +use std::io; use std::sync::OnceLock; use chrono::{DateTime, Utc}; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::json; use crate::identity::{ - validate_identity_tuple_and_placement_posture, IdentityTuple, PlacementPosture, + validate_identity_tuple_and_placement_posture, IdentityTuple, PlacementExecution, + PlacementPosture, }; pub const AGENT_EVENT_CHANNEL_MAX_BYTES: usize = 64; +const PURE_AGENT_ROUTER: &str = "agent_hub"; +const PURE_AGENT_PROTOCOL: &str = "uaa.agent.session"; /// Canonical set of agent event categories. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -84,6 +89,8 @@ pub struct AgentEvent { #[serde(default, skip_serializing_if = "Option::is_none")] pub world_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub world_generation: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub cmd_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub span_id: Option, @@ -124,6 +131,8 @@ struct AgentEventDef { #[serde(default)] world_id: Option, #[serde(default)] + world_generation: Option, + #[serde(default)] cmd_id: Option, #[serde(default)] span_id: Option, @@ -178,6 +187,7 @@ impl AgentEvent { thread_id: None, role: None, world_id: None, + world_generation: None, cmd_id: None, span_id: None, channel: None, @@ -253,6 +263,64 @@ impl AgentEvent { self.placement_posture.as_ref(), ) } + + pub fn set_pure_agent_telemetry_identity(&mut self, client: impl Into) { + if self.identity_tuple.is_none() { + self.identity_tuple = Some(IdentityTuple { + client: client.into(), + router: PURE_AGENT_ROUTER.to_string(), + protocol: PURE_AGENT_PROTOCOL.to_string(), + provider: None, + auth_authority: None, + }); + } + + if self.placement_posture.is_none() { + self.placement_posture = Some(PlacementPosture { + execution: if self.world_id.is_some() { + PlacementExecution::InWorld + } else { + PlacementExecution::HostOnly + }, + host_to_world_bridge: None, + }); + } + } + + pub fn to_trace_record(&self) -> Result { + let mut entry = serde_json::to_value(self)?; + let Some(obj) = entry.as_object_mut() else { + return Err(serde_json::Error::io(io::Error::other( + "agent event must serialize as a JSON object", + ))); + }; + + if let Some(tuple) = self.identity_tuple.as_ref() { + obj.insert("client".to_string(), json!(tuple.client)); + obj.insert("router".to_string(), json!(tuple.router)); + obj.insert("protocol".to_string(), json!(tuple.protocol)); + + match tuple.provider.as_deref() { + Some(provider) => { + obj.insert("provider".to_string(), json!(provider)); + } + None => { + obj.remove("provider"); + } + } + + match tuple.auth_authority.as_deref() { + Some(auth_authority) => { + obj.insert("auth_authority".to_string(), json!(auth_authority)); + } + None => { + obj.remove("auth_authority"); + } + } + } + + Ok(entry) + } } impl TryFrom for AgentEvent { @@ -270,6 +338,7 @@ impl TryFrom for AgentEvent { thread_id: value.thread_id, role: value.role, world_id: value.world_id, + world_generation: value.world_generation, cmd_id: value.cmd_id, span_id: value.span_id, channel: value.channel, diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index e4c54c92b..1c1e68bc3 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -393,10 +393,12 @@ fn build_status_report<'a>( let mut world_generation = None; if scope == AgentExecutionScope::World { let maybe_world_id = event.world_id.clone(); - let maybe_world_generation = event - .data - .get("world_generation") - .and_then(serde_json::Value::as_u64); + let maybe_world_generation = event.world_generation.or_else(|| { + event + .data + .get("world_generation") + .and_then(serde_json::Value::as_u64) + }); if maybe_world_id.is_some() && maybe_world_generation.is_some() { world_id = maybe_world_id; world_generation = maybe_world_generation; diff --git a/crates/shell/src/execution/routing/telemetry.rs b/crates/shell/src/execution/routing/telemetry.rs index 6746c66e0..95165b382 100644 --- a/crates/shell/src/execution/routing/telemetry.rs +++ b/crates/shell/src/execution/routing/telemetry.rs @@ -189,7 +189,7 @@ fn append_agent_event_to_trace(config: &ShellConfig, event: &AgentEvent) -> Resu let channel = sanitized.channel.take(); sanitized.set_channel(channel); - let mut entry = serde_json::to_value(&sanitized)?; + let mut entry = sanitized.to_trace_record()?; let obj = entry .as_object_mut() .ok_or_else(|| anyhow::anyhow!("agent event must serialize as a JSON object"))?; diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index 9bd9c488b..d43cf4ac2 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -1388,7 +1388,9 @@ fn emit_world_restarted_alert( ); event.role = Some("orchestrator".to_string()); event.backend_id = Some("shell:repl".to_string()); - event.world_id = Some(previous_world_id.to_string()); + event.world_id = Some(new_world_id.to_string()); + event.world_generation = Some(new_world_generation); + event.set_pure_agent_telemetry_identity("shell"); if let Some(data) = event.data.as_object_mut() { data.insert("reason".to_string(), serde_json::json!(reason.code())); @@ -1426,6 +1428,8 @@ fn build_world_restart_required_alert( event.role = Some("orchestrator".to_string()); event.backend_id = Some("shell:repl".to_string()); event.world_id = Some(current_world_id.to_string()); + event.world_generation = Some(current_world_generation); + event.set_pure_agent_telemetry_identity("shell"); if let Some(data) = event.data.as_object_mut() { data.insert("reason".to_string(), serde_json::json!(reason.code())); diff --git a/crates/world-agent/src/service.rs b/crates/world-agent/src/service.rs index 6c2d3c328..8a373004d 100644 --- a/crates/world-agent/src/service.rs +++ b/crates/world-agent/src/service.rs @@ -1324,6 +1324,7 @@ impl WorldAgentService { thread_id: None, role: None, world_id: Some(world.id.clone()), + world_generation: None, cmd_id: None, span_id: Some(span_id.clone()), channel: None, From b7ec0f55b3630e5695019825cfb8996f1e2482e0 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:03:25 -0400 Subject: [PATCH 17/48] task: AHCSITC2-test --- .../agent_successor_contract_ahcsitc0.rs | 455 +++++++++++++++++- .../tests/repl_world_first_routing_v1.rs | 70 ++- 2 files changed, 500 insertions(+), 25 deletions(-) diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index e1a4a4943..27fcde59c 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -3,7 +3,7 @@ mod common; use common::{substrate_shell_driver, temp_dir}; -use serde_json::Value; +use serde_json::{json, Value}; use std::fs; use std::path::PathBuf; use std::process::Output; @@ -72,6 +72,16 @@ impl AgentSuccessorFixture { fs::write(agents_dir.join(file_name), contents).expect("failed to write agent file"); } + fn write_trace_events(&self, events: &[Value]) { + let trace = self.substrate_home.join("trace.jsonl"); + let body = events + .iter() + .map(|event| serde_json::to_string(event).expect("serialize trace event")) + .collect::>() + .join("\n"); + fs::write(trace, format!("{body}\n")).expect("failed to write trace.jsonl"); + } + fn run(&self, args: &[&str]) -> Output { self.command() .current_dir(&self.workspace_root) @@ -460,3 +470,446 @@ fn docs_usage_and_repo_boundary_match_the_successor_contract() { "AHCSITC0 must not introduce a new crates/agent-hub package" ); } + +#[test] +fn agent_doctor_fails_closed_on_world_member_allowlist_before_world_boundary() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"world: + enabled: false +agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + fixture.write_agent_file( + "codex.yaml", + &cli_agent_file("codex", "world", true, false, true), + ); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_eq!( + output.status.code(), + Some(5), + "doctor should fail on the member backend allowlist before world boundary checks: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!(json.get("healthy").and_then(Value::as_bool), Some(false)); + assert_eq!(json.get("fail_closed").and_then(Value::as_bool), Some(true)); + + let checks = json["checks"] + .as_array() + .expect("checks should be an array"); + let observed: Vec<&str> = checks + .iter() + .map(|check| { + check + .pointer("/check") + .and_then(Value::as_str) + .expect("check id should be a string") + }) + .collect(); + assert_eq!( + observed, + vec!["inventory_scan", "orchestrator_selection", "policy_allowlist"], + "fail-closed routing must stop at the member backend allowlist before world_boundary: {json}" + ); + assert_eq!( + checks[2].pointer("/reason").and_then(Value::as_str), + Some( + "required world-scoped member backend 'cli:codex' is not allowlisted by effective policy agents.allowed_backends" + ), + "member dispatch must be gated by the derived backend_id before world boundary handling: {json}" + ); +} + +#[test] +fn agent_doctor_does_not_treat_trace_records_as_control_plane_authorization() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: "" +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_trace_events(&[json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_trace_only", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "data": { "message": "trace says the orchestrator is healthy" } + })]); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_eq!( + output.status.code(), + Some(2), + "trace-only observation must not authorize orchestrator selection: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!(json.get("healthy").and_then(Value::as_bool), Some(false)); + let checks = json["checks"] + .as_array() + .expect("checks should be an array"); + assert_eq!( + checks[1].pointer("/check").and_then(Value::as_str), + Some("orchestrator_selection"), + "doctor must still fail at orchestrator selection even when trace records look healthy: {json}" + ); + assert_eq!( + checks[1].pointer("/status").and_then(Value::as_str), + Some("fail") + ); + assert_eq!( + checks[1].pointer("/reason").and_then(Value::as_str), + Some("agents.hub.orchestrator_agent_id must select an orchestrator"), + "event-plane or trace-plane records must not back-authorize control-plane actions: {json}" + ); +} + +#[test] +fn agent_status_uses_top_level_tuple_fields_for_pure_and_nested_records() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "world", true, true, true), + ); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "agent status should stay readable with tuple-compatible trace records: {output:?}" + ); + + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + sessions.len(), + 1, + "pure-agent records must still project into the status session surface from top-level tuple fields: {json}" + ); + let session = &sessions[0]; + assert_eq!( + session.pointer("/client").and_then(Value::as_str), + Some("claude_code") + ); + assert_eq!( + session.pointer("/router").and_then(Value::as_str), + Some("agent_hub") + ); + assert_eq!( + session.pointer("/protocol").and_then(Value::as_str), + Some("uaa.agent.session") + ); + assert_eq!( + session.pointer("/world_id").and_then(Value::as_str), + Some("wld_active_0002"), + "world-scoped pure-agent records must publish world_id at the top level: {json}" + ); + assert_eq!( + session.pointer("/world_generation").and_then(Value::as_u64), + Some(7), + "world_generation must stay top-level on world-scoped pure-agent records: {json}" + ); + assert!( + session.get("provider").is_none() && session.get("auth_authority").is_none(), + "pure-agent status sessions must omit nested gateway tuple fields: {session}" + ); + + let nested = json["nested_llm_records"] + .as_array() + .expect("nested_llm_records should be an array"); + assert_eq!( + nested.len(), + 1, + "nested gateway-backed records must remain distinct from pure-agent session rows: {json}" + ); + let record = &nested[0]; + assert_eq!( + record.pointer("/client").and_then(Value::as_str), + Some("claude_code") + ); + assert_eq!( + record.pointer("/router").and_then(Value::as_str), + Some("substrate_gateway") + ); + assert_eq!( + record.pointer("/protocol").and_then(Value::as_str), + Some("openai.responses") + ); + assert_eq!( + record.pointer("/provider").and_then(Value::as_str), + Some("openai") + ); + assert_eq!( + record.pointer("/auth_authority").and_then(Value::as_str), + Some("codex_subscription") + ); + assert!( + record.get("world_id").is_none() && record.get("world_generation").is_none(), + "nested gateway-backed records must omit world scope fields: {record}" + ); +} + +#[test] +fn nested_gateway_policy_does_not_inherit_agent_hub_success_by_implication() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +llm: + enabled: true + gateway: + enabled: true + routing: + default_backend: cli:codex +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +llm: + allowed_backends: + - "api:openai" + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + fixture.write_agent_file( + "codex.yaml", + &cli_agent_file("codex", "host", true, false, true), + ); + + let doctor = fixture.run(&["agent", "doctor", "--json"]); + assert!( + doctor.status.success(), + "agent doctor must succeed for the host-scoped orchestrator fixture: {doctor:?}" + ); + let doctor_json = parse_json_output(&doctor); + assert_eq!( + doctor_json.get("healthy").and_then(Value::as_bool), + Some(true) + ); + + let missing_socket = fixture.workspace_root.join("missing-world.sock"); + let output = fixture + .command() + .current_dir(&fixture.workspace_root) + .env_remove("SUBSTRATE_OVERRIDE_WORLD") + .env("SUBSTRATE_WORLD_ENABLED", "1") + .env("SUBSTRATE_WORLD", "enabled") + .env("SUBSTRATE_WORLD_SOCKET", &missing_socket) + .args(["world", "gateway", "status"]) + .output() + .expect("failed to run world gateway status"); + + assert_eq!( + output.status.code(), + Some(5), + "nested gateway approval must remain an independent policy surface after agent-hub success: {output:?}" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("cli:codex is not allowlisted by effective policy llm.allowed_backends"), + "nested gateway denial must come from llm.allowed_backends, not implied success: {stderr}" + ); + assert!( + !stderr.contains("required gateway/world component unavailable"), + "gateway allowlist denial must happen before socket/runtime checks: {stderr}" + ); +} diff --git a/crates/shell/tests/repl_world_first_routing_v1.rs b/crates/shell/tests/repl_world_first_routing_v1.rs index f97a016b2..fda56ad87 100644 --- a/crates/shell/tests/repl_world_first_routing_v1.rs +++ b/crates/shell/tests/repl_world_first_routing_v1.rs @@ -839,8 +839,13 @@ fn c3_drift_restart_restarts_session_and_emits_message() { ); assert_eq!( alert.get("world_id").and_then(Value::as_str), - Some("wld_stub_0001"), - "top-level world_id must point at the pre-restart world: {alert:?}" + Some("wld_stub_0002"), + "top-level world_id must point at the active replacement world: {alert:?}" + ); + assert_eq!( + alert.get("world_generation").and_then(Value::as_u64), + Some(1), + "top-level world_generation must point at the active replacement generation: {alert:?}" ); assert_eq!( alert.pointer("/data/reason").and_then(Value::as_str), @@ -878,6 +883,10 @@ fn c3_drift_restart_restarts_session_and_emits_message() { Some(1), "restart alert must capture new generation 1: {alert:?}" ); + assert!( + alert.pointer("/data/world_id").is_none() && alert.pointer("/data/world_generation").is_none(), + "restart alerts must keep active world identity at the top level, not duplicate it under data: {alert:?}" + ); } #[test] @@ -938,6 +947,16 @@ fn c3_startup_drift_restart_emits_world_restarted_alert() { ); let alert = alerts[0]; + assert_eq!( + alert.get("world_id").and_then(Value::as_str), + Some("wld_stub_0002"), + "startup restart alert must publish the active replacement world at the top level: {alert:?}" + ); + assert_eq!( + alert.get("world_generation").and_then(Value::as_u64), + Some(1), + "startup restart alert must publish the replacement generation at the top level: {alert:?}" + ); assert_eq!( alert.pointer("/data/reason").and_then(Value::as_str), Some("workspace_root_changed"), @@ -969,6 +988,10 @@ fn c3_startup_drift_restart_emits_world_restarted_alert() { Some(1), "startup drift alert must increment to generation 1: {alert:?}" ); + assert!( + alert.pointer("/data/world_id").is_none() && alert.pointer("/data/world_generation").is_none(), + "startup restart alerts must keep active world identity at the top level, not under data: {alert:?}" + ); } #[test] @@ -1079,6 +1102,11 @@ fn c3_drift_fail_closed_emits_world_restart_required_alert() { Some("wld_stub_0001"), "top-level world_id must point at the current world: {alert:?}" ); + assert_eq!( + alert.get("world_generation").and_then(Value::as_u64), + Some(0), + "top-level world_generation must publish the current generation for fail-closed restart-required alerts: {alert:?}" + ); assert_eq!( alert.pointer("/data/reason").and_then(Value::as_str), Some("policy_snapshot_changed"), @@ -1096,17 +1124,9 @@ fn c3_drift_fail_closed_emits_world_restart_required_alert() { Some("restart_world"), "restart-required alert must record required_action=restart_world: {alert:?}" ); - assert_eq!( - alert.pointer("/data/world_id").and_then(Value::as_str), - Some("wld_stub_0001"), - "restart-required alert must capture the current world_id: {alert:?}" - ); - assert_eq!( - alert - .pointer("/data/world_generation") - .and_then(Value::as_u64), - Some(0), - "restart-required alert must capture the current generation: {alert:?}" + assert!( + alert.pointer("/data/world_id").is_none() && alert.pointer("/data/world_generation").is_none(), + "restart-required alerts must keep current world identity at the top level when known: {alert:?}" ); } @@ -1187,6 +1207,16 @@ fn c3_startup_drift_fail_closed_emits_world_restart_required_alert() { ); let alert = alerts[0]; + assert_eq!( + alert.get("world_id").and_then(Value::as_str), + Some("wld_stub_0001"), + "startup fail-closed alert must publish the current world id at the top level: {alert:?}" + ); + assert_eq!( + alert.get("world_generation").and_then(Value::as_u64), + Some(0), + "startup fail-closed alert must publish the current generation at the top level: {alert:?}" + ); assert_eq!( alert.pointer("/data/reason").and_then(Value::as_str), Some("workspace_root_changed"), @@ -1204,17 +1234,9 @@ fn c3_startup_drift_fail_closed_emits_world_restart_required_alert() { Some("restart_world"), "startup fail-closed alert must record required_action=restart_world: {alert:?}" ); - assert_eq!( - alert.pointer("/data/world_id").and_then(Value::as_str), - Some("wld_stub_0001"), - "startup fail-closed alert must capture the current world id: {alert:?}" - ); - assert_eq!( - alert - .pointer("/data/world_generation") - .and_then(Value::as_u64), - Some(0), - "startup fail-closed alert must capture the current generation: {alert:?}" + assert!( + alert.pointer("/data/world_id").is_none() && alert.pointer("/data/world_generation").is_none(), + "startup fail-closed alerts must keep current world identity at the top level when known: {alert:?}" ); } From c37317d705ca9b0b1c1212b609041d49cc37622e Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:05:00 -0400 Subject: [PATCH 18/48] docs: allow AHCSITC2 code touch set --- .../pre-planning/impact_map.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md index da1d2919b..819e25015 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md @@ -80,11 +80,13 @@ Strict packs (`tasks.json` → `meta.slice_spec_version >= 2`) requirements: - `crates/shell/src/execution/agent_inventory.rs` - `crates/shell/src/execution/agent_events.rs` - `crates/shell/src/execution/routing/telemetry.rs` +- `crates/shell/src/repl/async_repl.rs` - `crates/common/src/agent_events.rs` - `crates/common/src/lib.rs` - `crates/common/tests/agent_hub_event_envelope_schema.rs` - `crates/trace/src/span.rs` - `crates/trace/src/tests.rs` +- `crates/world-agent/src/service.rs` - `crates/agent-api-types/src/lib.rs` - `crates/agent-api-client/src/lib.rs` - `crates/agent-api-core/src/lib.rs` From 846020b49bceceba965d23564e37d976f1375787 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:07:55 -0400 Subject: [PATCH 19/48] docs: finish AHCSITC2 code+test --- .../session_log.md | 8 ++++++++ .../tasks.json | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index e54c0ac86..b59bd89f4 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -169,3 +169,11 @@ ## START — 2026-04-25T03:50:28Z — test — AHCSITC2-test - Dispatch: - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC2"` + +## END — 2026-04-25T04:07:55Z — code — AHCSITC2-code +- HEAD: `66c56b74362470d7b0891a7692cf1fca1b28b263` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/code/last_message.md` + +## END — 2026-04-25T04:07:55Z — test — AHCSITC2-test +- HEAD: `b7ec0f55b3630e5695019825cfb8996f1e2482e0` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/test/last_message.md` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 9fa0a64ad..004ec5e4f 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -398,7 +398,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" ], - "status": "in_progress", + "status": "completed", "type": "code", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-code" }, @@ -452,7 +452,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" ], - "status": "in_progress", + "status": "completed", "type": "test", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-test" }, From 6844cd5d8dca0094c45bcb01881dcb55591ae4d1 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:07:55 -0400 Subject: [PATCH 20/48] docs: start AHCSITC2-integ-core --- .../session_log.md | 4 ++++ .../tasks.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index b59bd89f4..6af035866 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -177,3 +177,7 @@ ## END — 2026-04-25T04:07:55Z — test — AHCSITC2-test - HEAD: `b7ec0f55b3630e5695019825cfb8996f1e2482e0` - Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/test/last_message.md` + +## START — 2026-04-25T04:07:55Z — integration — AHCSITC2-integ-core +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC2-integ-core" LAUNCH_CODEX=1` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 004ec5e4f..b79ca2045 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -503,7 +503,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-core\"" ], - "status": "pending", + "status": "in_progress", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-core" }, From 5d8644570b17903550c7992eb931aa9adbec048c Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:17:11 -0400 Subject: [PATCH 21/48] fix: reconcile AHCSITC2 integration telemetry --- crates/common/src/agent_events.rs | 25 ++++++++++++++++++++++++- crates/shell/src/repl/async_repl.rs | 5 ----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/crates/common/src/agent_events.rs b/crates/common/src/agent_events.rs index e31dacfbf..d053bc4f0 100644 --- a/crates/common/src/agent_events.rs +++ b/crates/common/src/agent_events.rs @@ -139,6 +139,16 @@ struct AgentEventDef { #[serde(default, deserialize_with = "deserialize_sanitized_channel")] channel: Option, #[serde(default)] + client: Option, + #[serde(default)] + router: Option, + #[serde(default)] + protocol: Option, + #[serde(default)] + provider: Option, + #[serde(default)] + auth_authority: Option, + #[serde(default)] identity_tuple: Option, #[serde(default)] placement_posture: Option, @@ -327,6 +337,19 @@ impl TryFrom for AgentEvent { type Error = String; fn try_from(value: AgentEventDef) -> Result { + let identity_tuple = value.identity_tuple.or_else(|| { + let client = value.client?; + let router = value.router?; + let protocol = value.protocol?; + Some(IdentityTuple { + client, + router, + protocol, + provider: value.provider, + auth_authority: value.auth_authority, + }) + }); + let event = Self { ts: value.ts, kind: value.kind, @@ -342,7 +365,7 @@ impl TryFrom for AgentEvent { cmd_id: value.cmd_id, span_id: value.span_id, channel: value.channel, - identity_tuple: value.identity_tuple, + identity_tuple, placement_posture: value.placement_posture, project: value.project, }; diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index d43cf4ac2..ffc4388fc 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -1438,11 +1438,6 @@ fn build_world_restart_required_alert( serde_json::json!("restart_world"), ); data.insert("on_drift".to_string(), serde_json::json!("fail_closed")); - data.insert("world_id".to_string(), serde_json::json!(current_world_id)); - data.insert( - "world_generation".to_string(), - serde_json::json!(current_world_generation), - ); } event From 277d48adca56ce2f212dbf6574c05c5efe53a43b Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:26:12 -0400 Subject: [PATCH 22/48] docs: finish AHCSITC2-integ-core --- .../session_log.md | 4 ++++ .../tasks.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 6af035866..332e9eeef 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -181,3 +181,7 @@ ## START — 2026-04-25T04:07:55Z — integration — AHCSITC2-integ-core - Dispatch: - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC2-integ-core" LAUNCH_CODEX=1` + +## END — 2026-04-25T04:26:12Z — integration — AHCSITC2-integ-core +- HEAD: `5d8644570b17903550c7992eb931aa9adbec048c` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/integ-core/last_message.md` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index b79ca2045..b2f0d4d1d 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -503,7 +503,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-core\"" ], - "status": "in_progress", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-core" }, From 07383edb7013ace750547d0350af8cc84856e53a Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:28:05 -0400 Subject: [PATCH 23/48] docs: start CP1-ci-checkpoint --- .../session_log.md | 4 ++++ .../tasks.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 332e9eeef..44cba4084 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -185,3 +185,7 @@ ## END — 2026-04-25T04:26:12Z — integration — AHCSITC2-integ-core - HEAD: `5d8644570b17903550c7992eb931aa9adbec048c` - Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/integ-core/last_message.md` + +## START — 2026-04-25T04:27:47Z — checkpoint — CP1-ci-checkpoint +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="CP1-ci-checkpoint"` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index b2f0d4d1d..308f1eeea 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -542,7 +542,7 @@ "Read pre-planning/ci_checkpoint_plan.md, tasks.json, session_log.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs." ], - "status": "pending", + "status": "in_progress", "type": "ops", "worktree": null }, From d29c131d5e684c8aefb6e0eef591ed17189c4a9c Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:33:23 -0400 Subject: [PATCH 24/48] docs: finish CP1-ci-checkpoint --- .../session_log.md | 14 ++++++++++++++ .../tasks.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 44cba4084..764aeb1d9 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -189,3 +189,17 @@ ## START — 2026-04-25T04:27:47Z — checkpoint — CP1-ci-checkpoint - Dispatch: - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="CP1-ci-checkpoint"` + +## END — 2026-04-25T04:30:34Z — checkpoint — CP1-ci-checkpoint +- Candidate SHA: `5d8644570b17903550c7992eb931aa9adbec048c` +- Advisory CI audit: + - `scripts/ci-audit/ci_audit.sh --ledger-path "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/ci-audit/ledger.jsonl" --kind ci-testing --orch-branch "feat/agent-hub-core-successor-identity-tuple-compatible" --head-sha "5d8644570b17903550c7992eb931aa9adbec048c"` → `RECOMMEND=run` (`REASON=no_last_green_run_found`) +- Compile parity: + - `make ci-compile-parity CI_WORKFLOW_REF="feat/agent-hub-core-successor-identity-tuple-compatible" CI_REMOTE=origin CI_CLEANUP=1 CI_CHECKOUT_REF="5d8644570b17903550c7992eb931aa9adbec048c"` → `RUN_ID=24922568397` → `RUN_URL=https://github.com/atomize-hq/substrate/actions/runs/24922568397` → `CONCLUSION=success` + - Passed OSes: `macos-14,ubuntu-24.04,windows-2022` + - Failed OSes: `NONE` + - Failed jobs: `NONE` +- CI audit ledger: + - `scripts/ci-audit/ci_audit_record.sh --ledger-path "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/ci-audit/ledger.jsonl" --kind ci-testing --mode compile-parity --orch-branch "feat/agent-hub-core-successor-identity-tuple-compatible" --run-id "24922568397" --tested-sha "5d8644570b17903550c7992eb931aa9adbec048c"` → `RECORDED=1` +- Feature smoke: + - Not dispatched. `pre-planning/ci_checkpoint_plan.md` and `CP1-ci-checkpoint.md` keep `feature_smoke=false` for this checkpoint, including Windows. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 308f1eeea..29f75ba26 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -542,7 +542,7 @@ "Read pre-planning/ci_checkpoint_plan.md, tasks.json, session_log.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs." ], - "status": "in_progress", + "status": "completed", "type": "ops", "worktree": null }, From 486a3dc37df30b73a8d5f88ce9e3abe8540c4048 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:33:46 -0400 Subject: [PATCH 25/48] docs: mark AHCSITC2 parity no-ops --- .../tasks.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 29f75ba26..9f2adec96 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -590,7 +590,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-linux\"" ], - "status": "pending", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-linux" }, @@ -638,7 +638,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-macos\"" ], - "status": "pending", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-macos" }, @@ -686,7 +686,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-windows\"" ], - "status": "pending", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-windows" }, From 56b9e019269a78326eceed5450c18d07bc2f57dc Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 00:46:32 -0400 Subject: [PATCH 26/48] docs: finish AHCSITC2-integ --- .../session_log.md | 10 ++++++++++ .../tasks.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 764aeb1d9..30d1bdaf0 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -203,3 +203,13 @@ - `scripts/ci-audit/ci_audit_record.sh --ledger-path "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/ci-audit/ledger.jsonl" --kind ci-testing --mode compile-parity --orch-branch "feat/agent-hub-core-successor-identity-tuple-compatible" --run-id "24922568397" --tested-sha "5d8644570b17903550c7992eb931aa9adbec048c"` → `RECORDED=1` - Feature smoke: - Not dispatched. `pre-planning/ci_checkpoint_plan.md` and `CP1-ci-checkpoint.md` keep `feature_smoke=false` for this checkpoint, including Windows. + +## START — 2026-04-25T04:33:55Z — integration-final — AHCSITC2-integ +- Dispatch: + - `make triad-task-start-integ-final FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC2" LAUNCH_CODEX=1` + +## END — 2026-04-25T04:43:26Z — integration-final — AHCSITC2-integ +- HEAD: `61cdc894150b0d9bc197ce5bdf1b211b7e01bda0` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/integ/last_message.md` +- Merge result: + - `make triad-task-finish TASK_ID="AHCSITC2-integ"` → `MERGED_TO_ORCH=true` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 9f2adec96..b1f5ab5ac 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -749,7 +749,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ\"" ], - "status": "pending", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ" }, From e6d68d7b0d099906559b77d5ef1df72a9806ba48 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 06:58:56 -0400 Subject: [PATCH 27/48] docs: start AHCSITC3 code+test --- .../session_log.md | 8 ++++++++ .../tasks.json | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 30d1bdaf0..521bdd17e 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -213,3 +213,11 @@ - Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/integ/last_message.md` - Merge result: - `make triad-task-finish TASK_ID="AHCSITC2-integ"` → `MERGED_TO_ORCH=true` + +## START — 2026-04-25T10:58:56Z — code — AHCSITC3-code +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC3"` + +## START — 2026-04-25T10:58:56Z — test — AHCSITC3-test +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC3"` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index b1f5ab5ac..7fd1b408a 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -804,7 +804,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" ], - "status": "pending", + "status": "in_progress", "type": "code", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-code" }, @@ -859,7 +859,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" ], - "status": "pending", + "status": "in_progress", "type": "test", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-test" }, From 19b972a20b747098ddd9df2bc503c60fadfdeb47 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:03:47 -0400 Subject: [PATCH 28/48] test: lock ahcsitc3 closeout surfaces --- .../agent_successor_contract_ahcsitc0.rs | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index 27fcde59c..bcbbb026d 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -160,6 +160,11 @@ fn repo_root() -> PathBuf { .expect("repo root should exist") } +fn read_repo_file(path: &str) -> String { + fs::read_to_string(repo_root().join(path)) + .unwrap_or_else(|err| panic!("failed to read {path}: {err}")) +} + fn parse_json_output(output: &Output) -> Value { serde_json::from_slice(&output.stdout).expect("stdout should be valid JSON") } @@ -440,8 +445,7 @@ fn agent_doctor_json_locks_field_names_omissions_and_check_order() { #[test] fn docs_usage_and_repo_boundary_match_the_successor_contract() { - let usage = fs::read_to_string(repo_root().join("docs/USAGE.md")) - .expect("docs/USAGE.md should be readable"); + let usage = read_repo_file("docs/USAGE.md"); assert!( usage.contains("substrate agent list"), "docs/USAGE.md must document the canonical singular list command" @@ -471,6 +475,107 @@ fn docs_usage_and_repo_boundary_match_the_successor_contract() { ); } +#[test] +fn ahcsitc3_specs_lock_supersession_parity_and_validation_boundaries() { + let compatibility = read_repo_file( + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/compatibility-spec.md", + ); + for required in [ + "ADR-0025 is historical evidence only", + "ADR-0025 may be cited only as superseded historical evidence.", + "Existing `agents.allowed_backends` entries remain valid without rewriting because `backend_id` stays derived as `:`.", + "`substrate agents validate` remains supported as an additive compatibility leaf for inventory validation only.", + "`backend_id` remains the agent-side adapter identifier and allowlist token", + ] { + assert!( + compatibility.contains(required), + "compatibility-spec.md must lock AHCSITC3 closeout rule `{required}`" + ); + } + + let parity = read_repo_file( + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/platform-parity-spec.md", + ); + for required in [ + "| Linux |", + "| macOS |", + "| Windows |", + "`substrate agents validate` remains a compatibility leaf on every platform and never becomes an alias for list, status, or doctor.", + "Nested gateway-backed LLM records always omit `world_id` and `world_generation` on every platform.", + "If the effective command path requires a world-scoped member posture and the required world boundary is temporarily unavailable, `substrate agent doctor` returns exit `3`.", + "If the current build or platform cannot satisfy the required world posture at all, `substrate agent doctor` returns exit `4`.", + "`crates/shell/tests/agents_validate.rs`", + "`crates/shell/tests/agent_hub_trace_persistence.rs`", + "`crates/shell/tests/repl_world_first_routing_v1.rs`", + "`scripts/linux/world-provision.sh`", + "`scripts/mac/lima-warm.sh`", + "`scripts/mac/smoke.sh`", + "`scripts/windows/wsl-warm.ps1`", + "`scripts/windows/wsl-smoke.ps1`", + ] { + assert!( + parity.contains(required), + "platform-parity-spec.md must lock AHCSITC3 parity evidence `{required}`" + ); + } + + let playbook = read_repo_file( + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", + ); + for required in [ + "### Case 1 — `substrate agent list --json` keeps adapter identity and omission rules", + "### Case 2 — `substrate agent status --json` proves a host-scoped orchestrator", + "### Case 3 — world-scoped members publish `world_id` and `world_generation`", + "### Case 4 — nested gateway-backed records publish `provider` and `auth_authority` on the nested record only", + "### Case 5 — canonical trace keeps the same pure-agent versus nested-record split", + "### Case 6 — `substrate agent doctor --json` proves healthy ordered checks", + "### Case 7 — `substrate agent doctor` fails closed for invalid orchestrator state", + "### Case 8 — `substrate agent doctor` fails closed for world-boundary loss", + "`crates/shell/tests/agents_validate.rs`", + "`crates/shell/tests/agent_hub_trace_persistence.rs`", + "`crates/shell/tests/repl_world_first_routing_v1.rs`", + ] { + assert!( + playbook.contains(required), + "manual_testing_playbook.md must keep AHCSITC3 validation coverage `{required}`" + ); + } +} + +#[test] +fn ahcsitc3_configuration_doc_locks_successor_config_surface() { + let configuration = read_repo_file("docs/CONFIGURATION.md"); + for required in [ + "agents.hub.orchestrator_agent_id", + "agents.allowed_backends", + ] { + assert!( + configuration.contains(required), + "docs/CONFIGURATION.md must document successor config surface `{required}`" + ); + } +} + +#[test] +fn ahcsitc3_trace_doc_locks_tuple_compatible_fields() { + let trace = read_repo_file("docs/TRACE.md"); + for required in [ + "`backend_id`", + "`client`", + "`router`", + "`protocol`", + "`provider`", + "`auth_authority`", + "`world_id`", + "`world_generation`", + ] { + assert!( + trace.contains(required), + "docs/TRACE.md must document tuple-compatible trace field `{required}`" + ); + } +} + #[test] fn agent_doctor_fails_closed_on_world_member_allowlist_before_world_boundary() { let fixture = AgentSuccessorFixture::new(); From 9a63080ef24f49e16b1368f2e741b0f40e9047b3 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:06:29 -0400 Subject: [PATCH 29/48] fix: align agent doctor world boundary checks --- crates/shell/src/execution/agents_cmd.rs | 178 ++++++++++++++++++++++- 1 file changed, 170 insertions(+), 8 deletions(-) diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 1c1e68bc3..9915c055b 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -16,6 +16,7 @@ use std::env; use std::fs::File; use std::io::{self, BufRead, BufReader}; use std::path::{Path, PathBuf}; +use std::process::Command; use substrate_broker::Policy; use substrate_common::paths as substrate_paths; use substrate_common::{AgentEvent, PlacementExecution}; @@ -723,6 +724,13 @@ struct DoctorReportJson<'a> { #[serde(skip_serializing_if = "Option::is_none")] orchestrator: Option>, checks: Vec, + #[serde(skip)] + world_boundary_exit_code: Option, +} + +enum RequiredWorldBoundaryState { + Ready, + Failed { reason: String, exit_code: i32 }, } fn build_doctor_report(cli: &Cli) -> Result> { @@ -802,6 +810,7 @@ fn build_doctor_report(cli: &Cli) -> Result> { fail_closed: true, orchestrator: None, checks, + world_boundary_exit_code: None, }); } }; @@ -817,6 +826,7 @@ fn build_doctor_report(cli: &Cli) -> Result> { fail_closed: true, orchestrator: Some(orchestrator), checks, + world_boundary_exit_code: None, }); } checks.push(DoctorCheckJson { @@ -826,13 +836,7 @@ fn build_doctor_report(cli: &Cli) -> Result> { }); if enabled_world_member_exists(&inventory, &effective_config) { - if effective_config.world.enabled { - checks.push(DoctorCheckJson { - check: "world_boundary".to_string(), - status: "pass".to_string(), - reason: None, - }); - } else { + if !effective_config.world.enabled { checks.push(DoctorCheckJson { check: "world_boundary".to_string(), status: "fail".to_string(), @@ -846,8 +850,33 @@ fn build_doctor_report(cli: &Cli) -> Result> { fail_closed: true, orchestrator: Some(orchestrator), checks, + world_boundary_exit_code: Some(3), }); } + + match verify_required_world_boundary(cli) { + RequiredWorldBoundaryState::Ready => { + checks.push(DoctorCheckJson { + check: "world_boundary".to_string(), + status: "pass".to_string(), + reason: None, + }); + } + RequiredWorldBoundaryState::Failed { reason, exit_code } => { + checks.push(DoctorCheckJson { + check: "world_boundary".to_string(), + status: "fail".to_string(), + reason: Some(reason), + }); + return Ok(DoctorReportJson { + healthy: false, + fail_closed: true, + orchestrator: Some(orchestrator), + checks, + world_boundary_exit_code: Some(exit_code), + }); + } + } } else { checks.push(DoctorCheckJson { check: "world_boundary".to_string(), @@ -861,6 +890,7 @@ fn build_doctor_report(cli: &Cli) -> Result> { fail_closed: false, orchestrator: Some(orchestrator), checks, + world_boundary_exit_code: None, }) } @@ -878,6 +908,138 @@ fn failed_doctor_report( status: "fail".to_string(), reason: Some(reason), }], + world_boundary_exit_code: None, + } +} + +fn verify_required_world_boundary(cli: &Cli) -> RequiredWorldBoundaryState { + let exe = match env::current_exe() { + Ok(path) => path, + Err(err) => { + return RequiredWorldBoundaryState::Failed { + reason: format!( + "failed to resolve the substrate executable for required world-boundary validation: {err}" + ), + exit_code: 3, + } + } + }; + + let mut command = Command::new(exe); + if cli.world { + command.arg("--world"); + } else if cli.no_world { + command.arg("--no-world"); + } + + let output = match command.args(["world", "doctor", "--json"]).output() { + Ok(output) => output, + Err(err) => { + return RequiredWorldBoundaryState::Failed { + reason: format!( + "failed to run `substrate world doctor --json` for required world-boundary validation: {err}" + ), + exit_code: 3, + } + } + }; + + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let value: serde_json::Value = match serde_json::from_slice(&output.stdout) { + Ok(value) => value, + Err(err) => { + let reason = if stderr.is_empty() { + format!( + "required world-boundary validation returned invalid JSON from `substrate world doctor --json`: {err}" + ) + } else { + format!( + "required world-boundary validation returned invalid JSON from `substrate world doctor --json`: {err}; stderr: {stderr}" + ) + }; + return RequiredWorldBoundaryState::Failed { + reason, + exit_code: 3, + }; + } + }; + + let world_status = value + .pointer("/world/status") + .and_then(serde_json::Value::as_str); + let ok = output.status.success() + && value + .get("ok") + .and_then(serde_json::Value::as_bool) + .unwrap_or(true); + + if ok { + return RequiredWorldBoundaryState::Ready; + } + + let exit_code = classify_world_boundary_exit_code(output.status.code(), world_status); + RequiredWorldBoundaryState::Failed { + reason: format_world_boundary_failure_reason(&value, world_status, &stderr, exit_code), + exit_code, + } +} + +fn classify_world_boundary_exit_code( + process_exit_code: Option, + world_status: Option<&str>, +) -> i32 { + match process_exit_code { + Some(3) => 3, + Some(4) => 4, + _ => match world_status { + Some("unreachable") => 3, + Some("not_provisioned") | Some("missing_prereqs") | Some("unsupported") => 4, + _ => 3, + }, + } +} + +fn format_world_boundary_failure_reason( + value: &serde_json::Value, + world_status: Option<&str>, + stderr: &str, + exit_code: i32, +) -> String { + let summary = match exit_code { + 4 => "required world-scoped member posture is unsupported or not provisioned on this platform/build", + _ => "required world-scoped member boundary is unavailable", + }; + + let detail = value + .get("world_disable_reason") + .and_then(serde_json::Value::as_str) + .or_else(|| { + value + .pointer("/host/world_socket/probe_error") + .and_then(serde_json::Value::as_str) + }) + .or_else(|| { + value + .pointer("/host/error") + .and_then(serde_json::Value::as_str) + }) + .or_else(|| { + value + .pointer("/world/landlock/reason") + .and_then(serde_json::Value::as_str) + }) + .or_else(|| { + value + .pointer("/world/world_fs_strategy/probe/failure_reason") + .and_then(serde_json::Value::as_str) + }) + .or_else(|| (!stderr.is_empty()).then_some(stderr)); + + match (world_status, detail) { + (Some(status), Some(detail)) => format!("{summary} (world.status={status}): {detail}"), + (Some(status), None) => format!("{summary} (world.status={status})"), + (None, Some(detail)) => format!("{summary}: {detail}"), + (None, None) => summary.to_string(), } } @@ -966,7 +1128,7 @@ fn doctor_exit_code(report: &DoctorReportJson<'_>) -> i32 { match failed_check { "policy_allowlist" => 5, - "world_boundary" => 3, + "world_boundary" => report.world_boundary_exit_code.unwrap_or(3), "inventory_scan" | "orchestrator_selection" => 2, _ => 1, } From 01ee9d5973c5c0c2d39164ebd3aed7cf0c79bbe6 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:09:49 -0400 Subject: [PATCH 30/48] docs: finish AHCSITC3 code+test --- .../session_log.md | 8 ++++++++ .../tasks.json | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 521bdd17e..5c3b09e5e 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -221,3 +221,11 @@ ## START — 2026-04-25T10:58:56Z — test — AHCSITC3-test - Dispatch: - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC3"` + +## END — 2026-04-25T11:09:49Z — code — AHCSITC3-code +- HEAD: `9a63080ef24f49e16b1368f2e741b0f40e9047b3` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/code/last_message.md` + +## END — 2026-04-25T11:09:49Z — test — AHCSITC3-test +- HEAD: `19b972a20b747098ddd9df2bc503c60fadfdeb47` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/test/last_message.md` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 7fd1b408a..c40d848c8 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -804,7 +804,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" ], - "status": "in_progress", + "status": "completed", "type": "code", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-code" }, @@ -859,7 +859,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" ], - "status": "in_progress", + "status": "completed", "type": "test", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-test" }, From d310b0a5c943dce509dafd0c2068746b588e458d Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:09:49 -0400 Subject: [PATCH 31/48] docs: start AHCSITC3-integ-core --- .../session_log.md | 4 ++++ .../tasks.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 5c3b09e5e..d9d08116a 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -229,3 +229,7 @@ ## END — 2026-04-25T11:09:49Z — test — AHCSITC3-test - HEAD: `19b972a20b747098ddd9df2bc503c60fadfdeb47` - Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/test/last_message.md` + +## START — 2026-04-25T11:09:49Z — integration — AHCSITC3-integ-core +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC3-integ-core" LAUNCH_CODEX=1` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index c40d848c8..d69b02481 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -910,7 +910,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-core\"" ], - "status": "pending", + "status": "in_progress", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-core" }, From c6466f0cc2eeed9f9c62f285ea82ad8d41313452 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:17:27 -0400 Subject: [PATCH 32/48] fix: close AHCSITC3 integration drift --- docs/CONFIGURATION.md | 27 +++++++++++++++++++++++++++ docs/TRACE.md | 18 ++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 9b173539c..74501b5c3 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -32,6 +32,33 @@ Environment variables and advanced configuration options for Substrate. | `SUBSTRATE_PTY_DEBUG` | Enable PTY debug logging | *none* | `1` | | `SUBSTRATE_PTY_PIPELINE_LAST` | PTY for last pipeline segment | *none* | `1` | +## Agent Hub Configuration + +Agent Hub successor routing is configured through the normal Substrate config and policy files. + +Config keys: +- `agents.hub.orchestrator_agent_id` selects the canonical host-scoped orchestrator agent for `substrate agent status` and `substrate agent doctor`. +- Agent inventory entries continue to define each agent's adapter kind and execution posture; the derived `backend_id` remains `:`. + +Policy keys: +- `agents.allowed_backends` remains the allowlist for derived agent adapter ids such as `cli:codex` or `api:openai`. +- Existing `agents.allowed_backends` entries stay valid across the successor `substrate agent ...` command surface because the policy token is still the derived `backend_id`, not `client`, `router`, `protocol`, `provider`, or `auth_authority`. + +Minimal example: + +```yaml +agents: + hub: + orchestrator_agent_id: claude_code +``` + +```yaml +agents: + allowed_backends: + - cli:claude_code + - cli:codex +``` + ## Manager Manifest & Init | Variable | Purpose | Default | Example | diff --git a/docs/TRACE.md b/docs/TRACE.md index 85fedc967..498411dc6 100644 --- a/docs/TRACE.md +++ b/docs/TRACE.md @@ -149,6 +149,24 @@ These are canonical cross-feature correlation identifiers. Details and required/ - `backend_id`: backend identifier in `:` form (e.g., `cli:codex`, `api:openai`) when a specific backend is involved. - `world_id`: world boundary identity; required on in-world telemetry families (e.g., `world_process_*`) and any record that describes an in-world boundary/session. +### Agent Identity-Tuple Fields + +Agent-hub successor telemetry keeps adapter identity separate from semantic identity: + +- `backend_id`: derived adapter identifier in `:` form. This remains the allowlist and adapter-selection token. +- `client`: pure-agent client identity for successor agent-hub records. +- `router`: routing surface for the record. Pure-agent records use `agent_hub`; nested gateway-backed records use `substrate_gateway`. +- `protocol`: protocol family for the record, such as `uaa.agent.session`. +- `provider`: nested gateway-backed provider identity only; pure-agent records omit it. +- `auth_authority`: nested gateway-backed auth authority only; pure-agent records omit it. +- `world_id`: world boundary identifier for world-scoped pure-agent records and in-world telemetry families. +- `world_generation`: generation counter for the active world-scoped pure-agent session or world-backed execution. + +Operator-facing omission rules: +- Pure-agent records keep `client`, `router`, and `protocol`, and omit `provider` plus `auth_authority`. +- Nested gateway-backed records may add `provider` and `auth_authority`, but they must omit `world_id` and `world_generation`. +- Host-scoped pure-agent records omit `world_id` and `world_generation`. + ### Router-Derived Event Families (workflow router daemon; Phase 8) The workflow-router daemon appends derived events to `trace.jsonl` (append-only). v1 uses explicit `event_type` values (see router DR-0016): From 48919c4062d22ca925cb560de071f9e595b282da Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:21:48 -0400 Subject: [PATCH 33/48] docs: finish AHCSITC3-integ-core --- .../session_log.md | 4 ++++ .../tasks.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index d9d08116a..154dd64c4 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -233,3 +233,7 @@ ## START — 2026-04-25T11:09:49Z — integration — AHCSITC3-integ-core - Dispatch: - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC3-integ-core" LAUNCH_CODEX=1` + +## END — 2026-04-25T11:21:48Z — integration — AHCSITC3-integ-core +- HEAD: `c6466f0cc2eeed9f9c62f285ea82ad8d41313452` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/integ-core/last_message.md` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index d69b02481..2ca5d7958 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -910,7 +910,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-core\"" ], - "status": "in_progress", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-core" }, From 725a62f8a6087c7e8eacefc81c17a5d93dcb302c Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:23:11 -0400 Subject: [PATCH 34/48] docs: start CP2-ci-checkpoint --- .../session_log.md | 4 ++++ .../tasks.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index 154dd64c4..eb5894fd0 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -237,3 +237,7 @@ ## END — 2026-04-25T11:21:48Z — integration — AHCSITC3-integ-core - HEAD: `c6466f0cc2eeed9f9c62f285ea82ad8d41313452` - Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/integ-core/last_message.md` + +## START — 2026-04-25T11:23:11Z — checkpoint — CP2-ci-checkpoint +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="CP2-ci-checkpoint"` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 2ca5d7958..c71ebf7c7 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -949,7 +949,7 @@ "Read pre-planning/ci_checkpoint_plan.md, tasks.json, session_log.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs." ], - "status": "pending", + "status": "in_progress", "type": "ops", "worktree": null }, From ca77a806170222df1d42721b96c32bb733909786 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:26:49 -0400 Subject: [PATCH 35/48] docs: finish CP2-ci-checkpoint --- .../tasks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index c71ebf7c7..55524962d 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -949,7 +949,7 @@ "Read pre-planning/ci_checkpoint_plan.md, tasks.json, session_log.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs." ], - "status": "in_progress", + "status": "completed", "type": "ops", "worktree": null }, From d49efc6bb930e33097da31c10d53315c5da8f4c5 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:27:39 -0400 Subject: [PATCH 36/48] docs: log CP2-ci-checkpoint evidence --- .../session_log.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index eb5894fd0..f1ea1238f 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -241,3 +241,17 @@ ## START — 2026-04-25T11:23:11Z — checkpoint — CP2-ci-checkpoint - Dispatch: - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="CP2-ci-checkpoint"` + +## END — 2026-04-25T11:27:39Z — checkpoint — CP2-ci-checkpoint +- Candidate SHA: `c6466f0cc2eeed9f9c62f285ea82ad8d41313452` +- Advisory CI audit: + - `scripts/ci-audit/ci_audit.sh --ledger-path "/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/ci-audit/ledger.jsonl" --kind ci-testing --orch-branch "feat/agent-hub-core-successor-identity-tuple-compatible" --head-sha "c6466f0cc2eeed9f9c62f285ea82ad8d41313452"` → `RECOMMEND=run` (`REASON=changes_since_last_green`) +- Compile parity: + - `make ci-compile-parity CI_WORKFLOW_REF="feat/agent-hub-core-successor-identity-tuple-compatible" CI_REMOTE=origin CI_CLEANUP=1 CI_CHECKOUT_REF="c6466f0cc2eeed9f9c62f285ea82ad8d41313452"` → `RUN_ID=24929777193` → `RUN_URL=https://github.com/atomize-hq/substrate/actions/runs/24929777193` → `CONCLUSION=success` + - Passed OSes: `macos-14,ubuntu-24.04,windows-2022` + - Failed OSes: `NONE` + - Failed jobs: `NONE` +- CI audit ledger: + - `scripts/ci-audit/ci_audit_record.sh --ledger-path "/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/ci-audit/ledger.jsonl" --kind ci-testing --mode compile-parity --orch-branch "feat/agent-hub-core-successor-identity-tuple-compatible" --run-id "24929777193" --tested-sha "c6466f0cc2eeed9f9c62f285ea82ad8d41313452"` → `RECORDED=1` +- Feature smoke: + - Not dispatched. `pre-planning/ci_checkpoint_plan.md` and `CP2-ci-checkpoint.md` keep `feature_smoke=false` for this checkpoint, including Windows. From 0fbd1a36f7b6ceec5ba07f19c1121dccbce7a7ea Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:28:05 -0400 Subject: [PATCH 37/48] docs: mark AHCSITC3 platform fixes noop --- .../tasks.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index 55524962d..a00d6ae35 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -997,7 +997,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-linux\"" ], - "status": "pending", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-linux" }, @@ -1045,7 +1045,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-macos\"" ], - "status": "pending", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-macos" }, @@ -1093,7 +1093,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-windows\"" ], - "status": "pending", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-windows" }, From fd3e72a88bc8ab9895aef7539527bf9c57542118 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 07:38:26 -0400 Subject: [PATCH 38/48] docs: finish AHCSITC3-integ --- .../session_log.md | 10 ++++++++++ .../tasks.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index f1ea1238f..0252e3125 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -255,3 +255,13 @@ - `scripts/ci-audit/ci_audit_record.sh --ledger-path "/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/ci-audit/ledger.jsonl" --kind ci-testing --mode compile-parity --orch-branch "feat/agent-hub-core-successor-identity-tuple-compatible" --run-id "24929777193" --tested-sha "c6466f0cc2eeed9f9c62f285ea82ad8d41313452"` → `RECORDED=1` - Feature smoke: - Not dispatched. `pre-planning/ci_checkpoint_plan.md` and `CP2-ci-checkpoint.md` keep `feature_smoke=false` for this checkpoint, including Windows. + +## START — 2026-04-25T11:28:10Z — integration-final — AHCSITC3-integ +- Dispatch: + - `make triad-task-start-integ-final FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC3" LAUNCH_CODEX=1` + +## END — 2026-04-25T11:37:10Z — integration-final — AHCSITC3-integ +- HEAD: `25c7b3448c7131c66d861273bacefbb86520ee69` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/integ/last_message.md` +- Merge result: + - `make triad-task-finish TASK_ID="AHCSITC3-integ"` → `MERGED_TO_ORCH=true` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index a00d6ae35..f05565a29 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -1156,7 +1156,7 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ\"" ], - "status": "pending", + "status": "completed", "type": "integration", "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ" }, From 6083b3f32cab329c956b4e13f771a637efde2fc6 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 08:02:09 -0400 Subject: [PATCH 39/48] patch: enhance world_gateway_disabled_state test with fixture setup --- crates/shell/tests/world_gateway.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/shell/tests/world_gateway.rs b/crates/shell/tests/world_gateway.rs index 5dd8c3dde..5ecdc74ef 100644 --- a/crates/shell/tests/world_gateway.rs +++ b/crates/shell/tests/world_gateway.rs @@ -1218,9 +1218,19 @@ fn world_gateway_status_human_output_omits_missing_optional_fields_without_place #[test] fn world_gateway_disabled_state_skips_typed_runtime_bootstrap() { - let (_temp, _socket, socket_path) = gateway_socket_fixture(); + let (_temp, socket, socket_path) = gateway_socket_fixture(); + let fixture = GatewayAuthFixture::new(); + fixture.write_global_config(gateway_config_with_codex_backend()); + fixture.write_global_agent_inventory("codex.yaml", gateway_inventory_for_codex()); + fixture.write_global_policy(gateway_policy_with_codex_host_credentials()); + fixture.write_codex_auth_state( + r#"{ + "account_id": "acct_disabled_state", + "access_token": "token-disabled-state" +}"#, + ); - let mut cmd = substrate_shell_driver(); + let mut cmd = fixture.command(); let assert = cmd .env("SUBSTRATE_WORLD_ENABLED", "0") .env("SUBSTRATE_WORLD", "disabled") @@ -1252,6 +1262,11 @@ fn world_gateway_disabled_state_skips_typed_runtime_bootstrap() { parsed.pointer("/placement_posture/execution"), Some(&json!("in_world")) ); + assert_eq!( + socket.connection_count(), + 0, + "disabled state must not bootstrap the typed runtime", + ); } #[test] From ff6d5eda16bdf9d7da12214be461d5a0c49a3a9f Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 08:58:26 -0400 Subject: [PATCH 40/48] fix: preserve nested agent status rows by run id --- crates/shell/src/execution/agents_cmd.rs | 15 +- .../agent_successor_contract_ahcsitc0.rs | 178 +++++++++++++++++- .../agent-hub-session-protocol-spec.md | 4 +- .../contract.md | 18 +- .../manual_testing_playbook.md | 25 ++- 5 files changed, 217 insertions(+), 23 deletions(-) diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 9915c055b..6cd99b164 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -325,6 +325,7 @@ struct NestedParentJson { #[derive(Clone, Serialize)] struct NestedLlmRecordJson { parent: NestedParentJson, + run_id: String, backend_id: String, client: String, router: String, @@ -351,7 +352,7 @@ struct SessionProjection { #[derive(Clone)] struct NestedProjection { record: NestedLlmRecordJson, - sort_key: (String, String, String, String, String, String), + sort_key: (String, String, String), } fn build_status_report<'a>( @@ -379,8 +380,7 @@ fn build_status_report<'a>( let events = read_trace_agent_events()?; let mut sessions = BTreeMap::<(String, String), SessionProjection>::new(); - let mut nested = - BTreeMap::<(String, String, String, String, String, String), NestedProjection>::new(); + let mut nested = BTreeMap::<(String, String, String), NestedProjection>::new(); for event in events { let Some(entry) = context.inventory.get(&event.agent_id) else { @@ -522,9 +522,10 @@ fn render_status_report(report: &StatusReportJson<'_>, json_mode: bool) -> Resul println!("nested_llm_records"); for record in &report.nested_llm_records { println!( - " parent.orchestration_session_id={} | parent.agent_id={} | backend_id={} | client={} | router={} | provider={} | auth_authority={} | protocol={}", + " parent.orchestration_session_id={} | parent.agent_id={} | run_id={} | backend_id={} | client={} | router={} | provider={} | auth_authority={} | protocol={}", record.parent.orchestration_session_id, record.parent.agent_id, + record.run_id, record.backend_id, record.client, record.router, @@ -608,10 +609,7 @@ fn nested_projection( let sort_key = ( event.orchestration_session_id.clone(), event.agent_id.clone(), - entry.derived_backend_id(), - provider.clone(), - auth_authority.clone(), - tuple.protocol.clone(), + event.run_id.clone(), ); Some(NestedProjection { @@ -620,6 +618,7 @@ fn nested_projection( orchestration_session_id: event.orchestration_session_id.clone(), agent_id: event.agent_id.clone(), }, + run_id: event.run_id.clone(), backend_id: entry.derived_backend_id(), client: event.agent_id.clone(), router: NESTED_ROUTER.to_string(), diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index bcbbb026d..a67d92fa2 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -526,7 +526,7 @@ fn ahcsitc3_specs_lock_supersession_parity_and_validation_boundaries() { "### Case 1 — `substrate agent list --json` keeps adapter identity and omission rules", "### Case 2 — `substrate agent status --json` proves a host-scoped orchestrator", "### Case 3 — world-scoped members publish `world_id` and `world_generation`", - "### Case 4 — nested gateway-backed records publish `provider` and `auth_authority` on the nested record only", + "### Case 4 — nested gateway-backed records publish `run_id`, `provider`, and `auth_authority` on the nested record only", "### Case 5 — canonical trace keeps the same pure-agent versus nested-record split", "### Case 6 — `substrate agent doctor --json` proves healthy ordered checks", "### Case 7 — `substrate agent doctor` fails closed for invalid orchestrator state", @@ -896,6 +896,10 @@ metadata: {} record.pointer("/client").and_then(Value::as_str), Some("claude_code") ); + assert_eq!( + record.pointer("/run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14") + ); assert_eq!( record.pointer("/router").and_then(Value::as_str), Some("substrate_gateway") @@ -918,6 +922,178 @@ metadata: {} ); } +#[test] +fn agent_status_preserves_same_tuple_nested_rows_and_sorts_them_by_run_id() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "world", true, true, true), + ); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:02Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "second nested gateway request completed" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "first nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "agent status should preserve multiple same-tuple nested rows in JSON mode: {output:?}" + ); + + let json = parse_json_output(&output); + let nested = json["nested_llm_records"] + .as_array() + .expect("nested_llm_records should be an array"); + assert_eq!( + nested.len(), + 2, + "same-tuple nested gateway records with different run_id values must stay distinct: {json}" + ); + assert_eq!( + nested[0].pointer("/run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14") + ); + assert_eq!( + nested[1].pointer("/run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15") + ); + for record in nested { + assert_eq!( + record.pointer("/router").and_then(Value::as_str), + Some("substrate_gateway") + ); + assert_eq!( + record.pointer("/provider").and_then(Value::as_str), + Some("openai") + ); + assert_eq!( + record.pointer("/auth_authority").and_then(Value::as_str), + Some("codex_subscription") + ); + assert_eq!( + record.pointer("/protocol").and_then(Value::as_str), + Some("openai.responses") + ); + assert!( + record.get("world_id").is_none() && record.get("world_generation").is_none(), + "nested gateway-backed records must omit world scope fields: {record}" + ); + } + + let text_output = fixture.run(&["agent", "status"]); + assert!( + text_output.status.success(), + "agent status should preserve multiple same-tuple nested rows in text mode: {text_output:?}" + ); + let stdout = String::from_utf8_lossy(&text_output.stdout); + assert!( + stdout.contains("nested_llm_records"), + "text mode should render a nested_llm_records section when nested records exist\nstdout: {stdout}" + ); + let first_idx = stdout + .find("run_id=0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14") + .expect("text mode should include the lexically first nested run_id"); + let second_idx = stdout + .find("run_id=0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15") + .expect("text mode should include the lexically second nested run_id"); + assert!( + first_idx < second_idx, + "text mode should sort nested rows by run_id rather than insertion order\nstdout: {stdout}" + ); +} + #[test] fn nested_gateway_policy_does_not_inherit_agent_hub_success_by_implication() { let fixture = AgentSuccessorFixture::new(); diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md index 6d2e740cd..9f549ecb7 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md @@ -303,6 +303,7 @@ This is the exact object shape for each nested record in `substrate agent status "orchestration_session_id": "sess_001", "agent_id": "codex" }, + "run_id": "run_nested_001", "backend_id": "cli:codex", "client": "codex", "router": "substrate_gateway", @@ -314,6 +315,7 @@ This is the exact object shape for each nested record in `substrate agent status Field rules: - `parent.orchestration_session_id` and `parent.agent_id` identify the pure-agent session that triggered the nested request. +- `run_id` is the nested request correlation id and is required on every nested row. - `backend_id` remains the parent agent backend id. - `client` remains the parent agent id. - `router` is exactly `substrate_gateway`. @@ -341,7 +343,7 @@ Field rules: - `sessions[*]` uses `AgentSessionStatusV1` exactly. - `nested_llm_records[*]` uses `NestedLlmStatusRecordV1` exactly. - `sessions` sort by `orchestration_session_id`, then `agent_id`, ascending byte order. -- `nested_llm_records` sort by `parent.orchestration_session_id`, then `parent.agent_id`, then nested request correlation id ascending byte order. +- `nested_llm_records` sort by `parent.orchestration_session_id`, then `parent.agent_id`, then `run_id` ascending byte order. ## Structured event exchange diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md index 560d6871f..510644ba7 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md @@ -84,7 +84,7 @@ Authoritative inputs: - Stable ordering rules: - inventory rows sort by `agent_id` ascending byte order - session rows sort by `orchestration_session_id`, then `agent_id`, ascending byte order - - nested LLM rows sort by parent `(orchestration_session_id, agent_id)`, then by nested request correlation id once `telemetry-spec.md` names that field + - nested LLM rows sort by parent `(orchestration_session_id, agent_id)`, then by nested `run_id` ascending byte order ### `substrate agent list` @@ -196,14 +196,16 @@ Nested LLM render rules: - Each nested row renders: 1. `parent.orchestration_session_id` 2. `parent.agent_id` - 3. `backend_id` - 4. `client` - 5. `router` - 6. `provider` - 7. `auth_authority` - 8. `protocol` + 3. `run_id` + 4. `backend_id` + 5. `client` + 6. `router` + 7. `provider` + 8. `auth_authority` + 9. `protocol` - `router` renders `substrate_gateway`. - `client` renders the parent agent session's `agent_id`. +- `run_id` renders the nested request correlation id. - `provider` and `auth_authority` are required on every nested row. - Nested rows omit `world_id` and `world_generation`. @@ -238,6 +240,7 @@ Nested LLM render rules: "orchestration_session_id": "sess_001", "agent_id": "codex" }, + "run_id": "run_nested_001", "backend_id": "cli:codex", "client": "codex", "router": "substrate_gateway", @@ -255,6 +258,7 @@ JSON rules: - Each pure-agent session object omits `provider` and `auth_authority`. - `world_id` and `world_generation` are both present or both absent. - `world_generation` is an integer that starts at `0` for a fresh world allocation and increments by `1` on each hub-driven restart of that orchestration session's world. +- Each nested record includes `run_id` as the nested request correlation id. - Each nested record includes `provider` and `auth_authority`. - Each nested record omits `world_id` and `world_generation`. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md index d1e4850bc..060b5c777 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md @@ -49,7 +49,7 @@ Fixture requirements: - the effective inventory contains one host-scoped orchestrator agent selected by `agents.hub.orchestrator_agent_id` - the effective inventory contains at least one host-scoped member and one world-scoped member - the effective policy allowlists the derived orchestrator and member `backend_id` values under `agents.allowed_backends` -- the fixture can trigger one nested gateway-backed request so `nested_llm_records` and correlated trace output are non-empty +- the fixture can trigger at least two nested gateway-backed requests so `nested_llm_records` and correlated trace output are non-empty Evidence capture requirements: @@ -225,9 +225,9 @@ Expected results: - every world-scoped member row includes both `world_id` and `world_generation` - every host-scoped row omits both fields -### Case 4 — nested gateway-backed records publish `provider` and `auth_authority` on the nested record only +### Case 4 — nested gateway-backed records publish `run_id`, `provider`, and `auth_authority` on the nested record only -Trigger one nested gateway-backed request with the feature fixture, then rerun status: +Trigger two nested gateway-backed requests with the feature fixture, then rerun status: ```bash substrate agent status --json | tee "$artifacts_dir/agent-status-nested.json" @@ -238,8 +238,10 @@ Assertions: ```bash jq -e ' (.nested_llm_records | type == "array") and - (length > 0) and + (length > 1) and + ([.nested_llm_records[].run_id] == ([.nested_llm_records[].run_id] | sort)) and all(.nested_llm_records[]; + (.run_id | type == "string") and .router == "substrate_gateway" and (.provider | type == "string") and (.auth_authority | type == "string") and @@ -258,12 +260,23 @@ jq -e ' ' "$artifacts_dir/agent-status-nested.json" ``` +```bash +substrate agent status | tee "$artifacts_dir/agent-status-nested.txt" +``` + +```bash +rg -n "run_id=" "$artifacts_dir/agent-status-nested.txt" +``` + Expected results: - command exits `0` -- every nested record includes `provider` and `auth_authority` +- every nested record includes `run_id`, `provider`, and `auth_authority` +- nested rows stay distinct even when `router`, `provider`, `auth_authority`, and `protocol` match +- nested rows sort by `run_id` ascending byte order - every pure-agent session row omits `provider` and `auth_authority` - no nested record includes `world_id` or `world_generation` +- text output renders distinct `run_id=` values for nested rows ### Case 5 — canonical trace keeps the same pure-agent versus nested-record split @@ -399,7 +412,7 @@ Pass condition: Check: - `agent-hub-session-protocol-spec.md` owns capability descriptors, session handles, lifecycle states, and the machine-readable status objects -- this playbook uses the same `sessions` and `nested_llm_records` object names without widening them +- this playbook uses the same `sessions` and `nested_llm_records` object names and matches the protocol spec's nested `run_id` field - world reuse and restart wording in this playbook matches the protocol spec Pass condition: From 9aacc384ec61228f481541ff53e9eef15caadc3a Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 10:04:58 -0400 Subject: [PATCH 41/48] fix: preserve member roles in agent status --- crates/shell/src/execution/agents_cmd.rs | 2 + .../agent_successor_contract_ahcsitc0.rs | 245 ++++++++++++++++++ 2 files changed, 247 insertions(+) diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 6cd99b164..440a7825f 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -24,6 +24,7 @@ use substrate_common::{AgentEvent, PlacementExecution}; const PURE_AGENT_PROTOCOL: &str = "uaa.agent.session"; const PURE_AGENT_ROUTER: &str = "agent_hub"; const NESTED_ROUTER: &str = "substrate_gateway"; +const MEMBER_ROLE: &str = "member"; const ORCHESTRATOR_ROLE: &str = "orchestrator"; pub(crate) fn handle_agent_command(cmd: &AgentCmd, cli: &Cli) -> i32 { @@ -660,6 +661,7 @@ fn role_for_event<'a>( effective_config: &'a SubstrateConfig, ) -> Option<&'a str> { match event.role.as_deref() { + Some(MEMBER_ROLE) => Some(MEMBER_ROLE), Some(ORCHESTRATOR_ROLE) => Some(ORCHESTRATOR_ROLE), _ => role_for_entry(agent_id, effective_config), } diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index a67d92fa2..48e58a9af 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -169,6 +169,13 @@ fn parse_json_output(output: &Output) -> Value { serde_json::from_slice(&output.stdout).expect("stdout should be valid JSON") } +fn find_session_by_agent<'a>(sessions: &'a [Value], agent_id: &str) -> &'a Value { + sessions + .iter() + .find(|session| session.pointer("/agent_id").and_then(Value::as_str) == Some(agent_id)) + .unwrap_or_else(|| panic!("expected session row for agent `{agent_id}`")) +} + #[test] fn plural_agents_namespace_keeps_validate_as_the_only_compatibility_leaf() { let fixture = AgentSuccessorFixture::new(); @@ -355,6 +362,244 @@ fn agent_status_json_uses_locked_top_level_field_names() { ); } +#[test] +fn agent_status_preserves_member_roles_and_filters_them_by_contract_label() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "data": { "message": "orchestrator session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "member session is live" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "unfiltered agent status should succeed for member-role fixtures: {output:?}" + ); + + let json = parse_json_output(&output); + assert!( + json.pointer("/role_filter") + .is_some_and(serde_json::Value::is_null), + "unfiltered status must publish a null role_filter: {json}" + ); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + sessions.len(), + 2, + "unfiltered status should keep both orchestrator and member sessions: {json}" + ); + + let orchestrator = find_session_by_agent(sessions, "claude_code"); + assert_eq!( + orchestrator.pointer("/role").and_then(Value::as_str), + Some("orchestrator"), + "orchestrator session must preserve the orchestrator role label: {json}" + ); + + let member = find_session_by_agent(sessions, "codex"); + assert_eq!( + member.pointer("/role").and_then(Value::as_str), + Some("member"), + "member session must preserve the member role label: {json}" + ); + assert_eq!( + member.pointer("/world_id").and_then(Value::as_str), + Some("wld_active_0002"), + "world-scoped member status rows must keep world_id: {json}" + ); + assert_eq!( + member.pointer("/world_generation").and_then(Value::as_u64), + Some(7), + "world-scoped member status rows must keep world_generation: {json}" + ); + + let member_output = fixture.run(&["agent", "status", "--role", "member", "--json"]); + assert!( + member_output.status.success(), + "member-filtered agent status should succeed: {member_output:?}" + ); + let member_json = parse_json_output(&member_output); + assert_eq!( + member_json.pointer("/role_filter").and_then(Value::as_str), + Some("member"), + "member-filtered status must report role_filter=member: {member_json}" + ); + let member_sessions = member_json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + member_sessions.len(), + 1, + "--role member should return exactly one member session: {member_json}" + ); + let member_only = find_session_by_agent(member_sessions, "codex"); + assert_eq!( + member_only.pointer("/role").and_then(Value::as_str), + Some("member") + ); + assert_eq!( + member_only.pointer("/world_id").and_then(Value::as_str), + Some("wld_active_0002") + ); + assert_eq!( + member_only + .pointer("/world_generation") + .and_then(Value::as_u64), + Some(7) + ); + + let orchestrator_output = fixture.run(&["agent", "status", "--role", "orchestrator", "--json"]); + assert!( + orchestrator_output.status.success(), + "orchestrator-filtered agent status should succeed: {orchestrator_output:?}" + ); + let orchestrator_json = parse_json_output(&orchestrator_output); + assert_eq!( + orchestrator_json + .pointer("/role_filter") + .and_then(Value::as_str), + Some("orchestrator"), + "orchestrator-filtered status must report role_filter=orchestrator: {orchestrator_json}" + ); + let orchestrator_sessions = orchestrator_json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + orchestrator_sessions.len(), + 1, + "--role orchestrator should return exactly one orchestrator session: {orchestrator_json}" + ); + let orchestrator_only = find_session_by_agent(orchestrator_sessions, "claude_code"); + assert_eq!( + orchestrator_only.pointer("/role").and_then(Value::as_str), + Some("orchestrator") + ); +} + +#[test] +fn agent_status_unsupported_event_roles_fall_back_to_contract_roles() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "unexpected", + "data": { "message": "unsupported role should not leak" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "unexpected", + "data": { "message": "unsupported role should collapse to null" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "status should succeed when unsupported event roles are present: {output:?}" + ); + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + + let orchestrator = find_session_by_agent(sessions, "claude_code"); + assert_eq!( + orchestrator.pointer("/role").and_then(Value::as_str), + Some("orchestrator"), + "unsupported explicit roles for the configured orchestrator must fall back to orchestrator: {json}" + ); + + let member = find_session_by_agent(sessions, "codex"); + assert!( + member + .pointer("/role") + .is_some_and(serde_json::Value::is_null), + "unsupported explicit roles for non-orchestrators must fall back to null: {json}" + ); + + let unexpected_output = fixture.run(&["agent", "status", "--role", "unexpected", "--json"]); + assert!( + unexpected_output.status.success(), + "status should not reject unknown role filters even when no rows match: {unexpected_output:?}" + ); + let unexpected_json = parse_json_output(&unexpected_output); + assert_eq!( + unexpected_json + .pointer("/role_filter") + .and_then(Value::as_str), + Some("unexpected"), + "status should echo the requested unexpected role filter: {unexpected_json}" + ); + assert_eq!( + unexpected_json["sessions"] + .as_array() + .expect("sessions should be an array") + .len(), + 0, + "unsupported event roles must not become filterable session roles: {unexpected_json}" + ); +} + #[test] fn agent_doctor_json_locks_field_names_omissions_and_check_order() { let fixture = AgentSuccessorFixture::new(); From 40f65fa32dd4fa2d8576d1e60d9ae1dd6b1a8767 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 10:57:32 -0400 Subject: [PATCH 42/48] fix: harden agent status session projection --- crates/shell/src/execution/agents_cmd.rs | 38 ++- .../agent_successor_contract_ahcsitc0.rs | 278 ++++++++++++++++++ 2 files changed, 312 insertions(+), 4 deletions(-) diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 440a7825f..19d2c0e36 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -10,6 +10,7 @@ use crate::execution::config_model::{ self, AgentExecutionScope, CliConfigOverrides, SubstrateConfig, }; use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; use serde::Serialize; use std::collections::{BTreeMap, BTreeSet}; use std::env; @@ -347,6 +348,7 @@ struct StatusReportJson<'a> { #[derive(Clone)] struct SessionProjection { + last_event_ts: DateTime, session: StatusSessionJson, } @@ -387,8 +389,20 @@ fn build_status_report<'a>( let Some(entry) = context.inventory.get(&event.agent_id) else { continue; }; - let role = role_for_event(&event, &entry.file.id, &context.effective_config); - let scope = scope_for_event(&event, entry, &context.effective_config); + let is_selected_orchestrator = + context.effective_config.agents.hub.orchestrator_agent_id == entry.file.id; + let role = role_for_event( + &event, + &entry.file.id, + is_selected_orchestrator, + &context.effective_config, + ); + let scope = scope_for_event( + &event, + entry, + is_selected_orchestrator, + &context.effective_config, + ); if let Some(session_key) = pure_session_key(&event) { let mut world_id = None; @@ -408,6 +422,7 @@ fn build_status_report<'a>( } let projection = SessionProjection { + last_event_ts: event.ts, session: StatusSessionJson { orchestration_session_id: session_key.0.clone(), agent_id: entry.file.id.clone(), @@ -425,7 +440,13 @@ fn build_status_report<'a>( }, }; - sessions.insert(session_key, projection); + let should_replace = match sessions.get(&session_key) { + Some(existing) => projection.last_event_ts >= existing.last_event_ts, + None => true, + }; + if should_replace { + sessions.insert(session_key, projection); + } } if let Some(projection) = nested_projection(&event, entry) { @@ -634,8 +655,13 @@ fn nested_projection( fn scope_for_event( event: &AgentEvent, entry: &AgentInventoryEntryV1, + is_selected_orchestrator: bool, effective_config: &SubstrateConfig, ) -> AgentExecutionScope { + if is_selected_orchestrator { + return entry.effective_scope(effective_config); + } + match event .placement_posture .as_ref() @@ -658,11 +684,15 @@ fn role_for_entry<'a>(agent_id: &str, effective_config: &'a SubstrateConfig) -> fn role_for_event<'a>( event: &'a AgentEvent, agent_id: &str, + is_selected_orchestrator: bool, effective_config: &'a SubstrateConfig, ) -> Option<&'a str> { + if is_selected_orchestrator { + return role_for_entry(agent_id, effective_config); + } + match event.role.as_deref() { Some(MEMBER_ROLE) => Some(MEMBER_ROLE), - Some(ORCHESTRATOR_ROLE) => Some(ORCHESTRATOR_ROLE), _ => role_for_entry(agent_id, effective_config), } } diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index 48e58a9af..61dcd3e56 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -512,6 +512,284 @@ fn agent_status_preserves_member_roles_and_filters_them_by_contract_label() { ); } +#[test] +fn agent_status_prefers_newest_pure_session_event_when_trace_lines_are_out_of_order() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:02Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "data": { "message": "newest member session event intentionally omits world fields" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "world_id": "wld_stale_0001", + "world_generation": 6, + "data": { "message": "older stale member session event arrives later in file order" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "status should succeed when pure-agent events arrive out of timestamp order: {output:?}" + ); + + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + let codex_sessions: Vec<&Value> = sessions + .iter() + .filter(|session| session.pointer("/agent_id").and_then(Value::as_str) == Some("codex")) + .collect(); + assert_eq!( + codex_sessions.len(), + 1, + "out-of-order pure-agent events must still collapse to one codex session row: {json}" + ); + + let codex = codex_sessions[0]; + assert_eq!( + codex.pointer("/last_event_at").and_then(Value::as_str), + Some("2026-04-05T00:00:02+00:00"), + "the collapsed codex row must use the newest event timestamp rather than later file order: {json}" + ); + assert!( + codex.get("world_id").is_none(), + "the winning newest projection must not inherit stale world_id from the older event: {json}" + ); + assert!( + codex.get("world_generation").is_none(), + "the winning newest projection must not inherit stale world_generation from the older event: {json}" + ); +} + +#[test] +fn agent_status_ignores_non_selected_trace_orchestrator_roles() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "data": { "message": "selected orchestrator session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "stale member row claims orchestrator" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "status should succeed when a non-selected agent claims orchestrator: {output:?}" + ); + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + + let orchestrator = find_session_by_agent(sessions, "claude_code"); + assert_eq!( + orchestrator.pointer("/role").and_then(Value::as_str), + Some("orchestrator"), + "selected orchestrator must remain the only orchestrator session: {json}" + ); + + let codex = find_session_by_agent(sessions, "codex"); + assert!( + codex + .pointer("/role") + .is_some_and(serde_json::Value::is_null), + "non-selected stale orchestrator rows must collapse to null role: {json}" + ); + + let orchestrator_output = fixture.run(&["agent", "status", "--role", "orchestrator", "--json"]); + assert!( + orchestrator_output.status.success(), + "orchestrator-filtered status should succeed: {orchestrator_output:?}" + ); + let orchestrator_json = parse_json_output(&orchestrator_output); + let orchestrator_sessions = orchestrator_json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + orchestrator_sessions.len(), + 1, + "only the configured orchestrator should match --role orchestrator: {orchestrator_json}" + ); + assert_eq!( + find_session_by_agent(orchestrator_sessions, "claude_code") + .pointer("/role") + .and_then(Value::as_str), + Some("orchestrator") + ); + + let member_output = fixture.run(&["agent", "status", "--role", "member", "--json"]); + assert!( + member_output.status.success(), + "member-filtered status should succeed: {member_output:?}" + ); + let member_json = parse_json_output(&member_output); + assert_eq!( + member_json.pointer("/role_filter").and_then(Value::as_str), + Some("member"), + "member-filtered status must echo the requested role filter: {member_json}" + ); + assert_eq!( + member_json["sessions"] + .as_array() + .expect("sessions should be an array") + .len(), + 0, + "stale non-selected orchestrator roles must not become filterable member rows: {member_json}" + ); +} + +#[test] +fn agent_status_keeps_selected_orchestrator_host_scoped_when_trace_posture_says_in_world() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + fixture.write_global_policy_patch( + r#"agents: + allowed_backends: + - cli:claude_code +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + fixture.write_trace_events(&[json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "placement_posture": { + "execution": "in_world" + }, + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "trace posture should not move the selected orchestrator into world scope" } + })]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "status should succeed when the selected orchestrator trace posture says in_world: {output:?}" + ); + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + sessions.len(), + 1, + "fixture should project exactly one selected orchestrator session: {json}" + ); + + let orchestrator = &sessions[0]; + assert_eq!( + orchestrator + .pointer("/execution/scope") + .and_then(Value::as_str), + Some("host"), + "selected orchestrator status rows must stay host-scoped despite trace posture: {json}" + ); + assert!( + orchestrator.get("world_id").is_none(), + "host-scoped selected orchestrator rows must omit world_id: {json}" + ); + assert!( + orchestrator.get("world_generation").is_none(), + "host-scoped selected orchestrator rows must omit world_generation: {json}" + ); + + let world_output = fixture.run(&["agent", "status", "--scope", "world", "--json"]); + assert!( + world_output.status.success(), + "world-filtered status should succeed: {world_output:?}" + ); + let world_json = parse_json_output(&world_output); + assert_eq!( + world_json["sessions"] + .as_array() + .expect("sessions should be an array") + .len(), + 0, + "world-filtered status must exclude the selected orchestrator row: {world_json}" + ); +} + #[test] fn agent_status_unsupported_event_roles_fall_back_to_contract_roles() { let fixture = AgentSuccessorFixture::new(); From f2c62c7399c57b7386afd19cf9ef6c91939eb28a Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 12:06:34 -0400 Subject: [PATCH 43/48] Fix macOS gateway TCP fallback --- crates/shell/src/builtins/world_gateway.rs | 71 +++++++++++++--------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/crates/shell/src/builtins/world_gateway.rs b/crates/shell/src/builtins/world_gateway.rs index 0b2198818..df120a859 100644 --- a/crates/shell/src/builtins/world_gateway.rs +++ b/crates/shell/src/builtins/world_gateway.rs @@ -202,15 +202,7 @@ fn build_macos_gateway_client() -> anyhow::Result { }); } - let default_sock = macos_default_world_socket_path(); - if default_sock.exists() && probe_gateway_caps_uds(&default_sock) { - return Ok(MacosGatewayClient { - client: AgentClient::unix_socket(default_sock)?, - _forwarding: None, - }); - } - - if substrate_home_is_explicitly_set() { + if let Some(default_sock) = resolve_macos_host_gateway_socket() { return Ok(MacosGatewayClient { client: AgentClient::unix_socket(default_sock)?, _forwarding: None, @@ -247,17 +239,22 @@ fn resolve_macos_gateway_client_endpoint() -> MacosGatewayClientEndpoint { return MacosGatewayClientEndpoint::Unix(std::path::PathBuf::from(socket_path)); } - let default_sock = macos_default_world_socket_path(); - - if substrate_home_is_explicitly_set() - || (default_sock.exists() && probe_gateway_caps_uds(&default_sock)) - { - MacosGatewayClientEndpoint::Unix(default_sock) - } else { - MacosGatewayClientEndpoint::Tcp { + match resolve_macos_host_gateway_socket() { + Some(default_sock) => MacosGatewayClientEndpoint::Unix(default_sock), + None => MacosGatewayClientEndpoint::Tcp { host: "127.0.0.1".to_string(), port: 17788, - } + }, + } +} + +#[cfg(target_os = "macos")] +fn resolve_macos_host_gateway_socket() -> Option { + let default_sock = macos_default_world_socket_path(); + if default_sock.exists() && probe_gateway_caps_uds(&default_sock) { + Some(default_sock) + } else { + None } } @@ -282,11 +279,6 @@ fn probe_gateway_caps_uds(path: &std::path::Path) -> bool { } } -#[cfg_attr(not(target_os = "macos"), allow(dead_code))] -fn substrate_home_is_explicitly_set() -> bool { - std::env::var_os("SUBSTRATE_HOME").is_some_and(|value| !value.is_empty()) -} - #[cfg_attr(not(target_os = "macos"), allow(dead_code))] fn macos_default_world_socket_path() -> PathBuf { substrate_common::paths::substrate_home() @@ -1025,8 +1017,10 @@ where #[cfg(all(test, target_os = "macos"))] mod tests { use super::*; + use crate::execution::world_env_guard; fn with_env_var(key: &str, value: Option<&std::ffi::OsStr>, f: impl FnOnce() -> T) -> T { + let _guard = world_env_guard(); let prev = std::env::var_os(key); match value { Some(value) => std::env::set_var(key, value), @@ -1090,6 +1084,29 @@ mod tests { }); } + #[test] + fn macos_gateway_client_endpoint_falls_back_to_tcp_when_explicit_substrate_home_socket_missing() + { + let temp = tempfile::tempdir().expect("tempdir"); + let substrate_home = temp.path().join("isolated-substrate-home"); + + with_env_var("SUBSTRATE_HOME", Some(substrate_home.as_os_str()), || { + with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { + match resolve_macos_gateway_client_endpoint() { + MacosGatewayClientEndpoint::Tcp { host, port } => { + assert_eq!(host, "127.0.0.1"); + assert_eq!(port, 17788); + } + MacosGatewayClientEndpoint::Unix(path) => { + panic!( + "expected tcp fallback when explicit substrate home socket is missing, got {path:?}" + ) + } + } + }) + }); + } + #[test] fn macos_gateway_client_endpoint_falls_back_to_tcp_when_host_socket_is_stale() { let temp = tempfile::tempdir().expect("tempdir"); @@ -1116,12 +1133,11 @@ mod tests { #[cfg(test)] mod classification_tests { - use super::{ - error_is_component_unavailable, macos_default_world_socket_path, - substrate_home_is_explicitly_set, - }; + use super::{error_is_component_unavailable, macos_default_world_socket_path}; + use crate::execution::world_env_guard; fn with_env_var(key: &str, value: Option<&std::ffi::OsStr>, f: impl FnOnce() -> T) -> T { + let _guard = world_env_guard(); let prev = std::env::var_os(key); match value { Some(value) => std::env::set_var(key, value), @@ -1156,7 +1172,6 @@ mod classification_tests { let substrate_home = temp.path().join("isolated-substrate-home"); with_env_var("SUBSTRATE_HOME", Some(substrate_home.as_os_str()), || { - assert!(substrate_home_is_explicitly_set()); assert_eq!( macos_default_world_socket_path(), substrate_home.join("sock/agent.sock") From 6c104a06fceeb50194a98f2510b0b8078ad57753 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 16:12:49 -0400 Subject: [PATCH 44/48] Validate filtered world identity in agent status --- crates/shell/src/execution/agent_inventory.rs | 40 +- crates/shell/src/execution/agents_cmd.rs | 121 ++++-- .../agent_successor_contract_ahcsitc0.rs | 402 ++++++++++++++++-- crates/shell/tests/agents_validate.rs | 108 +++++ 4 files changed, 608 insertions(+), 63 deletions(-) diff --git a/crates/shell/src/execution/agent_inventory.rs b/crates/shell/src/execution/agent_inventory.rs index 8c42539ce..e44e9dbd7 100644 --- a/crates/shell/src/execution/agent_inventory.rs +++ b/crates/shell/src/execution/agent_inventory.rs @@ -6,7 +6,7 @@ use std::collections::BTreeMap; use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; -use substrate_broker::{validate_backend_id, Policy, WorldFsDenyEnforcement}; +use substrate_broker::{validate_backend_id, validate_dotted_id, Policy, WorldFsDenyEnforcement}; use substrate_common::derive_agent_backend_id; use substrate_common::paths as substrate_paths; @@ -27,6 +27,8 @@ pub(crate) struct AgentConfigV1 { pub enabled: bool, pub kind: AgentConfigKind, #[serde(default)] + pub protocol: Option, + #[serde(default)] pub execution: AgentExecutionConfigV1, #[serde(default)] pub cli: Option, @@ -84,6 +86,12 @@ pub(crate) struct AgentApiAuthConfigV1 { #[derive(Debug, Clone, Default, Deserialize)] #[serde(default, deny_unknown_fields)] pub(crate) struct AgentCapabilitiesV1 { + pub session_start: bool, + pub session_resume: bool, + pub session_fork: bool, + pub session_stop: bool, + pub status_snapshot: bool, + pub event_stream: bool, pub llm: bool, pub mcp_client: bool, } @@ -350,9 +358,39 @@ fn validate_agent_config(path: &Path, config: &AgentConfigV1) -> Result<()> { } } + if let Some(protocol) = &config.protocol { + let trimmed = protocol.trim(); + if trimmed.is_empty() { + return Err(config_model::user_error(format!( + "invalid agent file in {}: config.protocol must not be empty", + path.display() + ))); + } + if trimmed != protocol { + return Err(config_model::user_error(format!( + "invalid agent file in {}: config.protocol must not include leading or trailing whitespace", + path.display() + ))); + } + validate_dotted_id(protocol).map_err(|_| { + config_model::user_error(format!( + "invalid agent file in {}: config.protocol '{}' must be a lowercase dotted id", + path.display(), + protocol + )) + })?; + } + + let _ = &config.protocol; let _ = config.execution.scope; let _ = config.cli.as_ref().and_then(|cli| cli.mode); let _ = config.enabled; + let _ = config.capabilities.session_start; + let _ = config.capabilities.session_resume; + let _ = config.capabilities.session_fork; + let _ = config.capabilities.session_stop; + let _ = config.capabilities.status_snapshot; + let _ = config.capabilities.event_stream; let _ = config.capabilities.llm; let _ = config.capabilities.mcp_client; diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 19d2c0e36..f6621ebad 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -1,5 +1,5 @@ use crate::execution::agent_inventory::{ - discover_agent_files, load_effective_agent_inventory, validate_agent_file, + discover_agent_files, load_effective_agent_inventory, validate_agent_file, AgentCapabilitiesV1, AgentInventoryEntryV1, }; use crate::execution::cli::{ @@ -350,6 +350,18 @@ struct StatusReportJson<'a> { struct SessionProjection { last_event_ts: DateTime, session: StatusSessionJson, + source: SessionProjectionSource, +} + +#[derive(Clone)] +struct SessionProjectionSource { + orchestration_session_id: String, + agent_id: String, + run_id: String, + ts: DateTime, + is_world_scoped: bool, + has_top_level_world_id: bool, + has_top_level_world_generation: bool, } #[derive(Clone)] @@ -405,26 +417,14 @@ fn build_status_report<'a>( ); if let Some(session_key) = pure_session_key(&event) { - let mut world_id = None; - let mut world_generation = None; - if scope == AgentExecutionScope::World { - let maybe_world_id = event.world_id.clone(); - let maybe_world_generation = event.world_generation.or_else(|| { - event - .data - .get("world_generation") - .and_then(serde_json::Value::as_u64) - }); - if maybe_world_id.is_some() && maybe_world_generation.is_some() { - world_id = maybe_world_id; - world_generation = maybe_world_generation; - } - } + let orchestration_session_id = session_key.0.clone(); + let world_id = event.world_id.clone(); + let world_generation = event.world_generation; let projection = SessionProjection { last_event_ts: event.ts, session: StatusSessionJson { - orchestration_session_id: session_key.0.clone(), + orchestration_session_id: orchestration_session_id.clone(), agent_id: entry.file.id.clone(), backend_id: entry.derived_backend_id(), client: entry.file.id.clone(), @@ -435,8 +435,25 @@ fn build_status_report<'a>( }, role: role.map(ToOwned::to_owned), last_event_at: event.ts.to_rfc3339(), - world_id, - world_generation, + world_id: if scope == AgentExecutionScope::World { + world_id.clone() + } else { + None + }, + world_generation: if scope == AgentExecutionScope::World { + world_generation + } else { + None + }, + }, + source: SessionProjectionSource { + orchestration_session_id, + agent_id: entry.file.id.clone(), + run_id: event.run_id.clone(), + ts: event.ts, + is_world_scoped: scope == AgentExecutionScope::World, + has_top_level_world_id: world_id.is_some(), + has_top_level_world_generation: world_generation.is_some(), }, }; @@ -454,16 +471,31 @@ fn build_status_report<'a>( } } - let filtered_sessions: Vec = sessions - .into_values() - .filter(|projection| { - matches_scope( - scope_from_label(projection.session.execution.scope), - args.scope, - ) && matches_role(projection.session.role.as_deref(), role_filter) - }) - .map(|projection| projection.session) - .collect(); + let mut filtered_sessions = Vec::new(); + for projection in sessions.into_values() { + if !matches_scope( + scope_from_label(projection.session.execution.scope), + args.scope, + ) || !matches_role(projection.session.role.as_deref(), role_filter) + { + continue; + } + + if projection.source.is_world_scoped + && (!projection.source.has_top_level_world_id + || !projection.source.has_top_level_world_generation) + { + return Err(config_model::user_error(format!( + "malformed world identity on newest selected world-scoped pure-agent status event: agent_id={} orchestration_session_id={} run_id={} ts={} requires top-level world_id and world_generation", + projection.source.agent_id, + projection.source.orchestration_session_id, + projection.source.run_id, + projection.source.ts.to_rfc3339(), + ))); + } + + filtered_sessions.push(projection.session); + } let allowed_parents: BTreeSet<(String, String)> = filtered_sessions .iter() @@ -1108,9 +1140,40 @@ fn validate_orchestrator_selection<'a>( )); } + if entry.file.config.protocol.as_deref() != Some(PURE_AGENT_PROTOCOL) { + return Err(format!( + "orchestrator agent '{}' does not advertise protocol '{}'", + orchestrator_agent_id, PURE_AGENT_PROTOCOL + )); + } + + if let Some(capability) = + missing_required_orchestrator_capability(&entry.file.config.capabilities) + { + return Err(format!( + "orchestrator agent '{}' is missing required capability '{}'", + orchestrator_agent_id, capability + )); + } + Ok(entry) } +fn missing_required_orchestrator_capability( + capabilities: &AgentCapabilitiesV1, +) -> Option<&'static str> { + [ + ("session_start", capabilities.session_start), + ("session_resume", capabilities.session_resume), + ("session_fork", capabilities.session_fork), + ("session_stop", capabilities.session_stop), + ("status_snapshot", capabilities.status_snapshot), + ("event_stream", capabilities.event_stream), + ] + .into_iter() + .find_map(|(name, enabled)| (!enabled).then_some(name)) +} + fn policy_allowlist_failure( effective_config: &SubstrateConfig, inventory: &BTreeMap, diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index 61dcd3e56..d9b4ad741 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -9,6 +9,8 @@ use std::path::PathBuf; use std::process::Output; use tempfile::TempDir; +const PURE_AGENT_PROTOCOL: &str = "uaa.agent.session"; + struct AgentSuccessorFixture { _temp: TempDir, home: PathBuf, @@ -116,6 +118,18 @@ impl AgentSuccessorFixture { } fn seed_inventory_for_doctor_contract(&self) { + self.seed_doctor_prereqs(); + self.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + self.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + } + + fn seed_doctor_prereqs(&self) { self.write_global_config_patch( r#"agents: enabled: true @@ -130,14 +144,6 @@ impl AgentSuccessorFixture { - cli:helper "#, ); - self.write_agent_file( - "claude_code.yaml", - &cli_agent_file("claude_code", "host", true, true, true), - ); - self.write_agent_file( - "helper.yaml", - &cli_agent_file("helper", "host", false, true, true), - ); } } @@ -148,11 +154,61 @@ fn cli_agent_file( mcp_client: bool, enabled: bool, ) -> String { - format!( - "version: 1\nid: {agent_id}\nconfig:\n kind: cli\n enabled: {enabled}\n execution:\n scope: {scope}\n cli:\n binary: {agent_id}\n mode: persistent\n capabilities:\n llm: {llm}\n mcp_client: {mcp_client}\n" + cli_agent_file_with_session_contract( + agent_id, + scope, + llm, + mcp_client, + enabled, + Some(PURE_AGENT_PROTOCOL), + None, + None, ) } +fn cli_agent_file_with_session_contract( + agent_id: &str, + scope: &str, + llm: bool, + mcp_client: bool, + enabled: bool, + protocol: Option<&str>, + false_capability: Option<&str>, + omitted_capability: Option<&str>, +) -> String { + assert!( + false_capability.is_none() + || omitted_capability.is_none() + || false_capability != omitted_capability, + "a capability cannot be both explicit-false and omitted" + ); + + let mut body = + format!("version: 1\nid: {agent_id}\nconfig:\n kind: cli\n enabled: {enabled}\n"); + if let Some(protocol) = protocol { + body.push_str(&format!(" protocol: {protocol}\n")); + } + body.push_str(&format!( + " execution:\n scope: {scope}\n cli:\n binary: {agent_id}\n mode: persistent\n capabilities:\n" + )); + for capability in [ + "session_start", + "session_resume", + "session_fork", + "session_stop", + "status_snapshot", + "event_stream", + ] { + if omitted_capability == Some(capability) { + continue; + } + let value = false_capability != Some(capability); + body.push_str(&format!(" {capability}: {value}\n")); + } + body.push_str(&format!(" llm: {llm}\n mcp_client: {mcp_client}\n")); + body +} + fn repo_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../..") @@ -169,6 +225,81 @@ fn parse_json_output(output: &Output) -> Value { serde_json::from_slice(&output.stdout).expect("stdout should be valid JSON") } +fn assert_malformed_world_identity_failure( + output: &Output, + agent_id: &str, + orchestration_session_id: &str, + run_id: &str, + ts: &str, +) { + assert_eq!( + output.status.code(), + Some(2), + "status should fail closed on malformed world identity: {output:?}" + ); + assert!( + String::from_utf8_lossy(&output.stdout).trim().is_empty(), + "malformed world identity failures should not print stdout: {output:?}" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + for needle in [ + "malformed world identity", + &format!("agent_id={agent_id}"), + &format!("orchestration_session_id={orchestration_session_id}"), + &format!("run_id={run_id}"), + &format!("ts={ts}"), + ] { + assert!( + stderr.contains(needle), + "stderr must contain `{needle}` for malformed world identity failures: {stderr}" + ); + } +} + +fn assert_doctor_fails_at_orchestrator_selection(output: &Output, expected_reason: &str) { + assert_eq!( + output.status.code(), + Some(2), + "doctor should fail closed at orchestrator_selection: {output:?}" + ); + + let json = parse_json_output(output); + assert_eq!(json.get("healthy").and_then(Value::as_bool), Some(false)); + assert_eq!(json.get("fail_closed").and_then(Value::as_bool), Some(true)); + + let checks = json["checks"] + .as_array() + .expect("checks should be an array"); + let observed: Vec<&str> = checks + .iter() + .map(|check| { + check + .pointer("/check") + .and_then(Value::as_str) + .expect("check id should be a string") + }) + .collect(); + assert_eq!( + observed, + vec!["inventory_scan", "orchestrator_selection"], + "doctor must stop before policy_allowlist/world_boundary on orchestrator failures: {json}" + ); + assert_eq!( + checks[1].pointer("/status").and_then(Value::as_str), + Some("fail") + ); + assert_eq!( + checks[1].pointer("/reason").and_then(Value::as_str), + Some(expected_reason), + "doctor must publish the exact orchestrator denial reason: {json}" + ); + assert!( + json.get("orchestrator").is_none() || json["orchestrator"].is_null(), + "failed orchestrator selection must not publish an orchestrator summary: {json}" + ); +} + fn find_session_by_agent<'a>(sessions: &'a [Value], agent_id: &str) -> &'a Value { sessions .iter() @@ -517,6 +648,9 @@ fn agent_status_prefers_newest_pure_session_event_when_trace_lines_are_out_of_or let fixture = AgentSuccessorFixture::new(); fixture.init_workspace(); fixture.seed_inventory_for_list_and_status_contracts(); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + let run_id = "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14"; + let ts = "2026-04-05T00:00:02+00:00"; fixture.write_trace_events(&[ json!({ "ts": "2026-04-05T00:00:02Z", @@ -525,8 +659,8 @@ fn agent_status_prefers_newest_pure_session_event_when_trace_lines_are_out_of_or "component": "agent-hub", "kind": "status", "agent_id": "codex", - "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", - "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "orchestration_session_id": orchestration_session_id, + "run_id": run_id, "backend_id": "cli:codex", "client": "codex", "router": "agent_hub", @@ -541,7 +675,7 @@ fn agent_status_prefers_newest_pure_session_event_when_trace_lines_are_out_of_or "component": "agent-hub", "kind": "status", "agent_id": "codex", - "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "orchestration_session_id": orchestration_session_id, "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", "backend_id": "cli:codex", "client": "codex", @@ -555,38 +689,118 @@ fn agent_status_prefers_newest_pure_session_event_when_trace_lines_are_out_of_or ]); let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_world_identity_failure(&output, "codex", orchestration_session_id, run_id, ts); +} + +#[test] +fn agent_status_fails_when_newest_world_scoped_event_omits_top_level_world_id() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + let run_id = "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f16"; + let ts = "2026-04-05T00:00:03+00:00"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:03Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": orchestration_session_id, + "run_id": run_id, + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "world_generation": 8, + "data": { "message": "newest member session event omits top-level world_id" } + }), + json!({ + "ts": "2026-04-05T00:00:02Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "world_id": "wld_stale_0001", + "world_generation": 7, + "data": { "message": "older event remains fully formed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_world_identity_failure(&output, "codex", orchestration_session_id, run_id, ts); +} + +#[test] +fn agent_status_scope_host_ignores_filtered_out_malformed_world_rows() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:03Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f17", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "data": { "message": "filtered-out world row is malformed" } + }), + json!({ + "ts": "2026-04-05T00:00:02Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f18", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "data": { "message": "host-scoped orchestrator row remains valid" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--scope", "host", "--json"]); assert!( output.status.success(), - "status should succeed when pure-agent events arrive out of timestamp order: {output:?}" + "host scope should ignore malformed world-scoped rows that are filtered out: {output:?}" ); let json = parse_json_output(&output); let sessions = json["sessions"] .as_array() .expect("sessions should be an array"); - let codex_sessions: Vec<&Value> = sessions - .iter() - .filter(|session| session.pointer("/agent_id").and_then(Value::as_str) == Some("codex")) - .collect(); assert_eq!( - codex_sessions.len(), + sessions.len(), 1, - "out-of-order pure-agent events must still collapse to one codex session row: {json}" + "--scope host should only emit the host-scoped session row: {json}" ); - - let codex = codex_sessions[0]; + let session = find_session_by_agent(sessions, "claude_code"); assert_eq!( - codex.pointer("/last_event_at").and_then(Value::as_str), - Some("2026-04-05T00:00:02+00:00"), - "the collapsed codex row must use the newest event timestamp rather than later file order: {json}" - ); - assert!( - codex.get("world_id").is_none(), - "the winning newest projection must not inherit stale world_id from the older event: {json}" - ); - assert!( - codex.get("world_generation").is_none(), - "the winning newest projection must not inherit stale world_generation from the older event: {json}" + session.pointer("/execution/scope").and_then(Value::as_str), + Some("host") ); } @@ -826,6 +1040,8 @@ fn agent_status_unsupported_event_roles_fall_back_to_contract_roles() { "router": "agent_hub", "protocol": "uaa.agent.session", "role": "unexpected", + "world_id": "wld_active_0002", + "world_generation": 7, "data": { "message": "unsupported role should collapse to null" } }), ]); @@ -966,6 +1182,126 @@ fn agent_doctor_json_locks_field_names_omissions_and_check_order() { ); } +#[test] +fn agent_doctor_fails_at_orchestrator_selection_when_protocol_is_missing() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_doctor_prereqs(); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file_with_session_contract( + "claude_code", + "host", + true, + true, + true, + None, + None, + None, + ), + ); + fixture.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_doctor_fails_at_orchestrator_selection( + &output, + "orchestrator agent 'claude_code' does not advertise protocol 'uaa.agent.session'", + ); +} + +#[test] +fn agent_doctor_fails_at_orchestrator_selection_when_protocol_is_wrong() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_doctor_prereqs(); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file_with_session_contract( + "claude_code", + "host", + true, + true, + true, + Some("openai.responses"), + None, + None, + ), + ); + fixture.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_doctor_fails_at_orchestrator_selection( + &output, + "orchestrator agent 'claude_code' does not advertise protocol 'uaa.agent.session'", + ); +} + +#[test] +fn agent_doctor_fails_at_orchestrator_selection_when_required_capability_is_false() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_doctor_prereqs(); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file_with_session_contract( + "claude_code", + "host", + true, + true, + true, + Some(PURE_AGENT_PROTOCOL), + Some("event_stream"), + None, + ), + ); + fixture.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_doctor_fails_at_orchestrator_selection( + &output, + "orchestrator agent 'claude_code' is missing required capability 'event_stream'", + ); +} + +#[test] +fn agent_doctor_fails_at_orchestrator_selection_when_required_capability_is_omitted() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_doctor_prereqs(); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file_with_session_contract( + "claude_code", + "host", + true, + true, + true, + Some(PURE_AGENT_PROTOCOL), + None, + Some("event_stream"), + ), + ); + fixture.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_doctor_fails_at_orchestrator_selection( + &output, + "orchestrator agent 'claude_code' is missing required capability 'event_stream'", + ); +} + #[test] fn docs_usage_and_repo_boundary_match_the_successor_contract() { let usage = read_repo_file("docs/USAGE.md"); diff --git a/crates/shell/tests/agents_validate.rs b/crates/shell/tests/agents_validate.rs index 6a6f2641c..dd0484f09 100644 --- a/crates/shell/tests/agents_validate.rs +++ b/crates/shell/tests/agents_validate.rs @@ -77,6 +77,19 @@ impl AgentsValidateFixture { } fn valid_cli_agent_file(agent_id: &str, policy_overlay: Option<&str>) -> String { + let overlay = policy_overlay.unwrap_or(""); + if overlay.is_empty() { + format!( + "version: 1\nid: {agent_id}\nconfig:\n kind: cli\n enabled: true\n protocol: uaa.agent.session\n execution:\n scope: world\n cli:\n binary: codex\n mode: persistent\n capabilities:\n session_start: true\n session_resume: true\n session_fork: true\n session_stop: true\n status_snapshot: true\n event_stream: true\n" + ) + } else { + format!( + "version: 1\nid: {agent_id}\nconfig:\n kind: cli\n enabled: true\n protocol: uaa.agent.session\n execution:\n scope: world\n cli:\n binary: codex\n mode: persistent\n capabilities:\n session_start: true\n session_resume: true\n session_fork: true\n session_stop: true\n status_snapshot: true\n event_stream: true\npolicy_overlay:\n{overlay}" + ) + } +} + +fn legacy_valid_cli_agent_file(agent_id: &str, policy_overlay: Option<&str>) -> String { let overlay = policy_overlay.unwrap_or(""); if overlay.is_empty() { format!( @@ -114,6 +127,19 @@ world_fs: ); } +#[test] +fn agents_validate_accepts_legacy_inventory_without_protocol_and_session_capabilities() { + let fixture = AgentsValidateFixture::new(); + fixture.init_workspace(); + fixture.write_agent_file("codex.yaml", &legacy_valid_cli_agent_file("codex", None)); + + let output = fixture.validate(); + assert!( + output.status.success(), + "legacy inventory without protocol/session capabilities should still succeed: {output:?}" + ); +} + #[test] fn agents_validate_rejects_unknown_keys_with_exit_2() { let fixture = AgentsValidateFixture::new(); @@ -147,6 +173,88 @@ config: ); } +#[test] +fn agents_validate_rejects_protocol_with_leading_or_trailing_whitespace() { + let fixture = AgentsValidateFixture::new(); + fixture.init_workspace(); + fixture.write_agent_file( + "bad_protocol.yaml", + r#"version: 1 +id: bad_protocol +config: + kind: cli + enabled: true + protocol: " uaa.agent.session " + execution: + scope: world + cli: + binary: codex + mode: persistent + capabilities: + session_start: true + session_resume: true + session_fork: true + session_stop: true + status_snapshot: true + event_stream: true +"#, + ); + + let output = fixture.validate(); + assert_eq!( + output.status.code(), + Some(2), + "whitespace-padded protocol should exit 2: {output:?}" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("bad_protocol.yaml") + && stderr.contains("config.protocol must not include leading or trailing whitespace"), + "stderr should mention protocol whitespace rejection\nstderr: {stderr}" + ); +} + +#[test] +fn agents_validate_rejects_invalid_dotted_protocol_with_exit_2() { + let fixture = AgentsValidateFixture::new(); + fixture.init_workspace(); + fixture.write_agent_file( + "bad_protocol.yaml", + r#"version: 1 +id: bad_protocol +config: + kind: cli + enabled: true + protocol: "uaa agent session" + execution: + scope: world + cli: + binary: codex + mode: persistent + capabilities: + session_start: true + session_resume: true + session_fork: true + session_stop: true + status_snapshot: true + event_stream: true +"#, + ); + + let output = fixture.validate(); + assert_eq!( + output.status.code(), + Some(2), + "invalid dotted protocol should exit 2: {output:?}" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("bad_protocol.yaml") + && stderr.contains("config.protocol 'uaa agent session' must be a lowercase dotted id"), + "stderr should mention dotted-id validation\nstderr: {stderr}" + ); +} + #[test] fn agents_validate_rejects_filename_id_mismatch_with_exit_2() { let fixture = AgentsValidateFixture::new(); From cf82ae491cc52cae58f4bd7e8971eaa631eeb621 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 17:19:34 -0400 Subject: [PATCH 45/48] Preserve parent run IDs in agent event traces --- crates/common/src/agent_events.rs | 6 + .../tests/agent_hub_event_envelope_schema.rs | 39 ++ crates/shell/src/execution/agents_cmd.rs | 90 +++- .../tests/agent_hub_trace_persistence.rs | 36 ++ .../agent_successor_contract_ahcsitc0.rs | 491 ++++++++++++++++-- crates/world-agent/src/service.rs | 1 + docs/TRACE.md | 2 + docs/USAGE.md | 1 + .../agent-hub-session-protocol-spec.md | 4 + .../manual_testing_playbook.md | 6 + .../telemetry-spec.md | 1 + 11 files changed, 633 insertions(+), 44 deletions(-) diff --git a/crates/common/src/agent_events.rs b/crates/common/src/agent_events.rs index d053bc4f0..fc8bce93f 100644 --- a/crates/common/src/agent_events.rs +++ b/crates/common/src/agent_events.rs @@ -78,6 +78,8 @@ pub struct AgentEvent { pub agent_id: String, pub orchestration_session_id: String, pub run_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent_run_id: Option, // Attribution + correlation (optional) #[serde(default, skip_serializing_if = "Option::is_none")] @@ -123,6 +125,8 @@ struct AgentEventDef { orchestration_session_id: String, run_id: String, #[serde(default)] + parent_run_id: Option, + #[serde(default)] backend_id: Option, #[serde(default)] thread_id: Option, @@ -192,6 +196,7 @@ impl AgentEvent { kind, orchestration_session_id: orchestration_session_id.into(), run_id: run_id.into(), + parent_run_id: None, data, backend_id: None, thread_id: None, @@ -357,6 +362,7 @@ impl TryFrom for AgentEvent { agent_id: value.agent_id, orchestration_session_id: value.orchestration_session_id, run_id: value.run_id, + parent_run_id: value.parent_run_id, backend_id: value.backend_id, thread_id: value.thread_id, role: value.role, diff --git a/crates/common/tests/agent_hub_event_envelope_schema.rs b/crates/common/tests/agent_hub_event_envelope_schema.rs index ec3b2efd1..3c9a44c4b 100644 --- a/crates/common/tests/agent_hub_event_envelope_schema.rs +++ b/crates/common/tests/agent_hub_event_envelope_schema.rs @@ -50,6 +50,40 @@ fn envelope_missing_required_fields_is_rejected() { ); } +#[test] +fn parent_run_id_roundtrips_when_present() { + let mut value = minimal_valid_envelope_json(); + value.as_object_mut().expect("envelope object").insert( + "parent_run_id".to_string(), + json!("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f11"), + ); + + let event: AgentEvent = serde_json::from_value(value).expect("deserialize AgentEvent"); + let roundtrip = serde_json::to_value(&event).expect("serialize AgentEvent"); + + assert_eq!( + roundtrip.get("parent_run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f11"), + "expected parent_run_id to survive roundtrip; got: {roundtrip}" + ); +} + +#[test] +fn parent_run_id_omits_by_field_absence_when_unset() { + let event: AgentEvent = + serde_json::from_value(minimal_valid_envelope_json()).expect("deserialize AgentEvent"); + let roundtrip = serde_json::to_value(&event).expect("serialize AgentEvent"); + + assert!( + roundtrip.get("parent_run_id").is_none(), + "expected parent_run_id to omit when unset; got: {roundtrip}" + ); + assert!( + !roundtrip.to_string().contains("\"parent_run_id\":null"), + "parent_run_id must not serialize as null when unset: {roundtrip}" + ); +} + #[test] fn safe_channel_roundtrips() { let mut value = minimal_valid_envelope_json(); @@ -360,6 +394,7 @@ fn trace_tuple_metadata_preserves_existing_join_keys() { "agent_id": "demo-agent", "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f10", "backend_id": "cli:codex", "world_id": "wld_test", "cmd_id": "cmd_test", @@ -396,6 +431,10 @@ fn trace_tuple_metadata_preserves_existing_join_keys() { roundtrip.get("span_id").and_then(Value::as_str), Some("spn_test") ); + assert_eq!( + roundtrip.get("parent_run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f10") + ); assert_eq!( roundtrip .pointer("/identity_tuple/router") diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index f6621ebad..40c045950 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -368,6 +368,15 @@ struct SessionProjectionSource { struct NestedProjection { record: NestedLlmRecordJson, sort_key: (String, String, String), + source: NestedProjectionSource, +} + +#[derive(Clone)] +struct NestedProjectionSource { + orchestration_session_id: String, + agent_id: String, + run_id: String, + parent_run_id: Option, } fn build_status_report<'a>( @@ -396,6 +405,7 @@ fn build_status_report<'a>( let events = read_trace_agent_events()?; let mut sessions = BTreeMap::<(String, String), SessionProjection>::new(); let mut nested = BTreeMap::<(String, String, String), NestedProjection>::new(); + let mut historical_parent_runs = BTreeMap::<(String, String), BTreeSet>::new(); for event in events { let Some(entry) = context.inventory.get(&event.agent_id) else { @@ -417,6 +427,10 @@ fn build_status_report<'a>( ); if let Some(session_key) = pure_session_key(&event) { + historical_parent_runs + .entry(session_key.clone()) + .or_default() + .insert(event.run_id.clone()); let orchestration_session_id = session_key.0.clone(); let world_id = event.world_id.clone(); let world_generation = event.world_generation; @@ -471,7 +485,7 @@ fn build_status_report<'a>( } } - let mut filtered_sessions = Vec::new(); + let mut filtered_session_projections = Vec::new(); for projection in sessions.into_values() { if !matches_scope( scope_from_label(projection.session.execution.scope), @@ -494,28 +508,60 @@ fn build_status_report<'a>( ))); } - filtered_sessions.push(projection.session); + filtered_session_projections.push(projection); } - let allowed_parents: BTreeSet<(String, String)> = filtered_sessions + let selected_parent_runs: BTreeMap<(String, String), String> = filtered_session_projections .iter() - .map(|session| { + .map(|projection| { ( - session.orchestration_session_id.clone(), - session.agent_id.clone(), + ( + projection.source.orchestration_session_id.clone(), + projection.source.agent_id.clone(), + ), + projection.source.run_id.clone(), ) }) .collect(); - let filtered_nested: Vec = nested - .into_values() - .filter(|projection| { - allowed_parents.contains(&( - projection.record.parent.orchestration_session_id.clone(), - projection.record.parent.agent_id.clone(), - )) - }) - .map(|projection| projection.record) + let mut filtered_nested = Vec::new(); + for projection in nested.into_values() { + let parent_key = ( + projection.source.orchestration_session_id.clone(), + projection.source.agent_id.clone(), + ); + let Some(selected_parent_run_id) = selected_parent_runs.get(&parent_key) else { + continue; + }; + let parent_run_id = projection.source.parent_run_id.as_deref(); + let parent_run_matches_selected = parent_run_id == Some(selected_parent_run_id.as_str()); + if parent_run_matches_selected { + filtered_nested.push(projection.record); + continue; + } + + let invalid_parent_run_id = format_invalid_parent_run_id(parent_run_id); + let historical_match = parent_run_id.is_some_and(|candidate| { + historical_parent_runs + .get(&parent_key) + .is_some_and(|runs| runs.contains(candidate)) + }); + if historical_match { + continue; + } + + return Err(config_model::user_error(format!( + "malformed nested parent correlation on selected status surface: agent_id={} orchestration_session_id={} run_id={} parent_run_id={} requires parent_run_id to match the winning selected pure-agent run or a known historical pure-agent run for the same session", + projection.source.agent_id, + projection.source.orchestration_session_id, + projection.source.run_id, + invalid_parent_run_id, + ))); + } + + let filtered_sessions: Vec = filtered_session_projections + .into_iter() + .map(|projection| projection.session) .collect(); Ok(StatusReportJson { @@ -681,9 +727,23 @@ fn nested_projection( protocol: tuple.protocol.clone(), }, sort_key, + source: NestedProjectionSource { + orchestration_session_id: event.orchestration_session_id.clone(), + agent_id: event.agent_id.clone(), + run_id: event.run_id.clone(), + parent_run_id: event.parent_run_id.clone(), + }, }) } +fn format_invalid_parent_run_id(parent_run_id: Option<&str>) -> String { + match parent_run_id { + Some("") => "".to_string(), + Some(value) => value.to_string(), + None => "".to_string(), + } +} + fn scope_for_event( event: &AgentEvent, entry: &AgentInventoryEntryV1, diff --git a/crates/shell/tests/agent_hub_trace_persistence.rs b/crates/shell/tests/agent_hub_trace_persistence.rs index 2327d1f96..504884cff 100644 --- a/crates/shell/tests/agent_hub_trace_persistence.rs +++ b/crates/shell/tests/agent_hub_trace_persistence.rs @@ -11,6 +11,7 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +use substrate_common::agent_events::{AgentEvent, MessageEventKind}; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; use support::{binary_path, ensure_substrate_built, temp_dir}; @@ -376,3 +377,38 @@ fn agent_events_append_flattened_agent_event_records_with_join_keys() { ); } } + +#[test] +fn flattened_agent_event_records_retain_parent_run_id_when_present() { + let mut event = AgentEvent::message( + "nested-agent", + "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + MessageEventKind::Status, + "nested gateway request completed", + ); + event.parent_run_id = Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13".to_string()); + + let mut record = event.to_trace_record().expect("flatten agent event"); + let obj = record + .as_object_mut() + .expect("agent event trace record should be an object"); + obj.insert( + "event_type".to_string(), + Value::String("agent_event".to_string()), + ); + obj.insert( + "session_id".to_string(), + Value::String("ses_agent_hub".to_string()), + ); + obj.insert( + "component".to_string(), + Value::String("agent-hub".to_string()), + ); + + assert_eq!( + record.get("parent_run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13"), + "flattened agent_event records must retain parent_run_id when present: {record:?}" + ); +} diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index d9b4ad741..0471c3ce4 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -11,6 +11,27 @@ use tempfile::TempDir; const PURE_AGENT_PROTOCOL: &str = "uaa.agent.session"; +#[derive(Clone, Copy)] +enum CapabilityOverride<'a> { + ForceFalse(&'a str), + Omit(&'a str), +} + +#[derive(Clone, Copy)] +struct SessionContractOptions<'a> { + protocol: Option<&'a str>, + capability_override: Option>, +} + +impl SessionContractOptions<'_> { + const fn default() -> Self { + Self { + protocol: Some(PURE_AGENT_PROTOCOL), + capability_override: None, + } + } +} + struct AgentSuccessorFixture { _temp: TempDir, home: PathBuf, @@ -160,32 +181,21 @@ fn cli_agent_file( llm, mcp_client, enabled, - Some(PURE_AGENT_PROTOCOL), - None, - None, + SessionContractOptions::default(), ) } -fn cli_agent_file_with_session_contract( +fn cli_agent_file_with_session_contract<'a>( agent_id: &str, scope: &str, llm: bool, mcp_client: bool, enabled: bool, - protocol: Option<&str>, - false_capability: Option<&str>, - omitted_capability: Option<&str>, + options: SessionContractOptions<'a>, ) -> String { - assert!( - false_capability.is_none() - || omitted_capability.is_none() - || false_capability != omitted_capability, - "a capability cannot be both explicit-false and omitted" - ); - let mut body = format!("version: 1\nid: {agent_id}\nconfig:\n kind: cli\n enabled: {enabled}\n"); - if let Some(protocol) = protocol { + if let Some(protocol) = options.protocol { body.push_str(&format!(" protocol: {protocol}\n")); } body.push_str(&format!( @@ -199,10 +209,17 @@ fn cli_agent_file_with_session_contract( "status_snapshot", "event_stream", ] { - if omitted_capability == Some(capability) { + if matches!( + options.capability_override, + Some(CapabilityOverride::Omit(omitted)) if omitted == capability + ) { continue; } - let value = false_capability != Some(capability); + let value = !matches!( + options.capability_override, + Some(CapabilityOverride::ForceFalse(false_capability)) + if false_capability == capability + ); body.push_str(&format!(" {capability}: {value}\n")); } body.push_str(&format!(" llm: {llm}\n mcp_client: {mcp_client}\n")); @@ -257,6 +274,38 @@ fn assert_malformed_world_identity_failure( } } +fn assert_malformed_nested_parent_correlation_failure( + output: &Output, + agent_id: &str, + orchestration_session_id: &str, + run_id: &str, + parent_run_id: &str, +) { + assert_eq!( + output.status.code(), + Some(2), + "status should fail closed on malformed nested parent correlation: {output:?}" + ); + assert!( + String::from_utf8_lossy(&output.stdout).trim().is_empty(), + "malformed nested parent correlation failures should not print stdout: {output:?}" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + for needle in [ + "malformed nested parent correlation on selected status surface", + &format!("agent_id={agent_id}"), + &format!("orchestration_session_id={orchestration_session_id}"), + &format!("run_id={run_id}"), + &format!("parent_run_id={parent_run_id}"), + ] { + assert!( + stderr.contains(needle), + "stderr must contain `{needle}` for malformed nested parent correlation failures: {stderr}" + ); + } +} + fn assert_doctor_fails_at_orchestrator_selection(output: &Output, expected_reason: &str) { assert_eq!( output.status.code(), @@ -307,6 +356,53 @@ fn find_session_by_agent<'a>(sessions: &'a [Value], agent_id: &str) -> &'a Value .unwrap_or_else(|| panic!("expected session row for agent `{agent_id}`")) } +fn seed_nested_gateway_status_fixture(fixture: &AgentSuccessorFixture) { + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "world", true, true, true), + ); +} + #[test] fn plural_agents_namespace_keeps_validate_as_the_only_compatibility_leaf() { let fixture = AgentSuccessorFixture::new(); @@ -1195,9 +1291,10 @@ fn agent_doctor_fails_at_orchestrator_selection_when_protocol_is_missing() { true, true, true, - None, - None, - None, + SessionContractOptions { + protocol: None, + capability_override: None, + }, ), ); fixture.write_agent_file( @@ -1225,9 +1322,10 @@ fn agent_doctor_fails_at_orchestrator_selection_when_protocol_is_wrong() { true, true, true, - Some("openai.responses"), - None, - None, + SessionContractOptions { + protocol: Some("openai.responses"), + capability_override: None, + }, ), ); fixture.write_agent_file( @@ -1255,9 +1353,10 @@ fn agent_doctor_fails_at_orchestrator_selection_when_required_capability_is_fals true, true, true, - Some(PURE_AGENT_PROTOCOL), - Some("event_stream"), - None, + SessionContractOptions { + protocol: Some(PURE_AGENT_PROTOCOL), + capability_override: Some(CapabilityOverride::ForceFalse("event_stream")), + }, ), ); fixture.write_agent_file( @@ -1285,9 +1384,10 @@ fn agent_doctor_fails_at_orchestrator_selection_when_required_capability_is_omit true, true, true, - Some(PURE_AGENT_PROTOCOL), - None, - Some("event_stream"), + SessionContractOptions { + protocol: Some(PURE_AGENT_PROTOCOL), + capability_override: Some(CapabilityOverride::Omit("event_stream")), + }, ), ); fixture.write_agent_file( @@ -1953,6 +2053,339 @@ metadata: {} ); } +#[test] +fn agent_status_ignores_stale_nested_rows_from_historical_parent_runs() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0001", + "world_generation": 6, + "data": { "message": "older pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "stale nested gateway request completed" } + }), + json!({ + "ts": "2026-04-05T00:00:02Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "newest pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:03Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f16", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "current nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "status should ignore stale nested rows tied to historical pure-agent runs: {output:?}" + ); + + let json = parse_json_output(&output); + let nested = json["nested_llm_records"] + .as_array() + .expect("nested_llm_records should be an array"); + assert_eq!( + nested.len(), + 1, + "only nested rows for the winning selected parent run should remain: {json}" + ); + assert_eq!( + nested[0].pointer("/run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f16") + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_omits_parent_run_id() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_parent_correlation_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "", + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_has_empty_parent_run_id() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_parent_correlation_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "", + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_has_unknown_parent_run_id() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + let bad_parent_run_id = "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6faa"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": bad_parent_run_id, + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_parent_correlation_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + bad_parent_run_id, + ); +} + +#[test] +fn agent_status_ignores_malformed_nested_rows_when_parent_surface_is_filtered_out() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--role", "member", "--json"]); + assert!( + output.status.success(), + "status should ignore malformed nested rows when the parent pure-agent row is filtered out: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!( + json.pointer("/role_filter").and_then(Value::as_str), + Some("member") + ); + assert_eq!( + json["sessions"] + .as_array() + .expect("sessions should be an array") + .len(), + 0, + "filtered-out parent rows should not remain in the selected output surface: {json}" + ); + assert_eq!( + json["nested_llm_records"] + .as_array() + .expect("nested_llm_records should be an array") + .len(), + 0, + "nested rows under filtered-out parents should be ignored rather than failing or emitting output: {json}" + ); +} + #[test] fn nested_gateway_policy_does_not_inherit_agent_hub_success_by_implication() { let fixture = AgentSuccessorFixture::new(); diff --git a/crates/world-agent/src/service.rs b/crates/world-agent/src/service.rs index 8a373004d..9417618c9 100644 --- a/crates/world-agent/src/service.rs +++ b/crates/world-agent/src/service.rs @@ -1320,6 +1320,7 @@ impl WorldAgentService { kind: AgentEventKind::Status, orchestration_session_id: span_id.clone(), run_id: span_id.clone(), + parent_run_id: None, backend_id: None, thread_id: None, role: None, diff --git a/docs/TRACE.md b/docs/TRACE.md index 498411dc6..056204eaa 100644 --- a/docs/TRACE.md +++ b/docs/TRACE.md @@ -159,12 +159,14 @@ Agent-hub successor telemetry keeps adapter identity separate from semantic iden - `protocol`: protocol family for the record, such as `uaa.agent.session`. - `provider`: nested gateway-backed provider identity only; pure-agent records omit it. - `auth_authority`: nested gateway-backed auth authority only; pure-agent records omit it. +- `parent_run_id`: nested gateway-backed trace correlation only; points at the parent pure-agent `run_id`. - `world_id`: world boundary identifier for world-scoped pure-agent records and in-world telemetry families. - `world_generation`: generation counter for the active world-scoped pure-agent session or world-backed execution. Operator-facing omission rules: - Pure-agent records keep `client`, `router`, and `protocol`, and omit `provider` plus `auth_authority`. - Nested gateway-backed records may add `provider` and `auth_authority`, but they must omit `world_id` and `world_generation`. +- Nested gateway-backed `agent_event` records carry `parent_run_id` in trace; status consumers ignore stale historical nested rows and fail closed only on malformed selected-surface parent correlation. - Host-scoped pure-agent records omit `world_id` and `world_generation`. ### Router-Derived Event Families (workflow router daemon; Phase 8) diff --git a/docs/USAGE.md b/docs/USAGE.md index b0d01d57e..f4df596b5 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -81,6 +81,7 @@ Operator-visible identity rules on these surfaces: - Pure-agent status rows omit `provider` and `auth_authority`. - `world_id` and `world_generation` render only for world-scoped pure-agent session rows. - Nested gateway-backed status rows stay separate from pure-agent rows and are the only rows that publish `provider` and `auth_authority`. +- Nested gateway-backed status rows depend on valid trace-side `parent_run_id` correlation; stale historical nested rows are ignored, and malformed selected-surface rows fail closed. ## PTY Support diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md index 9f549ecb7..8146bc4d3 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md @@ -320,6 +320,10 @@ Field rules: - `client` remains the parent agent id. - `router` is exactly `substrate_gateway`. - `provider` and `auth_authority` are required. +- The source nested trace/event record carries `parent_run_id`, but `NestedLlmStatusRecordV1` does not surface it publicly. +- Status emits a nested row only when that source `parent_run_id` matches the winning selected pure-agent `run_id` for the same `(orchestration_session_id, agent_id)` pair. +- Nested rows tied to older historical pure-agent runs for the same pair are ignored as stale history. +- Missing, empty, or otherwise unknown `parent_run_id` on a nested row whose parent pure-agent row is selected causes `substrate agent status` to fail closed. - `world_id` and `world_generation` never appear on `NestedLlmStatusRecordV1`. ### `AgentStatusResponseV1` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md index 060b5c777..9a4b68f72 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md @@ -260,6 +260,12 @@ jq -e ' ' "$artifacts_dir/agent-status-nested.json" ``` +Expected behavior notes: + +- `substrate agent status --json` exits `0` only when nested trace records have valid selected-surface parent correlation. +- Nested rows tied to older historical pure-agent runs for the same `(orchestration_session_id, agent_id)` pair may be absent from `nested_llm_records`; they are ignored as stale history. +- If a nested row would otherwise be selected but its source `parent_run_id` is missing, empty, or unknown for that pair, `substrate agent status` fails closed instead of emitting partial output. + ```bash substrate agent status | tee "$artifacts_dir/agent-status-nested.txt" ``` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md index 7cffc184e..84555d947 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md @@ -122,6 +122,7 @@ Omission rules: - `client` on the nested record stays equal to the parent agent runtime id. - `router`, `provider`, `auth_authority`, and `protocol` on the nested record describe only the nested gateway-backed request. - The nested record never mutates, widens, or retroactively annotates the parent pure-agent record. +- Status consumers use `parent_run_id` for correlation validation only. A nested row is emitted only when its `parent_run_id` matches the winning selected pure-agent `run_id` for that `(orchestration_session_id, agent_id)` pair. Nested rows tied to older historical pure-agent runs are ignored as stale history, and malformed selected-surface rows fail closed. ## `world_generation` publication path From 7f5b07fa8d9bd132b26d935237e80ce09698c945 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 17:52:45 -0400 Subject: [PATCH 46/48] Fix macOS gateway tests to clear SUBSTRATE_HOME --- crates/shell/src/builtins/world_gateway.rs | 54 ++++++++++++---------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/crates/shell/src/builtins/world_gateway.rs b/crates/shell/src/builtins/world_gateway.rs index df120a859..11cb712dd 100644 --- a/crates/shell/src/builtins/world_gateway.rs +++ b/crates/shell/src/builtins/world_gateway.rs @@ -1051,13 +1051,15 @@ mod tests { }); with_env_var("HOME", Some(home.as_os_str()), || { - with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { - match resolve_macos_gateway_client_endpoint() { - MacosGatewayClientEndpoint::Unix(path) => assert_eq!(path, sock), - MacosGatewayClientEndpoint::Tcp { .. } => { - panic!("expected unix endpoint when host socket exists") + with_env_var("SUBSTRATE_HOME", None, || { + with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { + match resolve_macos_gateway_client_endpoint() { + MacosGatewayClientEndpoint::Unix(path) => assert_eq!(path, sock), + MacosGatewayClientEndpoint::Tcp { .. } => { + panic!("expected unix endpoint when host socket exists") + } } - } + }) }) }); @@ -1070,16 +1072,18 @@ mod tests { let home = temp.path(); with_env_var("HOME", Some(home.as_os_str()), || { - with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { - match resolve_macos_gateway_client_endpoint() { - MacosGatewayClientEndpoint::Tcp { host, port } => { - assert_eq!(host, "127.0.0.1"); - assert_eq!(port, 17788); - } - MacosGatewayClientEndpoint::Unix(path) => { - panic!("expected tcp fallback when socket is missing, got {path:?}") + with_env_var("SUBSTRATE_HOME", None, || { + with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { + match resolve_macos_gateway_client_endpoint() { + MacosGatewayClientEndpoint::Tcp { host, port } => { + assert_eq!(host, "127.0.0.1"); + assert_eq!(port, 17788); + } + MacosGatewayClientEndpoint::Unix(path) => { + panic!("expected tcp fallback when socket is missing, got {path:?}") + } } - } + }) }) }); } @@ -1116,16 +1120,18 @@ mod tests { std::fs::write(&sock, "").expect("create placeholder socket path"); with_env_var("HOME", Some(home.as_os_str()), || { - with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { - match resolve_macos_gateway_client_endpoint() { - MacosGatewayClientEndpoint::Tcp { host, port } => { - assert_eq!(host, "127.0.0.1"); - assert_eq!(port, 17788); - } - MacosGatewayClientEndpoint::Unix(path) => { - panic!("expected tcp fallback when socket is stale, got {path:?}") + with_env_var("SUBSTRATE_HOME", None, || { + with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { + match resolve_macos_gateway_client_endpoint() { + MacosGatewayClientEndpoint::Tcp { host, port } => { + assert_eq!(host, "127.0.0.1"); + assert_eq!(port, 17788); + } + MacosGatewayClientEndpoint::Unix(path) => { + panic!("expected tcp fallback when socket is stale, got {path:?}") + } } - } + }) }) }); } From b6a583da2adb1c700d0bdc6f3c0c36cd9e3abf54 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 19:20:35 -0400 Subject: [PATCH 47/48] Refine agent doctor policy allowlist handling --- .../agent-hub-session-protocol-spec.md | 5 +++-- .../manual_testing_playbook.md | 21 +++++++++++++++++++ .../policy-spec.md | 16 +++++++++++--- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md index 8146bc4d3..1b1435dec 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md @@ -189,12 +189,13 @@ An inventory item is orchestrator-eligible only when all of these checks pass: 8. `capabilities.session_stop=true` 9. `capabilities.status_snapshot=true` 10. `capabilities.event_stream=true` -11. the derived `backend_id` is allowed by `agents.allowed_backends` + +After orchestrator eligibility succeeds, control-plane policy evaluation separately checks that the selected derived `backend_id` is allowed by `agents.allowed_backends`. Failure rules: - The hub never falls back to a different orchestrator. - A world-scoped candidate is invalid even when every other capability check passes. -- `substrate agent doctor` and every control-plane entrypoint fail closed on any orchestrator-eligibility failure. +- `substrate agent doctor` and every control-plane entrypoint fail closed on any orchestrator-eligibility or policy-allowlist failure. ## Member dispatch model diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md index 9a4b68f72..e6412770f 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md @@ -377,6 +377,27 @@ Expected results: - the first failing check is `orchestrator_selection` - the reason names the exact failing condition from `policy-spec.md` +### Case 7a — `substrate agent doctor` fails closed for policy allowlist denial + +Run this case against two fixture variants: + +- host-scoped orchestrator whose derived `backend_id` is absent from `agents.allowed_backends` +- required world-scoped member whose derived `backend_id` is absent from `agents.allowed_backends` + +Command: + +```bash +substrate agent doctor --json +``` + +Expected results: + +- command exits `5` +- output reports `healthy = false` +- output reports `fail_closed = true` +- the first failing check is `policy_allowlist` +- the reason names the exact failing condition from `policy-spec.md` + ### Case 8 — `substrate agent doctor` fails closed for world-boundary loss Run this case against two fixture variants: diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md index a580139f3..14787eea3 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md @@ -110,7 +110,6 @@ These conditions deny before any member dispatch is considered: - `agents.hub.orchestrator_agent_id` is absent - the referenced `agent_id` is missing from the effective inventory - the referenced inventory item is disabled -- the derived orchestrator `backend_id` is denied by `agents.allowed_backends` - `execution.scope = world` - `protocol != uaa.agent.session` - any required orchestrator capability is `false` @@ -120,11 +119,22 @@ The deny explanation must identify the exact failing condition and, when applica - `"effective config agents.hub.orchestrator_agent_id is unset"` - `"orchestrator agent '' is missing from effective inventory"` - `"orchestrator agent '' is disabled in effective inventory"` -- `"orchestrator backend '' is not allowlisted by effective policy agents.allowed_backends"` - `"orchestrator agent '' is world-scoped and host-scoped orchestration is required"` - `"orchestrator agent '' does not advertise protocol 'uaa.agent.session'"` - `"orchestrator agent '' is missing required capability ''"` +### Policy allowlist denial + +These conditions deny after orchestrator selection succeeds and before any world-boundary check or member dispatch is considered: + +- the selected orchestrator derived `backend_id` is denied by `agents.allowed_backends` +- any required world-scoped member derived `backend_id` is denied by `agents.allowed_backends` + +The deny explanation must identify the exact failing condition and the exact policy key: + +- `"selected orchestrator backend '' is not allowlisted by effective policy agents.allowed_backends"` +- `"required world-scoped member backend '' is not allowlisted by effective policy agents.allowed_backends"` + ### Member dispatch denial These conditions deny after orchestrator eligibility succeeds: @@ -202,7 +212,7 @@ These buckets remain distinct in docs, code, and tests. ## Acceptance criteria - Control-plane evaluation uses only the governing inputs named by this spec and rejects event-plane records as authorization inputs. -- Orchestrator eligibility always checks `agents.allowed_backends` before role, protocol, and capability gates. +- `substrate agent doctor` evaluates orchestrator host/protocol/capability selection before the `policy_allowlist` phase that checks derived `backend_id` values. - A world-scoped orchestrator is always denied. - Member dispatch always reapplies `agents.allowed_backends` to the member `backend_id`. - World-scoped member dispatch denies when the world boundary is unavailable under fail-closed posture. From 707baa30c633e8c97488d5bae71c9064fe2cf8ef Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sat, 25 Apr 2026 19:37:13 -0400 Subject: [PATCH 48/48] Fix nested selected status rows to fail closed on missing provider/auth --- crates/shell/src/execution/agents_cmd.rs | 66 ++++-- .../agent_successor_contract_ahcsitc0.rs | 192 +++++++++++++++++- 2 files changed, 239 insertions(+), 19 deletions(-) diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 40c045950..f21975641 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -366,9 +366,13 @@ struct SessionProjectionSource { #[derive(Clone)] struct NestedProjection { - record: NestedLlmRecordJson, sort_key: (String, String, String), source: NestedProjectionSource, + backend_id: String, + client: String, + protocol: String, + provider: Option, + auth_authority: Option, } #[derive(Clone)] @@ -536,7 +540,34 @@ fn build_status_report<'a>( let parent_run_id = projection.source.parent_run_id.as_deref(); let parent_run_matches_selected = parent_run_id == Some(selected_parent_run_id.as_str()); if parent_run_matches_selected { - filtered_nested.push(projection.record); + let missing_fields = missing_required_nested_fields(&projection); + if !missing_fields.is_empty() { + return Err(config_model::user_error(format!( + "malformed nested tuple on selected status surface: agent_id={} orchestration_session_id={} run_id={} missing_fields={} requires provider and auth_authority on selected nested substrate_gateway status rows", + projection.source.agent_id, + projection.source.orchestration_session_id, + projection.source.run_id, + missing_fields.join(","), + ))); + } + + filtered_nested.push(NestedLlmRecordJson { + parent: NestedParentJson { + orchestration_session_id: projection.source.orchestration_session_id.clone(), + agent_id: projection.source.agent_id.clone(), + }, + run_id: projection.source.run_id.clone(), + backend_id: projection.backend_id, + client: projection.client, + router: NESTED_ROUTER.to_string(), + provider: projection + .provider + .expect("missing required nested provider already validated"), + auth_authority: projection + .auth_authority + .expect("missing required nested auth_authority already validated"), + protocol: projection.protocol, + }); continue; } @@ -704,8 +735,6 @@ fn nested_projection( if tuple.router != NESTED_ROUTER { return None; } - let provider = tuple.provider.clone()?; - let auth_authority = tuple.auth_authority.clone()?; let sort_key = ( event.orchestration_session_id.clone(), event.agent_id.clone(), @@ -713,19 +742,6 @@ fn nested_projection( ); Some(NestedProjection { - record: NestedLlmRecordJson { - parent: NestedParentJson { - orchestration_session_id: event.orchestration_session_id.clone(), - agent_id: event.agent_id.clone(), - }, - run_id: event.run_id.clone(), - backend_id: entry.derived_backend_id(), - client: event.agent_id.clone(), - router: NESTED_ROUTER.to_string(), - provider, - auth_authority, - protocol: tuple.protocol.clone(), - }, sort_key, source: NestedProjectionSource { orchestration_session_id: event.orchestration_session_id.clone(), @@ -733,9 +749,25 @@ fn nested_projection( run_id: event.run_id.clone(), parent_run_id: event.parent_run_id.clone(), }, + backend_id: entry.derived_backend_id(), + client: event.agent_id.clone(), + protocol: tuple.protocol.clone(), + provider: tuple.provider.clone(), + auth_authority: tuple.auth_authority.clone(), }) } +fn missing_required_nested_fields(projection: &NestedProjection) -> Vec<&'static str> { + let mut missing_fields = Vec::new(); + if projection.provider.is_none() { + missing_fields.push("provider"); + } + if projection.auth_authority.is_none() { + missing_fields.push("auth_authority"); + } + missing_fields +} + fn format_invalid_parent_run_id(parent_run_id: Option<&str>) -> String { match parent_run_id { Some("") => "".to_string(), diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index 0471c3ce4..0f97cb131 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -306,6 +306,38 @@ fn assert_malformed_nested_parent_correlation_failure( } } +fn assert_malformed_nested_required_fields_failure( + output: &Output, + agent_id: &str, + orchestration_session_id: &str, + run_id: &str, + missing_fields: &str, +) { + assert_eq!( + output.status.code(), + Some(2), + "status should fail closed on malformed selected nested tuple fields: {output:?}" + ); + assert!( + String::from_utf8_lossy(&output.stdout).trim().is_empty(), + "malformed selected nested tuple field failures should not print stdout: {output:?}" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + for needle in [ + "malformed nested tuple on selected status surface", + &format!("agent_id={agent_id}"), + &format!("orchestration_session_id={orchestration_session_id}"), + &format!("run_id={run_id}"), + &format!("missing_fields={missing_fields}"), + ] { + assert!( + stderr.contains(needle), + "stderr must contain `{needle}` for malformed selected nested tuple field failures: {stderr}" + ); + } +} + fn assert_doctor_fails_at_orchestrator_selection(output: &Output, expected_reason: &str) { assert_eq!( output.status.code(), @@ -2315,6 +2347,164 @@ fn agent_status_fails_closed_when_selected_nested_row_has_unknown_parent_run_id( ); } +#[test] +fn agent_status_fails_closed_when_selected_nested_row_omits_provider() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_required_fields_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "provider", + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_omits_auth_authority() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_required_fields_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "auth_authority", + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_omits_provider_and_auth_authority() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_required_fields_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "provider,auth_authority", + ); +} + #[test] fn agent_status_ignores_malformed_nested_rows_when_parent_surface_is_filtered_out() { let fixture = AgentSuccessorFixture::new(); @@ -2351,8 +2541,6 @@ fn agent_status_ignores_malformed_nested_rows_when_parent_surface_is_filtered_ou "client": "claude_code", "router": "substrate_gateway", "protocol": "openai.responses", - "provider": "openai", - "auth_authority": "codex_subscription", "data": { "summary": "nested gateway request completed" } }), ]);