okstra 0.173.0 → 0.175.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -9
- package/bin/okstra +8 -4
- package/dist/cli-registry.d.mts +16 -0
- package/dist/cli-registry.mjs +560 -0
- package/dist/cli-registry.mjs.map +1 -0
- package/dist/commands/execute/agent-prompt.d.mts +1 -0
- package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
- package/dist/commands/execute/agent-prompt.mjs.map +1 -0
- package/dist/commands/execute/codex-dispatch.d.mts +3 -0
- package/dist/commands/execute/codex-dispatch.mjs +6 -0
- package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
- package/dist/commands/execute/codex-run.d.mts +3 -0
- package/{src → dist}/commands/execute/codex-run.mjs +30 -37
- package/dist/commands/execute/codex-run.mjs.map +1 -0
- package/dist/commands/execute/convergence.d.mts +1 -0
- package/{src → dist}/commands/execute/convergence.mjs +15 -16
- package/dist/commands/execute/convergence.mjs.map +1 -0
- package/dist/commands/execute/error-log.d.mts +1 -0
- package/{src → dist}/commands/execute/error-log.mjs +2 -3
- package/dist/commands/execute/error-log.mjs.map +1 -0
- package/dist/commands/execute/git-reconcile.d.mts +1 -0
- package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
- package/dist/commands/execute/git-reconcile.mjs.map +1 -0
- package/dist/commands/execute/handoff.d.mts +1 -0
- package/{src → dist}/commands/execute/handoff.mjs +10 -11
- package/dist/commands/execute/handoff.mjs.map +1 -0
- package/dist/commands/execute/incremental-carry.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
- package/dist/commands/execute/incremental-carry.mjs.map +1 -0
- package/dist/commands/execute/incremental-scope.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
- package/dist/commands/execute/incremental-scope.mjs.map +1 -0
- package/dist/commands/execute/integrate-stages.d.mts +1 -0
- package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
- package/dist/commands/execute/integrate-stages.mjs.map +1 -0
- package/dist/commands/execute/pane-title.d.mts +1 -0
- package/dist/commands/execute/pane-title.mjs +20 -0
- package/dist/commands/execute/pane-title.mjs.map +1 -0
- package/dist/commands/execute/plan-items.d.mts +1 -0
- package/dist/commands/execute/plan-items.mjs +9 -0
- package/dist/commands/execute/plan-items.mjs.map +1 -0
- package/dist/commands/execute/plan-validate.d.mts +1 -0
- package/dist/commands/execute/plan-validate.mjs +68 -0
- package/dist/commands/execute/plan-validate.mjs.map +1 -0
- package/dist/commands/execute/plan-verify.d.mts +1 -0
- package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
- package/dist/commands/execute/plan-verify.mjs.map +1 -0
- package/dist/commands/execute/render-bundle.d.mts +3 -0
- package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
- package/dist/commands/execute/render-bundle.mjs.map +1 -0
- package/dist/commands/execute/run.d.mts +24 -0
- package/dist/commands/execute/run.mjs +196 -0
- package/dist/commands/execute/run.mjs.map +1 -0
- package/dist/commands/execute/spawn-followups.d.mts +1 -0
- package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
- package/dist/commands/execute/spawn-followups.mjs.map +1 -0
- package/dist/commands/execute/team.d.mts +3 -0
- package/dist/commands/execute/team.mjs +56 -0
- package/dist/commands/execute/team.mjs.map +1 -0
- package/dist/commands/execute/token-usage.d.mts +1 -0
- package/{src → dist}/commands/execute/token-usage.mjs +2 -3
- package/dist/commands/execute/token-usage.mjs.map +1 -0
- package/dist/commands/execute/wizard.d.mts +8 -0
- package/dist/commands/execute/wizard.mjs +191 -0
- package/dist/commands/execute/wizard.mjs.map +1 -0
- package/dist/commands/execute/worker-audit-check.d.mts +1 -0
- package/{src → dist}/commands/execute/worker-audit-check.mjs +11 -12
- package/dist/commands/execute/worker-audit-check.mjs.map +1 -0
- package/dist/commands/execute/worker-dispatch.d.mts +7 -0
- package/{src → dist}/commands/execute/worker-dispatch.mjs +34 -46
- package/dist/commands/execute/worker-dispatch.mjs.map +1 -0
- package/dist/commands/execute/worker-state.d.mts +1 -0
- package/{src → dist}/commands/execute/worker-state.mjs +15 -16
- package/dist/commands/execute/worker-state.mjs.map +1 -0
- package/dist/commands/execute/worktree-lookup.d.mts +1 -0
- package/{src → dist}/commands/execute/worktree-lookup.mjs +38 -37
- package/dist/commands/execute/worktree-lookup.mjs.map +1 -0
- package/dist/commands/execute/worktree-status.d.mts +1 -0
- package/dist/commands/execute/worktree-status.mjs +115 -0
- package/dist/commands/execute/worktree-status.mjs.map +1 -0
- package/dist/commands/inspect/code-review.d.mts +1 -0
- package/{src → dist}/commands/inspect/code-review.mjs +16 -19
- package/dist/commands/inspect/code-review.mjs.map +1 -0
- package/dist/commands/inspect/container.d.mts +1 -0
- package/{src → dist}/commands/inspect/container.mjs +11 -13
- package/dist/commands/inspect/container.mjs.map +1 -0
- package/dist/commands/inspect/context-cost.d.mts +1 -0
- package/{src → dist}/commands/inspect/context-cost.mjs +11 -13
- package/dist/commands/inspect/context-cost.mjs.map +1 -0
- package/dist/commands/inspect/design-prep.d.mts +1 -0
- package/{src → dist}/commands/inspect/design-prep.mjs +10 -11
- package/dist/commands/inspect/design-prep.mjs.map +1 -0
- package/dist/commands/inspect/error-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/error-report.mjs +11 -13
- package/dist/commands/inspect/error-report.mjs.map +1 -0
- package/dist/commands/inspect/error-zip.d.mts +1 -0
- package/{src → dist}/commands/inspect/error-zip.mjs +11 -12
- package/dist/commands/inspect/error-zip.mjs.map +1 -0
- package/dist/commands/inspect/log-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/log-report.mjs +11 -13
- package/dist/commands/inspect/log-report.mjs.map +1 -0
- package/dist/commands/inspect/profile-show.d.mts +1 -0
- package/{src → dist}/commands/inspect/profile-show.mjs +10 -11
- package/dist/commands/inspect/profile-show.mjs.map +1 -0
- package/dist/commands/inspect/recap.d.mts +1 -0
- package/{src → dist}/commands/inspect/recap.mjs +11 -13
- package/dist/commands/inspect/recap.mjs.map +1 -0
- package/dist/commands/inspect/resolve-task-key.d.mts +1 -0
- package/{src → dist}/commands/inspect/resolve-task-key.mjs +11 -13
- package/dist/commands/inspect/resolve-task-key.mjs.map +1 -0
- package/dist/commands/inspect/rollup.d.mts +1 -0
- package/{src → dist}/commands/inspect/rollup.mjs +11 -13
- package/dist/commands/inspect/rollup.mjs.map +1 -0
- package/dist/commands/inspect/run-audit.d.mts +1 -0
- package/{src → dist}/commands/inspect/run-audit.mjs +11 -12
- package/dist/commands/inspect/run-audit.mjs.map +1 -0
- package/dist/commands/inspect/set-work-status.d.mts +1 -0
- package/{src → dist}/commands/inspect/set-work-status.mjs +11 -13
- package/dist/commands/inspect/set-work-status.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.d.mts +1 -0
- package/dist/commands/inspect/stage-map.mjs +110 -0
- package/dist/commands/inspect/stage-map.mjs.map +1 -0
- package/dist/commands/inspect/task-list.d.mts +1 -0
- package/dist/commands/inspect/task-list.mjs +103 -0
- package/dist/commands/inspect/task-list.mjs.map +1 -0
- package/dist/commands/inspect/task-show.d.mts +1 -0
- package/dist/commands/inspect/task-show.mjs +108 -0
- package/dist/commands/inspect/task-show.mjs.map +1 -0
- package/dist/commands/inspect/time-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/time-report.mjs +11 -13
- package/dist/commands/inspect/time-report.mjs.map +1 -0
- package/dist/commands/inspect/usage-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/usage-report.mjs +11 -13
- package/dist/commands/inspect/usage-report.mjs.map +1 -0
- package/dist/commands/inspect/user-response.d.mts +1 -0
- package/{src → dist}/commands/inspect/user-response.mjs +10 -11
- package/dist/commands/inspect/user-response.mjs.map +1 -0
- package/dist/commands/inspect/worker-liveness.d.mts +1 -0
- package/{src → dist}/commands/inspect/worker-liveness.mjs +11 -13
- package/dist/commands/inspect/worker-liveness.mjs.map +1 -0
- package/dist/commands/lifecycle/check-project.d.mts +21 -0
- package/dist/commands/lifecycle/check-project.mjs +175 -0
- package/dist/commands/lifecycle/check-project.mjs.map +1 -0
- package/dist/commands/lifecycle/config.d.mts +1 -0
- package/dist/commands/lifecycle/config.mjs +369 -0
- package/dist/commands/lifecycle/config.mjs.map +1 -0
- package/dist/commands/lifecycle/doctor.d.mts +10 -0
- package/dist/commands/lifecycle/doctor.mjs +317 -0
- package/dist/commands/lifecycle/doctor.mjs.map +1 -0
- package/dist/commands/lifecycle/install.d.mts +17 -0
- package/dist/commands/lifecycle/install.mjs +1077 -0
- package/dist/commands/lifecycle/install.mjs.map +1 -0
- package/dist/commands/lifecycle/migrate.d.mts +1 -0
- package/{src → dist}/commands/lifecycle/migrate.mjs +10 -11
- package/dist/commands/lifecycle/migrate.mjs.map +1 -0
- package/dist/commands/lifecycle/model.d.mts +1 -0
- package/dist/commands/lifecycle/model.mjs +22 -0
- package/dist/commands/lifecycle/model.mjs.map +1 -0
- package/dist/commands/lifecycle/paths.d.mts +1 -0
- package/dist/commands/lifecycle/paths.mjs +102 -0
- package/dist/commands/lifecycle/paths.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.d.mts +1 -0
- package/dist/commands/lifecycle/preflight.mjs +124 -0
- package/dist/commands/lifecycle/preflight.mjs.map +1 -0
- package/dist/commands/lifecycle/setup.d.mts +1 -0
- package/dist/commands/lifecycle/setup.mjs +297 -0
- package/dist/commands/lifecycle/setup.mjs.map +1 -0
- package/dist/commands/lifecycle/uninstall.d.mts +4 -0
- package/dist/commands/lifecycle/uninstall.mjs +262 -0
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -0
- package/dist/commands/manager.d.mts +3 -0
- package/{src → dist}/commands/manager.mjs +25 -32
- package/dist/commands/manager.mjs.map +1 -0
- package/dist/commands/memory/memory.d.mts +1 -0
- package/dist/commands/memory/memory.mjs +569 -0
- package/dist/commands/memory/memory.mjs.map +1 -0
- package/dist/commands/pr/pr.d.mts +33 -0
- package/dist/commands/pr/pr.mjs +296 -0
- package/dist/commands/pr/pr.mjs.map +1 -0
- package/dist/commands/report/agent-activity.d.mts +1 -0
- package/{src → dist}/commands/report/agent-activity.mjs +11 -12
- package/dist/commands/report/agent-activity.mjs.map +1 -0
- package/dist/commands/report/finalize.d.mts +4 -0
- package/dist/commands/report/finalize.mjs +64 -0
- package/dist/commands/report/finalize.mjs.map +1 -0
- package/dist/commands/report/inject-report-index.d.mts +1 -0
- package/{src → dist}/commands/report/inject-report-index.mjs +6 -7
- package/dist/commands/report/inject-report-index.mjs.map +1 -0
- package/dist/commands/report/render-final-report.d.mts +1 -0
- package/{src → dist}/commands/report/render-final-report.mjs +6 -7
- package/dist/commands/report/render-final-report.mjs.map +1 -0
- package/dist/commands/report/render-views.d.mts +1 -0
- package/{src → dist}/commands/report/render-views.mjs +6 -7
- package/dist/commands/report/render-views.mjs.map +1 -0
- package/dist/commands/report/translate.d.mts +1 -0
- package/{src → dist}/commands/report/translate.mjs +6 -7
- package/dist/commands/report/translate.mjs.map +1 -0
- package/dist/lib/helper-scripts.d.mts +3 -0
- package/{src → dist}/lib/helper-scripts.mjs +11 -14
- package/dist/lib/helper-scripts.mjs.map +1 -0
- package/dist/lib/host-registry-client.d.mts +25 -0
- package/dist/lib/host-registry-client.mjs +152 -0
- package/dist/lib/host-registry-client.mjs.map +1 -0
- package/dist/lib/install-assets.d.mts +8 -0
- package/{src → dist}/lib/install-assets.mjs +17 -20
- package/dist/lib/install-assets.mjs.map +1 -0
- package/dist/lib/okstra-dirs.d.mts +4 -0
- package/{src → dist}/lib/okstra-dirs.mjs +3 -6
- package/dist/lib/okstra-dirs.mjs.map +1 -0
- package/dist/lib/paths.d.mts +4 -0
- package/dist/lib/paths.mjs +78 -0
- package/dist/lib/paths.mjs.map +1 -0
- package/dist/lib/proc.d.mts +3 -0
- package/dist/lib/proc.mjs +31 -0
- package/dist/lib/proc.mjs.map +1 -0
- package/dist/lib/python-helper.d.mts +8 -0
- package/dist/lib/python-helper.mjs +123 -0
- package/dist/lib/python-helper.mjs.map +1 -0
- package/dist/lib/runtime-manifest.d.mts +7 -0
- package/dist/lib/runtime-manifest.mjs +73 -0
- package/dist/lib/runtime-manifest.mjs.map +1 -0
- package/dist/lib/skill-catalog.d.mts +4 -0
- package/dist/lib/skill-catalog.mjs +50 -0
- package/dist/lib/skill-catalog.mjs.map +1 -0
- package/dist/lib/types.d.mts +129 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/lib/types.mjs.map +1 -0
- package/dist/lib/version.d.mts +2 -0
- package/dist/lib/version.mjs +27 -0
- package/dist/lib/version.mjs.map +1 -0
- package/docs/architecture/storage-model.md +17 -3
- package/docs/architecture.md +12 -22
- package/docs/cli.md +29 -8
- package/docs/container.md +1 -1
- package/docs/contributor-change-matrix.md +3 -3
- package/docs/project-structure-overview.md +29 -20
- package/docs/task-process/README.md +3 -1
- package/docs/task-process/implementation-planning.md +2 -2
- package/docs/task-process/implementation.md +1 -1
- package/package.json +11 -5
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +3 -2
- package/runtime/agents/workers/report-writer-worker.md +3 -1
- package/runtime/agents/workers/translator-worker.md +3 -1
- package/runtime/bin/lib/okstra/cli.sh +12 -0
- package/runtime/bin/lib/okstra/globals.sh +4 -1
- package/runtime/bin/lib/okstra/usage.sh +4 -4
- package/runtime/bin/okstra-error-log.py +27 -0
- package/runtime/bin/okstra-provider-exec.py +245 -16
- package/runtime/bin/okstra-trace-cleanup.sh +58 -129
- package/runtime/bin/okstra.sh +10 -0
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/convergence.md +27 -18
- package/runtime/prompts/lead/okstra-lead-contract.md +4 -2
- package/runtime/prompts/lead/plan-body-verification.md +13 -6
- package/runtime/prompts/lead/report-writer.md +6 -6
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/change-impact-analysis.md +16 -0
- package/runtime/prompts/profiles/error-analysis.md +21 -1
- package/runtime/prompts/profiles/feature-analysis.md +16 -0
- package/runtime/prompts/profiles/final-verification.md +16 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +16 -0
- package/runtime/prompts/profiles/implementation-planning.md +32 -9
- package/runtime/prompts/profiles/implementation.md +17 -1
- package/runtime/prompts/profiles/improvement-discovery.md +16 -0
- package/runtime/prompts/profiles/project-analysis.md +16 -0
- package/runtime/prompts/profiles/release-handoff.md +5 -1
- package/runtime/prompts/profiles/requirements-discovery.md +20 -0
- package/runtime/prompts/wizard/prompts.ko.json +18 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +100 -25
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
- package/runtime/python/okstra_ctl/agent_activity.py +5 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
- package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
- package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
- package/runtime/python/okstra_ctl/conformance.py +68 -0
- package/runtime/python/okstra_ctl/convergence.py +409 -3
- package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
- package/runtime/python/okstra_ctl/convergence_store.py +170 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +1181 -102
- package/runtime/python/okstra_ctl/dispatch_state.py +835 -22
- package/runtime/python/okstra_ctl/doctor.py +16 -2
- package/runtime/python/okstra_ctl/domain/host.py +32 -1
- package/runtime/python/okstra_ctl/domain/provider.py +212 -6
- package/runtime/python/okstra_ctl/domain/role.py +125 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +13 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +39 -1
- package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
- package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
- package/runtime/python/okstra_ctl/error_log_write.py +9 -0
- package/runtime/python/okstra_ctl/error_report.py +21 -1
- package/runtime/python/okstra_ctl/execution_identity.py +529 -0
- package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
- package/runtime/python/okstra_ctl/index.py +6 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
- package/runtime/python/okstra_ctl/legacy_model_selection.py +490 -0
- package/runtime/python/okstra_ctl/model_cli.py +278 -0
- package/runtime/python/okstra_ctl/model_defaults.py +46 -0
- package/runtime/python/okstra_ctl/model_discovery.py +26 -7
- package/runtime/python/okstra_ctl/model_pool.py +501 -0
- package/runtime/python/okstra_ctl/models.py +47 -27
- package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
- package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
- package/runtime/python/okstra_ctl/pane_title.py +151 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
- package/runtime/python/okstra_ctl/render.py +78 -10
- package/runtime/python/okstra_ctl/render_final_report.py +34 -19
- package/runtime/python/okstra_ctl/report_contract.py +39 -1
- package/runtime/python/okstra_ctl/report_finalize.py +84 -9
- package/runtime/python/okstra_ctl/report_html/render.py +1 -0
- package/runtime/python/okstra_ctl/role_requirements.py +217 -0
- package/runtime/python/okstra_ctl/run.py +1250 -260
- package/runtime/python/okstra_ctl/run_index_row.py +7 -1
- package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
- package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
- package/runtime/python/okstra_ctl/session.py +69 -12
- package/runtime/python/okstra_ctl/team.py +51 -25
- package/runtime/python/okstra_ctl/time_report.py +38 -4
- package/runtime/python/okstra_ctl/tmux.py +19 -149
- package/runtime/python/okstra_ctl/usage_identity.py +86 -0
- package/runtime/python/okstra_ctl/usage_report.py +15 -3
- package/runtime/python/okstra_ctl/wizard.py +1253 -62
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
- package/runtime/python/okstra_ctl/worker_request.py +9 -1
- package/runtime/python/okstra_ctl/worker_runner.py +130 -5
- package/runtime/python/okstra_ctl/workers.py +7 -5
- package/runtime/python/okstra_ctl/worktree.py +69 -3
- package/runtime/python/okstra_ctl/write_policy.py +571 -0
- package/runtime/python/okstra_token_usage/cli.py +1 -1
- package/runtime/python/okstra_token_usage/collect.py +66 -6
- package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
- package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
- package/runtime/schemas/final-report-v2.0.schema.json +25 -0
- package/runtime/skills/okstra-run/SKILL.md +9 -9
- package/runtime/skills/okstra-setup/references/project-config.md +11 -0
- package/runtime/templates/report-writer-prompt-preamble.md +2 -0
- package/runtime/templates/reports/final-report-v2.template.md +7 -1
- package/runtime/templates/reports/html/base.template.html +21 -0
- package/runtime/templates/reports/html/i18n/en.json +4 -0
- package/runtime/templates/reports/html/i18n/ko.json +4 -0
- package/runtime/templates/reports/settings.template.json +0 -24
- package/runtime/templates/worker-prompt-preamble.md +6 -2
- package/runtime/validators/lib/fixtures.sh +98 -33
- package/runtime/validators/validate-implementation-plan-stages.py +63 -3
- package/runtime/validators/validate-run.py +443 -508
- package/runtime/validators/validate_session_conformance.py +26 -8
- package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
- package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
- package/runtime/templates/reports/final-report.template.md +0 -1258
- package/src/cli-registry.mjs +0 -532
- package/src/commands/execute/codex-dispatch.mjs +0 -10
- package/src/commands/execute/plan-items.mjs +0 -9
- package/src/commands/execute/plan-validate.mjs +0 -68
- package/src/commands/execute/run.mjs +0 -208
- package/src/commands/execute/team.mjs +0 -63
- package/src/commands/execute/wizard.mjs +0 -167
- package/src/commands/inspect/stage-map.mjs +0 -98
- package/src/commands/inspect/task-list.mjs +0 -97
- package/src/commands/inspect/task-show.mjs +0 -100
- package/src/commands/lifecycle/check-project.mjs +0 -187
- package/src/commands/lifecycle/config.mjs +0 -385
- package/src/commands/lifecycle/doctor.mjs +0 -308
- package/src/commands/lifecycle/install.mjs +0 -1172
- package/src/commands/lifecycle/paths.mjs +0 -104
- package/src/commands/lifecycle/preflight.mjs +0 -125
- package/src/commands/lifecycle/setup.mjs +0 -333
- package/src/commands/lifecycle/uninstall.mjs +0 -279
- package/src/commands/memory/memory.mjs +0 -524
- package/src/commands/pr/pr.mjs +0 -261
- package/src/commands/report/finalize.mjs +0 -64
- package/src/lib/host-registry-client.mjs +0 -176
- package/src/lib/paths.mjs +0 -86
- package/src/lib/proc.mjs +0 -31
- package/src/lib/python-helper.mjs +0 -142
- package/src/lib/runtime-manifest.mjs +0 -70
- package/src/lib/skill-catalog.mjs +0 -53
- package/src/lib/version.mjs +0 -20
- /package/{src → dist}/commands/pr/default.md +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Worker dispatch plumbing shared by both lead dispatchers.
|
|
2
2
|
|
|
3
3
|
`dispatch_core` (claude / external lead) and `codex_dispatch` (codex lead) differ
|
|
4
|
-
in capability — only the former has
|
|
5
|
-
persisted record. What they do *not* differ in is the job value object and the
|
|
4
|
+
in capability — only the former has cmux surfaces, blocking waits, and retry from
|
|
5
|
+
a persisted record. What they do *not* differ in is the job value object and the
|
|
6
6
|
run's state files: the same run-manifest keys, the same team-state document, the
|
|
7
7
|
same wrapper argv.
|
|
8
8
|
|
|
@@ -22,6 +22,7 @@ import fcntl
|
|
|
22
22
|
import json
|
|
23
23
|
import os
|
|
24
24
|
import tempfile
|
|
25
|
+
import uuid
|
|
25
26
|
from dataclasses import dataclass
|
|
26
27
|
from datetime import datetime, timezone
|
|
27
28
|
from pathlib import Path
|
|
@@ -31,8 +32,18 @@ from . import cmux
|
|
|
31
32
|
from .agent_invocation import (
|
|
32
33
|
AgentInvocationError,
|
|
33
34
|
agent_model_assignment_from_payload,
|
|
35
|
+
invocation_metadata_identity,
|
|
36
|
+
v2_role_assignment_authority_errors,
|
|
34
37
|
verify_agent_invocation,
|
|
35
38
|
)
|
|
39
|
+
from .domain.role import RoleCatalogError, role_for_duty
|
|
40
|
+
from .execution_identity import Attempt, Invocation
|
|
41
|
+
from .execution_manifest import (
|
|
42
|
+
finish_attempt_mutation,
|
|
43
|
+
read_execution_manifest,
|
|
44
|
+
record_invocation_attempt,
|
|
45
|
+
)
|
|
46
|
+
from .execution_mutation_audit import ExecutionMutationAudit, MutationSnapshot
|
|
36
47
|
from .final_report_paths import (
|
|
37
48
|
final_report_data_path,
|
|
38
49
|
final_report_markdown_path,
|
|
@@ -44,25 +55,33 @@ from .worker_prompt_contract import (
|
|
|
44
55
|
validate_reverify_prompt,
|
|
45
56
|
)
|
|
46
57
|
from .worker_runner import LIVE, QUIET
|
|
58
|
+
from .worker_request import verifier_extra_dirs
|
|
59
|
+
from .worker_artifact_paths import audit_sidecar_rel
|
|
60
|
+
from .wrapper_status import status_path_for_prompt
|
|
61
|
+
from .write_policy import (
|
|
62
|
+
build_invocation_write_contract,
|
|
63
|
+
planned_paths_from_run_manifest,
|
|
64
|
+
write_policy_from_payload,
|
|
65
|
+
)
|
|
66
|
+
from .convergence_store import write_json_atomic
|
|
47
67
|
|
|
48
68
|
BACKEND_CLI_WRAPPER = "cli-wrapper"
|
|
49
|
-
BACKEND_TMUX_PANE = "tmux-pane"
|
|
50
69
|
BACKEND_CMUX_PANE = "cmux-pane"
|
|
51
70
|
BACKEND_MIXED = "mixed"
|
|
52
71
|
|
|
53
72
|
|
|
54
73
|
def detect_terminal_backend() -> str:
|
|
55
|
-
"""Which
|
|
74
|
+
"""Which backend this run's workers get. Called once, by prepare.
|
|
56
75
|
|
|
57
|
-
cmux
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
cmux is the only backend that gives a worker a surface of its own. Without
|
|
77
|
+
it a worker runs as a cli-wrapper subprocess, which owns no pane and is read
|
|
78
|
+
through its status sidecar instead. The answer is written to the run manifest
|
|
79
|
+
and read back from there — consumers must not re-detect, or two phases of one
|
|
80
|
+
run can disagree.
|
|
62
81
|
"""
|
|
63
82
|
if cmux.cmux_available():
|
|
64
83
|
return BACKEND_CMUX_PANE
|
|
65
|
-
return
|
|
84
|
+
return BACKEND_CLI_WRAPPER
|
|
66
85
|
|
|
67
86
|
# `livenessMode` picks which artifact answers "is this worker still alive": the
|
|
68
87
|
# in-process worker's audit sidecar heartbeat, or the CLI wrapper's status
|
|
@@ -88,10 +107,26 @@ WORKER_STATUSES = frozenset(
|
|
|
88
107
|
REASON_REQUIRED_STATUSES = frozenset({"timeout", "error", "not-run"})
|
|
89
108
|
|
|
90
109
|
|
|
110
|
+
def generate_claude_session_id() -> str:
|
|
111
|
+
"""A UUIDv4 for `claude --session-id`.
|
|
112
|
+
|
|
113
|
+
The lead mints one for its own session (`run`) and dispatch mints one per
|
|
114
|
+
worker (`dispatch_session_id`); they are the same kind of value, so one
|
|
115
|
+
generator serves both. It sits here rather than in `session` because
|
|
116
|
+
`session` imports this module, so a worker-side call the other way would
|
|
117
|
+
close an import cycle.
|
|
118
|
+
"""
|
|
119
|
+
return str(uuid.uuid4())
|
|
120
|
+
|
|
121
|
+
|
|
91
122
|
class DispatchError(Exception):
|
|
92
123
|
"""Raised when a worker dispatch request cannot be executed."""
|
|
93
124
|
|
|
94
125
|
|
|
126
|
+
class IncompletePromptWriteArtifacts(DispatchError):
|
|
127
|
+
"""Raised when a worker prompt omits the standard artifact headers."""
|
|
128
|
+
|
|
129
|
+
|
|
95
130
|
@dataclass(frozen=True)
|
|
96
131
|
class WorkerJob:
|
|
97
132
|
worker_id: str
|
|
@@ -119,11 +154,23 @@ class WorkerJob:
|
|
|
119
154
|
prompt_digest: str = ""
|
|
120
155
|
host_model_value: str | None = None
|
|
121
156
|
enforcement_mode: str = ""
|
|
157
|
+
# The session id dispatch mints and carries through to the provider CLI, so
|
|
158
|
+
# token collection can find the worker's jsonl by this value rather than by
|
|
159
|
+
# guessing at `agentName`. A worker started in a cmux pane is a separate CLI
|
|
160
|
+
# process and leaves no `agentName` behind, so without this its usage cannot
|
|
161
|
+
# be attributed at all.
|
|
162
|
+
session_id: str = ""
|
|
163
|
+
participant_ref: str = ""
|
|
164
|
+
role_execution_ref: str = ""
|
|
165
|
+
execution_label: str = ""
|
|
166
|
+
duty_id: str = ""
|
|
167
|
+
invocation_ref: str = ""
|
|
168
|
+
attempt: int = 0
|
|
122
169
|
|
|
123
170
|
@property
|
|
124
171
|
def command(self) -> list[str]:
|
|
125
172
|
# The flag trails the positional contract the entrypoint reads by index.
|
|
126
|
-
|
|
173
|
+
argv = [
|
|
127
174
|
str(self.wrapper_path),
|
|
128
175
|
str(self.project_root),
|
|
129
176
|
self.model_execution_value,
|
|
@@ -134,6 +181,14 @@ class WorkerJob:
|
|
|
134
181
|
"--presentation",
|
|
135
182
|
self._presentation(),
|
|
136
183
|
]
|
|
184
|
+
if self.has_execution_identity:
|
|
185
|
+
argv.extend([
|
|
186
|
+
"--invocation-metadata",
|
|
187
|
+
str(self.prompt_metadata_path),
|
|
188
|
+
])
|
|
189
|
+
if self.session_id:
|
|
190
|
+
argv += ["--session-id", self.session_id]
|
|
191
|
+
return argv
|
|
137
192
|
|
|
138
193
|
def _presentation(self) -> str:
|
|
139
194
|
# A pane is a screen a person watches; a cli-wrapper dispatch's stdout is
|
|
@@ -143,7 +198,6 @@ class WorkerJob:
|
|
|
143
198
|
|
|
144
199
|
def to_payload(self) -> dict[str, Any]:
|
|
145
200
|
payload = {
|
|
146
|
-
"workerId": self.worker_id,
|
|
147
201
|
"provider": self.provider,
|
|
148
202
|
"backend": self.backend,
|
|
149
203
|
"modelExecutionValue": self.model_execution_value,
|
|
@@ -157,6 +211,19 @@ class WorkerJob:
|
|
|
157
211
|
"dispatchKind": self.dispatch_kind,
|
|
158
212
|
"command": self.command,
|
|
159
213
|
}
|
|
214
|
+
if self.has_execution_identity:
|
|
215
|
+
payload.update({
|
|
216
|
+
"schemaVersion": "2.0",
|
|
217
|
+
"executionIdentityVersion": 2,
|
|
218
|
+
"participantRef": self.participant_ref,
|
|
219
|
+
"roleExecutionRef": self.role_execution_ref,
|
|
220
|
+
"executionLabel": self.execution_label,
|
|
221
|
+
"dutyId": self.duty_id,
|
|
222
|
+
"invocationRef": self.invocation_ref,
|
|
223
|
+
"attempt": self.attempt,
|
|
224
|
+
})
|
|
225
|
+
else:
|
|
226
|
+
payload.update({"schemaVersion": 1, "workerId": self.worker_id})
|
|
160
227
|
if self.invocation_id:
|
|
161
228
|
payload.update({
|
|
162
229
|
"invocationId": self.invocation_id,
|
|
@@ -169,6 +236,22 @@ class WorkerJob:
|
|
|
169
236
|
})
|
|
170
237
|
return payload
|
|
171
238
|
|
|
239
|
+
@property
|
|
240
|
+
def has_execution_identity(self) -> bool:
|
|
241
|
+
values = (
|
|
242
|
+
self.participant_ref,
|
|
243
|
+
self.role_execution_ref,
|
|
244
|
+
self.execution_label,
|
|
245
|
+
self.duty_id,
|
|
246
|
+
self.invocation_ref,
|
|
247
|
+
)
|
|
248
|
+
present = tuple(bool(value) for value in values)
|
|
249
|
+
if any(present) and not all(present):
|
|
250
|
+
raise DispatchError("worker job has partial v2 execution identity")
|
|
251
|
+
if all(present) and self.attempt < 1:
|
|
252
|
+
raise DispatchError("worker job v2 attempt must be positive")
|
|
253
|
+
return all(present)
|
|
254
|
+
|
|
172
255
|
@property
|
|
173
256
|
def digests(self) -> dict[str, str]:
|
|
174
257
|
return {
|
|
@@ -298,7 +381,23 @@ def append_worker_dispatch(
|
|
|
298
381
|
team_state_path: Path,
|
|
299
382
|
record: Mapping[str, Any],
|
|
300
383
|
) -> None:
|
|
301
|
-
"""
|
|
384
|
+
"""Record one worker dispatch without losing another state writer's update.
|
|
385
|
+
|
|
386
|
+
A row carrying a dispatch id replaces the row that already holds it rather
|
|
387
|
+
than stacking beside it: an observed lead re-sent `okstra team dispatch` for
|
|
388
|
+
a prompt and attempt it had already sent, team-state kept both copies, and
|
|
389
|
+
`validate-run` failed that run with `agent dispatch ID is duplicated`.
|
|
390
|
+
|
|
391
|
+
The replaced row takes its `paneId` with it, and recorded ids are the only
|
|
392
|
+
reclaim candidates `team._reclaimable_panes` has, so that pane is left on
|
|
393
|
+
screen for the session. That is the price of the trade: before this, the
|
|
394
|
+
same re-send cost the whole run its report.
|
|
395
|
+
|
|
396
|
+
A job with no invocation carries no dispatch id, so it has no key to
|
|
397
|
+
collapse on and folding those rows would merge two unrelated dispatches into
|
|
398
|
+
one. They still stack, and `update_worker_dispatch_status` settles the
|
|
399
|
+
newest of them.
|
|
400
|
+
"""
|
|
302
401
|
with _team_state_lock(team_state_path):
|
|
303
402
|
payload = load_json_object(team_state_path, "team-state")
|
|
304
403
|
dispatches = payload.setdefault("workerDispatches", [])
|
|
@@ -306,7 +405,20 @@ def append_worker_dispatch(
|
|
|
306
405
|
raise DispatchError(
|
|
307
406
|
f"team-state workerDispatches must be an array: {team_state_path}"
|
|
308
407
|
)
|
|
309
|
-
|
|
408
|
+
new_record = dict(record)
|
|
409
|
+
new_id = str(new_record.get("dispatchId") or "")
|
|
410
|
+
replaced = False
|
|
411
|
+
if new_id:
|
|
412
|
+
for index, existing in enumerate(dispatches):
|
|
413
|
+
if (
|
|
414
|
+
isinstance(existing, Mapping)
|
|
415
|
+
and existing.get("dispatchId") == new_id
|
|
416
|
+
):
|
|
417
|
+
dispatches[index] = new_record
|
|
418
|
+
replaced = True
|
|
419
|
+
break
|
|
420
|
+
if not replaced:
|
|
421
|
+
dispatches.append(new_record)
|
|
310
422
|
write_json(team_state_path, payload)
|
|
311
423
|
|
|
312
424
|
|
|
@@ -388,9 +500,22 @@ def record_verified_agent_dispatch(
|
|
|
388
500
|
assignment = agent_model_assignment_from_payload(assignments.get(assignment_ref))
|
|
389
501
|
except AgentInvocationError as exc:
|
|
390
502
|
raise DispatchError(str(exc)) from exc
|
|
391
|
-
|
|
503
|
+
try:
|
|
504
|
+
execution_identity = invocation_metadata_identity(metadata)
|
|
505
|
+
except AgentInvocationError as exc:
|
|
506
|
+
raise DispatchError(str(exc)) from exc
|
|
507
|
+
worker_id = (
|
|
508
|
+
require_string(metadata, "workerId")
|
|
509
|
+
if execution_identity is None
|
|
510
|
+
else None
|
|
511
|
+
)
|
|
392
512
|
audience = require_string(metadata, "audience")
|
|
393
513
|
invocation_id = require_string(metadata, "invocationId")
|
|
514
|
+
if (
|
|
515
|
+
execution_identity is not None
|
|
516
|
+
and execution_identity.invocation_ref != invocation_id
|
|
517
|
+
):
|
|
518
|
+
raise DispatchError("v2 invocation reference does not match invocation ID")
|
|
394
519
|
errors = verify_agent_invocation(
|
|
395
520
|
metadata_path,
|
|
396
521
|
project_root=project_root,
|
|
@@ -400,6 +525,15 @@ def record_verified_agent_dispatch(
|
|
|
400
525
|
expected_worker_id=worker_id,
|
|
401
526
|
expected_assignment_ref=assignment_ref,
|
|
402
527
|
expected_audience=audience,
|
|
528
|
+
expected_participant_ref=(
|
|
529
|
+
execution_identity.participant_ref if execution_identity else None
|
|
530
|
+
),
|
|
531
|
+
expected_role_execution_ref=(
|
|
532
|
+
execution_identity.role_execution_ref if execution_identity else None
|
|
533
|
+
),
|
|
534
|
+
expected_invocation_ref=(
|
|
535
|
+
execution_identity.invocation_ref if execution_identity else None
|
|
536
|
+
),
|
|
403
537
|
)
|
|
404
538
|
if errors:
|
|
405
539
|
raise DispatchError("agent invocation verification failed: " + "; ".join(errors))
|
|
@@ -428,10 +562,15 @@ def record_verified_agent_dispatch(
|
|
|
428
562
|
if not isinstance(prompt, Mapping):
|
|
429
563
|
raise DispatchError("agent invocation prompt is missing")
|
|
430
564
|
prompt_path = require_string(prompt, "path")
|
|
431
|
-
|
|
565
|
+
# Identity-bearing records carry the stored attempt. A path without
|
|
566
|
+
# identity still records the first attempt; the conflict guard below
|
|
567
|
+
# refuses a second write under that id.
|
|
568
|
+
dispatch_id = build_dispatch_id(
|
|
569
|
+
invocation_id,
|
|
570
|
+
execution_identity.attempt if execution_identity else 1,
|
|
571
|
+
)
|
|
432
572
|
record = {
|
|
433
573
|
"dispatchId": dispatch_id,
|
|
434
|
-
"workerId": worker_id,
|
|
435
574
|
"audience": audience,
|
|
436
575
|
"invocationId": invocation_id,
|
|
437
576
|
"assignmentRef": assignment_ref,
|
|
@@ -444,7 +583,99 @@ def record_verified_agent_dispatch(
|
|
|
444
583
|
"promptDeliveryVerified": False,
|
|
445
584
|
"status": "dispatched",
|
|
446
585
|
}
|
|
586
|
+
if execution_identity is None:
|
|
587
|
+
record["workerId"] = worker_id
|
|
588
|
+
else:
|
|
589
|
+
record.update({
|
|
590
|
+
"schemaVersion": "2.0",
|
|
591
|
+
"executionIdentityVersion": 2,
|
|
592
|
+
"invocationRef": execution_identity.invocation_ref,
|
|
593
|
+
"participantRef": execution_identity.participant_ref,
|
|
594
|
+
"roleExecutionRef": execution_identity.role_execution_ref,
|
|
595
|
+
"executionLabel": execution_identity.execution_label,
|
|
596
|
+
"dutyId": execution_identity.duty_id,
|
|
597
|
+
"attempt": execution_identity.attempt,
|
|
598
|
+
"dispatchKind": require_string(metadata, "dispatchKind"),
|
|
599
|
+
})
|
|
447
600
|
team_state_path = resolve_required_path(project_root, manifest, "teamStatePath")
|
|
601
|
+
write_policy = None
|
|
602
|
+
write_enforcement = None
|
|
603
|
+
mutation_snapshot_path = metadata_path.with_suffix(
|
|
604
|
+
metadata_path.suffix + ".mutation-audit.json"
|
|
605
|
+
)
|
|
606
|
+
if execution_identity is not None:
|
|
607
|
+
write_policy, write_enforcement = _agent_write_contract(
|
|
608
|
+
project_root,
|
|
609
|
+
run_manifest_path,
|
|
610
|
+
manifest,
|
|
611
|
+
execution_identity.role_execution_ref,
|
|
612
|
+
resolve_project_path(project_root, prompt_path),
|
|
613
|
+
assignment_ref,
|
|
614
|
+
)
|
|
615
|
+
record.update({
|
|
616
|
+
"writePolicyDigest": write_policy.digest,
|
|
617
|
+
"writeEnforcement": write_enforcement.to_payload(),
|
|
618
|
+
"mutationAuditSnapshotPath": (
|
|
619
|
+
_relative_project_path(project_root, mutation_snapshot_path)
|
|
620
|
+
if write_enforcement.mutation_audit == "batch"
|
|
621
|
+
else ""
|
|
622
|
+
),
|
|
623
|
+
})
|
|
624
|
+
existing_state = load_json_object(team_state_path, "team-state")
|
|
625
|
+
existing = [
|
|
626
|
+
item
|
|
627
|
+
for item in existing_state.get("agentDispatches") or []
|
|
628
|
+
if isinstance(item, Mapping) and item.get("dispatchId") == dispatch_id
|
|
629
|
+
]
|
|
630
|
+
if existing:
|
|
631
|
+
if len(existing) != 1 or dict(existing[0]) != record:
|
|
632
|
+
raise DispatchError(f"agent dispatch ID conflicts: {dispatch_id}")
|
|
633
|
+
return record
|
|
634
|
+
if execution_identity is not None:
|
|
635
|
+
dispatch_kind = require_string(metadata, "dispatchKind")
|
|
636
|
+
prompt_digest = require_string(digests, "promptDigest")
|
|
637
|
+
if write_policy is None or write_enforcement is None:
|
|
638
|
+
raise DispatchError("agent write contract was not prepared")
|
|
639
|
+
record_invocation_attempt(
|
|
640
|
+
run_manifest_path,
|
|
641
|
+
Invocation(
|
|
642
|
+
invocation_ref=execution_identity.invocation_ref,
|
|
643
|
+
participant_ref=execution_identity.participant_ref,
|
|
644
|
+
role_execution_ref=execution_identity.role_execution_ref,
|
|
645
|
+
source_invocation_ref=None,
|
|
646
|
+
recovery_ref=None,
|
|
647
|
+
duty_id=execution_identity.duty_id,
|
|
648
|
+
dispatch_kind=dispatch_kind,
|
|
649
|
+
round=_execution_dispatch_round(dispatch_kind),
|
|
650
|
+
input_digest=prompt_digest,
|
|
651
|
+
write_policy=write_policy.to_payload(),
|
|
652
|
+
write_policy_digest=write_policy.digest,
|
|
653
|
+
write_enforcement=write_enforcement.to_payload(),
|
|
654
|
+
),
|
|
655
|
+
Attempt(
|
|
656
|
+
invocation_ref=execution_identity.invocation_ref,
|
|
657
|
+
attempt=execution_identity.attempt,
|
|
658
|
+
started_at=utc_now(),
|
|
659
|
+
finished_at=None,
|
|
660
|
+
status="started",
|
|
661
|
+
result_path=None,
|
|
662
|
+
error_path=None,
|
|
663
|
+
change_summary={},
|
|
664
|
+
evidence_seal_ref=None,
|
|
665
|
+
),
|
|
666
|
+
task_key=require_string(manifest, "taskKey"),
|
|
667
|
+
)
|
|
668
|
+
if write_enforcement.mutation_audit == "batch":
|
|
669
|
+
snapshot = ExecutionMutationAudit().snapshot(
|
|
670
|
+
(write_policy,),
|
|
671
|
+
orchestrator_paths=(
|
|
672
|
+
run_manifest_path,
|
|
673
|
+
team_state_path,
|
|
674
|
+
Path(f"{team_state_path}.lock"),
|
|
675
|
+
mutation_snapshot_path,
|
|
676
|
+
),
|
|
677
|
+
)
|
|
678
|
+
write_json_atomic(mutation_snapshot_path, snapshot.to_payload())
|
|
448
679
|
with _team_state_lock(team_state_path):
|
|
449
680
|
team_state = load_json_object(team_state_path, "team-state")
|
|
450
681
|
dispatches = team_state.setdefault("agentDispatches", [])
|
|
@@ -463,13 +694,191 @@ def record_verified_agent_dispatch(
|
|
|
463
694
|
return record
|
|
464
695
|
|
|
465
696
|
|
|
697
|
+
def _agent_write_contract(
|
|
698
|
+
project_root: Path,
|
|
699
|
+
manifest_path: Path,
|
|
700
|
+
authority: Mapping[str, Any],
|
|
701
|
+
role_execution_ref: str,
|
|
702
|
+
prompt_path: Path,
|
|
703
|
+
assignment_ref: str,
|
|
704
|
+
):
|
|
705
|
+
execution_manifest = read_execution_manifest(manifest_path)
|
|
706
|
+
execution = next(
|
|
707
|
+
(
|
|
708
|
+
row for row in execution_manifest.role_executions
|
|
709
|
+
if row.role_execution_ref == role_execution_ref
|
|
710
|
+
),
|
|
711
|
+
None,
|
|
712
|
+
)
|
|
713
|
+
capability = (
|
|
714
|
+
execution.binding.worker_write_capability
|
|
715
|
+
if execution is not None and execution.binding is not None
|
|
716
|
+
else None
|
|
717
|
+
)
|
|
718
|
+
if execution is None:
|
|
719
|
+
raise DispatchError("agent write policy has no role execution")
|
|
720
|
+
if execution.role == "leader":
|
|
721
|
+
artifacts = _leader_artifact_paths(project_root, authority)
|
|
722
|
+
worktree = None
|
|
723
|
+
maximum_precision = "none"
|
|
724
|
+
elif capability is not None:
|
|
725
|
+
try:
|
|
726
|
+
artifacts, worktree = _prompt_write_paths(project_root, prompt_path)
|
|
727
|
+
except IncompletePromptWriteArtifacts:
|
|
728
|
+
artifacts, worktree = _manifest_worker_write_paths(
|
|
729
|
+
project_root, authority, assignment_ref, prompt_path
|
|
730
|
+
)
|
|
731
|
+
maximum_precision = capability.max_boundary_precision
|
|
732
|
+
else:
|
|
733
|
+
raise DispatchError("agent write policy has no runner capability")
|
|
734
|
+
try:
|
|
735
|
+
return build_invocation_write_contract(
|
|
736
|
+
role=execution.role,
|
|
737
|
+
project_root=project_root,
|
|
738
|
+
worktree=worktree,
|
|
739
|
+
artifact_paths=artifacts,
|
|
740
|
+
maximum_precision=maximum_precision,
|
|
741
|
+
planned_paths=(
|
|
742
|
+
planned_paths_from_run_manifest(project_root, authority)
|
|
743
|
+
if execution.role == "implementer"
|
|
744
|
+
else ()
|
|
745
|
+
),
|
|
746
|
+
auxiliary_roots=verifier_extra_dirs(execution.role),
|
|
747
|
+
validated_auxiliary_roots=verifier_extra_dirs(execution.role),
|
|
748
|
+
)
|
|
749
|
+
except (OSError, ValueError) as exc:
|
|
750
|
+
raise DispatchError(f"agent write policy is invalid: {exc}") from exc
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _leader_artifact_paths(
|
|
754
|
+
project_root: Path, authority: Mapping[str, Any]
|
|
755
|
+
) -> tuple[Path, ...]:
|
|
756
|
+
value = authority.get("taskRootPath") or authority.get("runDirectoryPath")
|
|
757
|
+
if not isinstance(value, str) or not value.strip():
|
|
758
|
+
raise DispatchError("leader write policy has no artifact root")
|
|
759
|
+
artifact_home = project_root / ".okstra"
|
|
760
|
+
if not artifact_home.is_dir():
|
|
761
|
+
raise DispatchError("leader write policy artifact home does not exist")
|
|
762
|
+
return (artifact_home,)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _manifest_worker_write_paths(
|
|
766
|
+
project_root: Path,
|
|
767
|
+
authority: Mapping[str, Any],
|
|
768
|
+
assignment_ref: str,
|
|
769
|
+
prompt_path: Path,
|
|
770
|
+
) -> tuple[tuple[Path, ...], Path | None]:
|
|
771
|
+
prefix = "initial/"
|
|
772
|
+
if not assignment_ref.startswith(prefix):
|
|
773
|
+
raise DispatchError("agent prompt write artifact headers are incomplete")
|
|
774
|
+
worker_id = assignment_ref.removeprefix(prefix)
|
|
775
|
+
team_state_path = resolve_required_path(
|
|
776
|
+
project_root, authority, "teamStatePath"
|
|
777
|
+
)
|
|
778
|
+
team_state = load_json_object(team_state_path, "team-state")
|
|
779
|
+
matches = [
|
|
780
|
+
row for row in team_state.get("workers") or []
|
|
781
|
+
if isinstance(row, Mapping) and row.get("workerId") == worker_id
|
|
782
|
+
]
|
|
783
|
+
if len(matches) != 1:
|
|
784
|
+
raise DispatchError("agent prompt has no unique worker artifact authority")
|
|
785
|
+
result = resolve_project_path(
|
|
786
|
+
project_root, require_string(matches[0], "resultPath")
|
|
787
|
+
)
|
|
788
|
+
paths = {
|
|
789
|
+
*dispatch_completion_paths(worker_id, result, authority, project_root),
|
|
790
|
+
result,
|
|
791
|
+
Path(audit_sidecar_rel(str(result))),
|
|
792
|
+
status_path_for_prompt(prompt_path),
|
|
793
|
+
prompt_path.with_suffix(prompt_path.suffix + ".log"),
|
|
794
|
+
}
|
|
795
|
+
active_value = authority.get("activeRunContextPath")
|
|
796
|
+
active = (
|
|
797
|
+
load_json_object(
|
|
798
|
+
resolve_project_path(project_root, active_value),
|
|
799
|
+
"active run context",
|
|
800
|
+
)
|
|
801
|
+
if isinstance(active_value, str) and active_value
|
|
802
|
+
else {}
|
|
803
|
+
)
|
|
804
|
+
error_logs = active.get("errorLogs")
|
|
805
|
+
sidecars = (
|
|
806
|
+
error_logs.get("sidecarsByWorkerId")
|
|
807
|
+
if isinstance(error_logs, Mapping)
|
|
808
|
+
else None
|
|
809
|
+
)
|
|
810
|
+
error_value = sidecars.get(worker_id) if isinstance(sidecars, Mapping) else None
|
|
811
|
+
if isinstance(error_value, str) and error_value:
|
|
812
|
+
paths.add(resolve_project_path(project_root, error_value))
|
|
813
|
+
worktree_value = worktree_path(authority, active)
|
|
814
|
+
worktree = Path(worktree_value) if worktree_value else None
|
|
815
|
+
return tuple(sorted(paths, key=str)), worktree
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _prompt_write_paths(
|
|
819
|
+
project_root: Path, prompt_path: Path
|
|
820
|
+
) -> tuple[tuple[Path, ...], Path | None]:
|
|
821
|
+
try:
|
|
822
|
+
lines = prompt_path.read_text(encoding="utf-8").splitlines()
|
|
823
|
+
except (OSError, UnicodeError) as exc:
|
|
824
|
+
raise DispatchError("agent prompt cannot be read for write policy") from exc
|
|
825
|
+
values: dict[str, str] = {}
|
|
826
|
+
labels = {
|
|
827
|
+
"Result Path",
|
|
828
|
+
"Worker Result Path",
|
|
829
|
+
"Audit sidecar path",
|
|
830
|
+
"Errors sidecar path",
|
|
831
|
+
"Worktree",
|
|
832
|
+
}
|
|
833
|
+
for line in lines:
|
|
834
|
+
for label in labels:
|
|
835
|
+
prefix = f"**{label}:** "
|
|
836
|
+
if line.startswith(prefix):
|
|
837
|
+
values[label] = line.removeprefix(prefix).strip()
|
|
838
|
+
required = {"Result Path", "Audit sidecar path", "Errors sidecar path"}
|
|
839
|
+
if not required <= values.keys():
|
|
840
|
+
raise IncompletePromptWriteArtifacts(
|
|
841
|
+
"agent prompt write artifact headers are incomplete"
|
|
842
|
+
)
|
|
843
|
+
artifact_values = [values[label] for label in sorted(required)]
|
|
844
|
+
if "Worker Result Path" in values:
|
|
845
|
+
artifact_values.append(values["Worker Result Path"])
|
|
846
|
+
artifacts = {
|
|
847
|
+
_project_or_absolute(project_root, value) for value in artifact_values
|
|
848
|
+
}
|
|
849
|
+
artifacts.add(status_path_for_prompt(prompt_path))
|
|
850
|
+
artifacts.add(prompt_path.with_suffix(prompt_path.suffix + ".log"))
|
|
851
|
+
worktree_value = values.get("Worktree")
|
|
852
|
+
worktree = (
|
|
853
|
+
_project_or_absolute(project_root, worktree_value)
|
|
854
|
+
if worktree_value
|
|
855
|
+
else None
|
|
856
|
+
)
|
|
857
|
+
return tuple(sorted(artifacts, key=str)), worktree
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def _project_or_absolute(project_root: Path, value: str) -> Path:
|
|
861
|
+
path = Path(value)
|
|
862
|
+
return path if path.is_absolute() else project_root / path
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _execution_dispatch_round(dispatch_kind: str) -> int:
|
|
866
|
+
prefix = "reverify-r"
|
|
867
|
+
if not dispatch_kind.startswith(prefix):
|
|
868
|
+
return 1
|
|
869
|
+
suffix = dispatch_kind[len(prefix):]
|
|
870
|
+
if not suffix.isdigit() or int(suffix) < 1:
|
|
871
|
+
raise DispatchError(f"invalid reverify dispatch kind: {dispatch_kind}")
|
|
872
|
+
return int(suffix)
|
|
873
|
+
|
|
874
|
+
|
|
466
875
|
def link_agent_dispatch_result(
|
|
467
876
|
*,
|
|
468
877
|
project_root: Path,
|
|
469
878
|
run_manifest_path: Path,
|
|
470
879
|
dispatch_id: str,
|
|
471
880
|
result_path: Path,
|
|
472
|
-
) -> dict[str,
|
|
881
|
+
) -> dict[str, Any]:
|
|
473
882
|
"""Link an existing result artifact to exactly one verified dispatch."""
|
|
474
883
|
project_root = project_root.resolve()
|
|
475
884
|
manifest_path = resolve_project_path(
|
|
@@ -486,7 +895,20 @@ def link_agent_dispatch_result(
|
|
|
486
895
|
raise DispatchError("agent result link must stay inside the current run")
|
|
487
896
|
result_relative = _relative_project_path(project_root, resolved_result)
|
|
488
897
|
team_state_path = resolve_required_path(project_root, manifest, "teamStatePath")
|
|
489
|
-
|
|
898
|
+
preview = load_json_object(team_state_path, "team-state")
|
|
899
|
+
native_dispatches = [
|
|
900
|
+
item for item in preview.get("agentDispatches") or []
|
|
901
|
+
if isinstance(item, Mapping) and item.get("dispatchId") == dispatch_id
|
|
902
|
+
]
|
|
903
|
+
if len(native_dispatches) == 1:
|
|
904
|
+
_finish_native_agent_attempt(
|
|
905
|
+
project_root=project_root,
|
|
906
|
+
manifest_path=manifest_path,
|
|
907
|
+
manifest=manifest,
|
|
908
|
+
dispatch=native_dispatches[0],
|
|
909
|
+
result_path=resolved_result,
|
|
910
|
+
result_relative=result_relative,
|
|
911
|
+
)
|
|
490
912
|
with _team_state_lock(team_state_path):
|
|
491
913
|
team_state = load_json_object(team_state_path, "team-state")
|
|
492
914
|
dispatches = [
|
|
@@ -502,6 +924,21 @@ def link_agent_dispatch_result(
|
|
|
502
924
|
raise DispatchError(
|
|
503
925
|
f"agent result link requires exactly one dispatch record: {dispatch_id}"
|
|
504
926
|
)
|
|
927
|
+
dispatch = dispatches[0]
|
|
928
|
+
link: dict[str, Any] = {
|
|
929
|
+
"dispatchId": dispatch_id,
|
|
930
|
+
"resultPath": result_relative,
|
|
931
|
+
}
|
|
932
|
+
identity = worker_execution_identity(dispatch)
|
|
933
|
+
if identity is not None:
|
|
934
|
+
link.update({
|
|
935
|
+
"participantRef": identity["participant_ref"],
|
|
936
|
+
"roleExecutionRef": identity["role_execution_ref"],
|
|
937
|
+
"executionLabel": identity["execution_label"],
|
|
938
|
+
"dutyId": identity["duty_id"],
|
|
939
|
+
"invocationRef": identity["invocation_ref"],
|
|
940
|
+
"attempt": identity["attempt"],
|
|
941
|
+
})
|
|
505
942
|
links = team_state.setdefault("agentResultLinks", [])
|
|
506
943
|
if not isinstance(links, list):
|
|
507
944
|
raise DispatchError("team-state agentResultLinks must be an array")
|
|
@@ -539,6 +976,137 @@ def link_agent_dispatch_result(
|
|
|
539
976
|
return link
|
|
540
977
|
|
|
541
978
|
|
|
979
|
+
def _finish_native_agent_attempt(
|
|
980
|
+
*,
|
|
981
|
+
project_root: Path,
|
|
982
|
+
manifest_path: Path,
|
|
983
|
+
manifest: Mapping[str, Any],
|
|
984
|
+
dispatch: Mapping[str, Any],
|
|
985
|
+
result_path: Path,
|
|
986
|
+
result_relative: str,
|
|
987
|
+
) -> None:
|
|
988
|
+
identity = worker_execution_identity(dispatch)
|
|
989
|
+
if identity is None:
|
|
990
|
+
return
|
|
991
|
+
execution = read_execution_manifest(manifest_path)
|
|
992
|
+
invocation = next(
|
|
993
|
+
(
|
|
994
|
+
row for row in execution.invocations
|
|
995
|
+
if row.invocation_ref == identity["invocation_ref"]
|
|
996
|
+
),
|
|
997
|
+
None,
|
|
998
|
+
)
|
|
999
|
+
if invocation is None:
|
|
1000
|
+
raise DispatchError("agent result has no canonical invocation policy")
|
|
1001
|
+
if dispatch.get("writePolicyDigest") != invocation.write_policy_digest:
|
|
1002
|
+
raise DispatchError("agent dispatch write policy digest changed")
|
|
1003
|
+
if dispatch.get("writeEnforcement") != invocation.write_enforcement:
|
|
1004
|
+
raise DispatchError("agent dispatch write enforcement changed")
|
|
1005
|
+
policy = write_policy_from_payload(invocation.write_policy)
|
|
1006
|
+
snapshot_value = string_value(dispatch.get("mutationAuditSnapshotPath"))
|
|
1007
|
+
if snapshot_value:
|
|
1008
|
+
snapshot_payload = load_json_object(
|
|
1009
|
+
resolve_project_path(project_root, snapshot_value),
|
|
1010
|
+
"mutation audit snapshot",
|
|
1011
|
+
)
|
|
1012
|
+
try:
|
|
1013
|
+
snapshot = MutationSnapshot.from_payload(snapshot_payload)
|
|
1014
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
1015
|
+
raise DispatchError("mutation audit snapshot is invalid") from exc
|
|
1016
|
+
result = ExecutionMutationAudit().compare(
|
|
1017
|
+
snapshot,
|
|
1018
|
+
(policy,),
|
|
1019
|
+
out_of_plan_edits=_native_out_of_plan_paths(result_path),
|
|
1020
|
+
attempt_succeeded=True,
|
|
1021
|
+
result_present=True,
|
|
1022
|
+
)
|
|
1023
|
+
summary = result.change_summary()
|
|
1024
|
+
status = result.status
|
|
1025
|
+
else:
|
|
1026
|
+
status = "ok"
|
|
1027
|
+
summary = {
|
|
1028
|
+
"mutationStatus": "ok",
|
|
1029
|
+
"changedPaths": [],
|
|
1030
|
+
"sourceChanged": False,
|
|
1031
|
+
"gitChanged": False,
|
|
1032
|
+
"attribution": "call",
|
|
1033
|
+
"retryAllowed": False,
|
|
1034
|
+
"violations": [],
|
|
1035
|
+
"beforeDigest": "",
|
|
1036
|
+
"afterDigest": "",
|
|
1037
|
+
}
|
|
1038
|
+
_close_native_attempt(
|
|
1039
|
+
manifest_path,
|
|
1040
|
+
manifest,
|
|
1041
|
+
identity,
|
|
1042
|
+
status,
|
|
1043
|
+
result_relative,
|
|
1044
|
+
summary,
|
|
1045
|
+
)
|
|
1046
|
+
if status != "ok":
|
|
1047
|
+
violations = summary.get("violations")
|
|
1048
|
+
detail = "; ".join(violations) if isinstance(violations, list) else status
|
|
1049
|
+
raise DispatchError(f"agent mutation audit failed: {detail or status}")
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _close_native_attempt(
|
|
1053
|
+
manifest_path: Path,
|
|
1054
|
+
manifest: Mapping[str, Any],
|
|
1055
|
+
identity: Mapping[str, Any],
|
|
1056
|
+
status: str,
|
|
1057
|
+
result_path: str,
|
|
1058
|
+
summary: Mapping[str, Any],
|
|
1059
|
+
) -> None:
|
|
1060
|
+
execution = read_execution_manifest(manifest_path)
|
|
1061
|
+
existing = next(
|
|
1062
|
+
(
|
|
1063
|
+
row for row in execution.attempts
|
|
1064
|
+
if row.invocation_ref == identity["invocation_ref"]
|
|
1065
|
+
and row.attempt == identity["attempt"]
|
|
1066
|
+
),
|
|
1067
|
+
None,
|
|
1068
|
+
)
|
|
1069
|
+
if existing is not None and existing.finished_at is not None:
|
|
1070
|
+
if (
|
|
1071
|
+
existing.status == status
|
|
1072
|
+
and existing.result_path == result_path
|
|
1073
|
+
and dict(existing.change_summary) == dict(summary)
|
|
1074
|
+
):
|
|
1075
|
+
return
|
|
1076
|
+
raise DispatchError("agent attempt terminal mutation result drift")
|
|
1077
|
+
finish_attempt_mutation(
|
|
1078
|
+
manifest_path,
|
|
1079
|
+
invocation_ref=identity["invocation_ref"],
|
|
1080
|
+
attempt=identity["attempt"],
|
|
1081
|
+
finished_at=utc_now(),
|
|
1082
|
+
status=status,
|
|
1083
|
+
result_path=result_path,
|
|
1084
|
+
error_path=None,
|
|
1085
|
+
change_summary=summary,
|
|
1086
|
+
task_key=require_string(manifest, "taskKey"),
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def _native_out_of_plan_paths(result_path: Path) -> tuple[str, ...]:
|
|
1091
|
+
if result_path.suffix != ".json":
|
|
1092
|
+
return ()
|
|
1093
|
+
try:
|
|
1094
|
+
payload = json.loads(result_path.read_text(encoding="utf-8"))
|
|
1095
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
1096
|
+
return ()
|
|
1097
|
+
implementation = payload.get("implementation") if isinstance(payload, Mapping) else None
|
|
1098
|
+
rows = implementation.get("outOfPlanEdits") if isinstance(implementation, Mapping) else None
|
|
1099
|
+
if not isinstance(rows, list):
|
|
1100
|
+
return ()
|
|
1101
|
+
return tuple(
|
|
1102
|
+
str(row["file"])
|
|
1103
|
+
for row in rows
|
|
1104
|
+
if isinstance(row, Mapping)
|
|
1105
|
+
and isinstance(row.get("file"), str)
|
|
1106
|
+
and row["file"]
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
|
|
542
1110
|
def reject_agent_dispatch_result(
|
|
543
1111
|
*,
|
|
544
1112
|
project_root: Path,
|
|
@@ -712,6 +1280,25 @@ def _has_recorded_team_create(team_state: Mapping[str, Any]) -> bool:
|
|
|
712
1280
|
|
|
713
1281
|
# --- job facts ----------------------------------------------------------------
|
|
714
1282
|
|
|
1283
|
+
def build_dispatch_id(invocation_id: str, attempt: int) -> str:
|
|
1284
|
+
"""The only place Python assembles a dispatch id.
|
|
1285
|
+
|
|
1286
|
+
Two generators drifted: this module pinned the attempt to 1 while
|
|
1287
|
+
`dispatch_core` used the real one, so a re-dispatch took the first attempt's
|
|
1288
|
+
id, team-state kept the same row twice, and `validate-run` failed that run
|
|
1289
|
+
with `agent dispatch ID is duplicated`.
|
|
1290
|
+
|
|
1291
|
+
Prose mirrors of this shape live outside Python, where no guard reaches
|
|
1292
|
+
them: `prompts/lead/convergence.md`, `prompts/lead/report-writer.md`,
|
|
1293
|
+
`prompts/lead/plan-body-verification.md` and the five
|
|
1294
|
+
`adapters/hosts/*/relay.md` each spell an `--dispatch-id` argument out for
|
|
1295
|
+
the lead. They agree with this function only because
|
|
1296
|
+
`record_verified_agent_dispatch` records the first attempt and no other;
|
|
1297
|
+
changing that convention has to change them too.
|
|
1298
|
+
"""
|
|
1299
|
+
return f"{invocation_id}:attempt-{attempt}"
|
|
1300
|
+
|
|
1301
|
+
|
|
715
1302
|
def dispatch_mode(jobs: Sequence[WorkerJob]) -> str:
|
|
716
1303
|
backends = {job.backend for job in jobs}
|
|
717
1304
|
if len(backends) == 1:
|
|
@@ -723,6 +1310,59 @@ def missing_completion_paths(job: WorkerJob) -> tuple[Path, ...]:
|
|
|
723
1310
|
return tuple(path for path in job.completion_paths if not path.is_file())
|
|
724
1311
|
|
|
725
1312
|
|
|
1313
|
+
def dispatch_session_id(provider: str) -> str:
|
|
1314
|
+
"""The session this dispatch mints for *provider*, or '' for one that has no
|
|
1315
|
+
such flag.
|
|
1316
|
+
|
|
1317
|
+
Only the claude CLI reads `--session-id`; handing the value to another
|
|
1318
|
+
wrapper would start it with an argument it does not know. An empty value is
|
|
1319
|
+
the same as not issuing one — `WorkerJob.command` appends the flag only when
|
|
1320
|
+
it is set.
|
|
1321
|
+
|
|
1322
|
+
This lives beside `worker_jobs_from_file` for the same reason
|
|
1323
|
+
`dispatch_result_path` does: both job constructors apply it, and a rule
|
|
1324
|
+
written twice is a rule that drifts.
|
|
1325
|
+
"""
|
|
1326
|
+
return generate_claude_session_id() if provider == "claude" else ""
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
def worker_session_ids(
|
|
1330
|
+
team_state: Mapping[str, Any], worker_id: str | None = None
|
|
1331
|
+
) -> list[str]:
|
|
1332
|
+
"""The claude sessions this run's dispatches issued, in the order recorded.
|
|
1333
|
+
|
|
1334
|
+
A worker started in a cmux pane is its own `claude -p` process and tags its
|
|
1335
|
+
session jsonl with neither `agentName` nor `teamName`, so nothing in the
|
|
1336
|
+
transcript says which worker wrote it. The id `dispatch_session_id` minted
|
|
1337
|
+
and `_dispatch_record` wrote is the only handle on it.
|
|
1338
|
+
|
|
1339
|
+
Every id, not one: a retry opens a new session, so one worker legitimately
|
|
1340
|
+
owns several rows with different `sessionId` values, and taking only the
|
|
1341
|
+
first or last bills a single attempt of a retried worker.
|
|
1342
|
+
|
|
1343
|
+
Pass ``worker_id`` for one worker's sessions, or omit it for the run's.
|
|
1344
|
+
This reads a `workerDispatches[]` row shape, so it belongs beside the code
|
|
1345
|
+
that writes that shape rather than in each of the two modules that read it.
|
|
1346
|
+
|
|
1347
|
+
A non-list log yields nothing rather than raising: `session.observe_lead_session`
|
|
1348
|
+
calls this for its exclusion set and catches only OSError, so a `TypeError`
|
|
1349
|
+
from iterating a number here would take down `team dispatch` / `team await` /
|
|
1350
|
+
`report-finalize`. Observation not breaking its caller is a contract kept
|
|
1351
|
+
here, the same way `add_observed_session` keeps it for the keys it appends to.
|
|
1352
|
+
"""
|
|
1353
|
+
session_ids: list[str] = []
|
|
1354
|
+
dispatches = team_state.get("workerDispatches")
|
|
1355
|
+
for record in dispatches if isinstance(dispatches, list) else []:
|
|
1356
|
+
if not isinstance(record, Mapping):
|
|
1357
|
+
continue
|
|
1358
|
+
if worker_id is not None and record.get("workerId") != worker_id:
|
|
1359
|
+
continue
|
|
1360
|
+
session_id = str(record.get("sessionId") or "").strip()
|
|
1361
|
+
if session_id and session_id not in session_ids:
|
|
1362
|
+
session_ids.append(session_id)
|
|
1363
|
+
return session_ids
|
|
1364
|
+
|
|
1365
|
+
|
|
726
1366
|
def dispatch_result_path(
|
|
727
1367
|
worker_id: str,
|
|
728
1368
|
worker_result_path: Path,
|
|
@@ -921,6 +1561,121 @@ def _validate_agent_invocations(
|
|
|
921
1561
|
raise DispatchError("agent invocation contract: " + "; ".join(errors))
|
|
922
1562
|
|
|
923
1563
|
|
|
1564
|
+
def _v2_job_authority_errors(
|
|
1565
|
+
job: WorkerJob,
|
|
1566
|
+
metadata: Mapping[str, Any],
|
|
1567
|
+
manifest: Mapping[str, Any],
|
|
1568
|
+
) -> list[str]:
|
|
1569
|
+
try:
|
|
1570
|
+
identity = invocation_metadata_identity(metadata)
|
|
1571
|
+
except AgentInvocationError as exc:
|
|
1572
|
+
return [str(exc)]
|
|
1573
|
+
if identity is None:
|
|
1574
|
+
return ["jobs file v2 identity has v1 invocation metadata"]
|
|
1575
|
+
errors: list[str] = []
|
|
1576
|
+
if job.invocation_ref != identity.invocation_ref:
|
|
1577
|
+
errors.append("jobs file invocationRef does not match invocation metadata")
|
|
1578
|
+
if (
|
|
1579
|
+
job.participant_ref != identity.participant_ref
|
|
1580
|
+
or job.role_execution_ref != identity.role_execution_ref
|
|
1581
|
+
or job.execution_label != identity.execution_label
|
|
1582
|
+
or job.duty_id != identity.duty_id
|
|
1583
|
+
or job.attempt != identity.attempt
|
|
1584
|
+
):
|
|
1585
|
+
errors.append("jobs file v2 identity does not match invocation metadata")
|
|
1586
|
+
prompt = metadata.get("prompt")
|
|
1587
|
+
metadata_prompt_value = (
|
|
1588
|
+
string_value(prompt.get("path")) if isinstance(prompt, Mapping) else ""
|
|
1589
|
+
)
|
|
1590
|
+
metadata_prompt_path = resolve_project_path(
|
|
1591
|
+
job.project_root, metadata_prompt_value
|
|
1592
|
+
)
|
|
1593
|
+
if (
|
|
1594
|
+
not metadata_prompt_value
|
|
1595
|
+
or job.prompt_path.resolve() != metadata_prompt_path.resolve()
|
|
1596
|
+
):
|
|
1597
|
+
errors.append("jobs file promptPath does not match invocation metadata")
|
|
1598
|
+
role_executions = manifest.get("roleExecutions")
|
|
1599
|
+
execution = next((
|
|
1600
|
+
row for row in role_executions
|
|
1601
|
+
if isinstance(row, Mapping)
|
|
1602
|
+
and row.get("roleExecutionRef") == identity.role_execution_ref
|
|
1603
|
+
), None) if isinstance(role_executions, list) else None
|
|
1604
|
+
participants = manifest.get("participantAssignments")
|
|
1605
|
+
participant = next((
|
|
1606
|
+
row for row in participants
|
|
1607
|
+
if isinstance(row, Mapping)
|
|
1608
|
+
and row.get("participantRef") == identity.participant_ref
|
|
1609
|
+
), None) if isinstance(participants, list) else None
|
|
1610
|
+
assignment = metadata.get("modelAssignment")
|
|
1611
|
+
try:
|
|
1612
|
+
duty_role = role_for_duty(job.duty_id)
|
|
1613
|
+
except RoleCatalogError:
|
|
1614
|
+
duty_role = ""
|
|
1615
|
+
if isinstance(assignment, Mapping):
|
|
1616
|
+
try:
|
|
1617
|
+
parsed_assignment = agent_model_assignment_from_payload(assignment)
|
|
1618
|
+
except AgentInvocationError as exc:
|
|
1619
|
+
errors.append(str(exc))
|
|
1620
|
+
else:
|
|
1621
|
+
errors.extend(
|
|
1622
|
+
"jobs file " + error
|
|
1623
|
+
for error in v2_role_assignment_authority_errors(
|
|
1624
|
+
manifest,
|
|
1625
|
+
role_execution_ref=identity.role_execution_ref,
|
|
1626
|
+
participant_ref=identity.participant_ref,
|
|
1627
|
+
assignment=parsed_assignment,
|
|
1628
|
+
)
|
|
1629
|
+
)
|
|
1630
|
+
if (
|
|
1631
|
+
not isinstance(execution, Mapping)
|
|
1632
|
+
or not isinstance(participant, Mapping)
|
|
1633
|
+
or not isinstance(assignment, Mapping)
|
|
1634
|
+
or execution.get("participantRef") != job.participant_ref
|
|
1635
|
+
or execution.get("executionLabel") != job.execution_label
|
|
1636
|
+
or execution.get("role") != job.role
|
|
1637
|
+
or execution.get("role") != duty_role
|
|
1638
|
+
or execution.get("provider") != job.provider
|
|
1639
|
+
or assignment.get("modelExecutionValue") != job.model_execution_value
|
|
1640
|
+
or metadata.get("dispatchKind") != job.dispatch_kind
|
|
1641
|
+
):
|
|
1642
|
+
errors.append("jobs file v2 identity does not match role execution authority")
|
|
1643
|
+
return errors
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
def _validate_v2_jobs_file_authority(
|
|
1647
|
+
manifest: Mapping[str, Any], jobs: Sequence[WorkerJob]
|
|
1648
|
+
) -> None:
|
|
1649
|
+
if (
|
|
1650
|
+
manifest.get("schemaVersion") != "2.0"
|
|
1651
|
+
or manifest.get("executionIdentityVersion") != 2
|
|
1652
|
+
):
|
|
1653
|
+
return
|
|
1654
|
+
_validate_agent_invocations(manifest, jobs)
|
|
1655
|
+
errors: list[str] = []
|
|
1656
|
+
for job in jobs:
|
|
1657
|
+
metadata = load_json_object(job.prompt_metadata_path, "agent metadata")
|
|
1658
|
+
errors.extend(
|
|
1659
|
+
f"{job.worker_id}: {error}"
|
|
1660
|
+
for error in _v2_job_authority_errors(job, metadata, manifest)
|
|
1661
|
+
)
|
|
1662
|
+
if errors:
|
|
1663
|
+
raise DispatchError("jobs file v2 authority: " + "; ".join(errors))
|
|
1664
|
+
role_executions = {
|
|
1665
|
+
string_value(row.get("roleExecutionRef")): row
|
|
1666
|
+
for row in manifest.get("roleExecutions") or []
|
|
1667
|
+
if isinstance(row, Mapping)
|
|
1668
|
+
}
|
|
1669
|
+
if any(
|
|
1670
|
+
isinstance(role_executions.get(job.role_execution_ref), Mapping)
|
|
1671
|
+
and role_executions[job.role_execution_ref].get("role") == "translator"
|
|
1672
|
+
for job in jobs
|
|
1673
|
+
):
|
|
1674
|
+
raise DispatchError(
|
|
1675
|
+
"jobs file translator must use canonical reservation dispatch"
|
|
1676
|
+
)
|
|
1677
|
+
|
|
1678
|
+
|
|
924
1679
|
def worker_jobs_from_file(
|
|
925
1680
|
project_root: Path,
|
|
926
1681
|
jobs_file: Path,
|
|
@@ -940,7 +1695,7 @@ def worker_jobs_from_file(
|
|
|
940
1695
|
workers = payload.get("workers")
|
|
941
1696
|
if not isinstance(workers, list):
|
|
942
1697
|
raise DispatchError("jobs file workers must be an array")
|
|
943
|
-
|
|
1698
|
+
jobs = [
|
|
944
1699
|
_worker_job_from_file(
|
|
945
1700
|
project_root,
|
|
946
1701
|
item,
|
|
@@ -954,6 +1709,8 @@ def worker_jobs_from_file(
|
|
|
954
1709
|
for item in workers
|
|
955
1710
|
if isinstance(item, Mapping)
|
|
956
1711
|
]
|
|
1712
|
+
_validate_v2_jobs_file_authority(manifest, jobs)
|
|
1713
|
+
return jobs
|
|
957
1714
|
|
|
958
1715
|
|
|
959
1716
|
def _worker_job_from_file(
|
|
@@ -967,8 +1724,17 @@ def _worker_job_from_file(
|
|
|
967
1724
|
resolve_wrapper: Callable[[str], Path],
|
|
968
1725
|
default_provider: Callable[[str], str],
|
|
969
1726
|
) -> WorkerJob:
|
|
970
|
-
|
|
971
|
-
|
|
1727
|
+
identity = worker_execution_identity(item)
|
|
1728
|
+
worker_id = (
|
|
1729
|
+
require_string(item, "workerId")
|
|
1730
|
+
if identity is None
|
|
1731
|
+
else v2_worker_state_key(item)
|
|
1732
|
+
)
|
|
1733
|
+
provider = (
|
|
1734
|
+
string_value(item.get("provider")) or default_provider(worker_id)
|
|
1735
|
+
if identity is None
|
|
1736
|
+
else require_string(item, "provider")
|
|
1737
|
+
)
|
|
972
1738
|
prompt_path = resolve_project_path(
|
|
973
1739
|
project_root, require_string(item, "promptPath")
|
|
974
1740
|
)
|
|
@@ -1006,6 +1772,10 @@ def _worker_job_from_file(
|
|
|
1006
1772
|
role=require_string(item, "role"),
|
|
1007
1773
|
idle_timeout_seconds=idle_timeout_seconds,
|
|
1008
1774
|
dispatch_kind=dispatch_kind,
|
|
1775
|
+
# Minted here, not read from the file: the lead writes this file by
|
|
1776
|
+
# hand, and a session id it typed would be unfalsifiable — nothing can
|
|
1777
|
+
# tell an id that names no session from one that names the wrong one.
|
|
1778
|
+
session_id=dispatch_session_id(provider),
|
|
1009
1779
|
invocation_id=string_value(item.get("invocationId")),
|
|
1010
1780
|
audience=string_value(item.get("audience")),
|
|
1011
1781
|
assignment_ref=string_value(item.get("assignmentRef")),
|
|
@@ -1020,9 +1790,52 @@ def _worker_job_from_file(
|
|
|
1020
1790
|
prompt_digest=string_value(digest_values.get("promptDigest")),
|
|
1021
1791
|
host_model_value=host_model_value,
|
|
1022
1792
|
enforcement_mode=string_value(item.get("enforcementMode")),
|
|
1793
|
+
**(identity or {}),
|
|
1023
1794
|
)
|
|
1024
1795
|
|
|
1025
1796
|
|
|
1797
|
+
def worker_execution_identity(
|
|
1798
|
+
item: Mapping[str, Any],
|
|
1799
|
+
) -> dict[str, Any] | None:
|
|
1800
|
+
identity_keys = {
|
|
1801
|
+
"executionIdentityVersion",
|
|
1802
|
+
"participantRef",
|
|
1803
|
+
"roleExecutionRef",
|
|
1804
|
+
"executionLabel",
|
|
1805
|
+
"dutyId",
|
|
1806
|
+
"invocationRef",
|
|
1807
|
+
}
|
|
1808
|
+
schema = item.get("schemaVersion")
|
|
1809
|
+
has_v2_fields = bool(identity_keys.intersection(item))
|
|
1810
|
+
if schema in (None, 1, "1", "1.0"):
|
|
1811
|
+
if has_v2_fields:
|
|
1812
|
+
raise DispatchError("jobs file worker mixes v1 and v2 identity")
|
|
1813
|
+
return None
|
|
1814
|
+
if schema != "2.0" or item.get("executionIdentityVersion") != 2:
|
|
1815
|
+
raise DispatchError("jobs file worker has unsupported identity version")
|
|
1816
|
+
if "workerId" in item:
|
|
1817
|
+
raise DispatchError("jobs file worker mixes v1 and v2 identity")
|
|
1818
|
+
attempt = item.get("attempt")
|
|
1819
|
+
if not isinstance(attempt, int) or isinstance(attempt, bool) or attempt < 1:
|
|
1820
|
+
raise DispatchError("jobs file v2 attempt must be positive")
|
|
1821
|
+
return {
|
|
1822
|
+
"participant_ref": require_string(item, "participantRef"),
|
|
1823
|
+
"role_execution_ref": require_string(item, "roleExecutionRef"),
|
|
1824
|
+
"execution_label": require_string(item, "executionLabel"),
|
|
1825
|
+
"duty_id": require_string(item, "dutyId"),
|
|
1826
|
+
"invocation_ref": require_string(item, "invocationRef"),
|
|
1827
|
+
"attempt": attempt,
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
def v2_worker_state_key(item: Mapping[str, Any]) -> str:
|
|
1832
|
+
assignment_ref = require_string(item, "assignmentRef")
|
|
1833
|
+
worker_id = assignment_ref.rsplit("/", 1)[-1]
|
|
1834
|
+
if not worker_id:
|
|
1835
|
+
raise DispatchError("jobs file v2 assignmentRef has no worker projection")
|
|
1836
|
+
return worker_id
|
|
1837
|
+
|
|
1838
|
+
|
|
1026
1839
|
def utc_now() -> str:
|
|
1027
1840
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1028
1841
|
|