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
|
@@ -10,18 +10,18 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Any, Mapping, Sequence
|
|
11
11
|
|
|
12
12
|
from . import cmux
|
|
13
|
-
from . import tmux
|
|
14
13
|
from .dispatch_state import (
|
|
15
14
|
BACKEND_CLI_WRAPPER,
|
|
16
15
|
BACKEND_CMUX_PANE,
|
|
17
16
|
BACKEND_MIXED,
|
|
18
|
-
BACKEND_TMUX_PANE,
|
|
19
17
|
append_worker_dispatch as _append_worker_dispatch,
|
|
18
|
+
build_dispatch_id as _build_dispatch_id,
|
|
20
19
|
DispatchError,
|
|
21
20
|
WorkerJob,
|
|
22
21
|
dispatch_completion_paths as _completion_paths,
|
|
23
22
|
dispatch_mode as _dispatch_mode,
|
|
24
23
|
dispatch_result_path as _result_path_for_worker,
|
|
24
|
+
dispatch_session_id as _dispatch_session_id,
|
|
25
25
|
LIVENESS_AUDIT_HEARTBEAT,
|
|
26
26
|
LIVENESS_WRAPPER_STATUS,
|
|
27
27
|
load_json_object as _load_json_object,
|
|
@@ -39,10 +39,36 @@ from .dispatch_state import (
|
|
|
39
39
|
TEARDOWN_BEFORE_TERMINAL_REASON,
|
|
40
40
|
utc_now as _utc_now,
|
|
41
41
|
validate_dispatch_prompts as _validate_dispatch_prompts,
|
|
42
|
+
v2_worker_state_key as _v2_worker_state_key,
|
|
43
|
+
worker_execution_identity as _worker_execution_identity,
|
|
42
44
|
worker_jobs_from_file as _worker_jobs_from_file,
|
|
43
45
|
worker_state as _worker_state,
|
|
44
46
|
worktree_path as _worktree_path,
|
|
45
47
|
)
|
|
48
|
+
from .assignment_resolver import (
|
|
49
|
+
AssignmentEnvironment,
|
|
50
|
+
AssignmentResolutionError,
|
|
51
|
+
RoleInstance,
|
|
52
|
+
resolve_model_assignment,
|
|
53
|
+
)
|
|
54
|
+
from .domain.host import HostSessionContext
|
|
55
|
+
from .domain.provider import HostModelBinding, ServedModelAttestation
|
|
56
|
+
from .domain.worker_exec import SERVED_MODEL_MISMATCH_EXIT_CODE
|
|
57
|
+
from .execution_identity import Attempt, Invocation, RoleExecution, model_spec_digest
|
|
58
|
+
from .execution_manifest import (
|
|
59
|
+
finish_attempt_mutation,
|
|
60
|
+
read_execution_manifest,
|
|
61
|
+
record_invocation_attempt,
|
|
62
|
+
)
|
|
63
|
+
from .execution_mutation_audit import (
|
|
64
|
+
ExecutionMutationAudit,
|
|
65
|
+
MutationAuditResult,
|
|
66
|
+
MutationSnapshot,
|
|
67
|
+
assert_compatible_batch,
|
|
68
|
+
)
|
|
69
|
+
from .convergence_store import write_json_atomic
|
|
70
|
+
from .assignment_environment import load_assignment_context
|
|
71
|
+
from .model_pool import ModelPool
|
|
46
72
|
from .error_log_write import append_observed
|
|
47
73
|
from .lead_events import LeadEvent, append_lead_event
|
|
48
74
|
from .initial_prompt_materialization import (
|
|
@@ -52,6 +78,7 @@ from .initial_prompt_materialization import (
|
|
|
52
78
|
PromptDeliveryMode,
|
|
53
79
|
materialize_initial_prompts,
|
|
54
80
|
)
|
|
81
|
+
from .agent_invocation import AgentInvocationError, materialize_retry_invocation
|
|
55
82
|
from .path_hints import hydrate_active_run_context
|
|
56
83
|
from .schema_excerpt import bundle_excerpt_path, excerpt_version_skew
|
|
57
84
|
from .seeding import installed_version
|
|
@@ -72,6 +99,15 @@ from .worker_prompt_headers import (
|
|
|
72
99
|
)
|
|
73
100
|
from .worker_artifact_paths import audit_sidecar_rel
|
|
74
101
|
from .wrapper_status import read_wrapper_status, status_path_for_prompt
|
|
102
|
+
from .worker_request import verifier_extra_dirs
|
|
103
|
+
from .write_policy import (
|
|
104
|
+
WriteEnforcement,
|
|
105
|
+
WritePolicy,
|
|
106
|
+
build_invocation_write_contract,
|
|
107
|
+
planned_paths_from_run_manifest,
|
|
108
|
+
write_enforcement_from_payload,
|
|
109
|
+
write_policy_from_payload,
|
|
110
|
+
)
|
|
75
111
|
|
|
76
112
|
|
|
77
113
|
MAX_WORKER_ATTEMPTS = 2
|
|
@@ -84,6 +120,102 @@ _WRAPPER_TIMEOUT_EXIT_CODE = 124
|
|
|
84
120
|
_WRAPPER_LOG_TAIL_BYTES = 800
|
|
85
121
|
|
|
86
122
|
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class RevalidatedBinding:
|
|
125
|
+
role_execution_ref: str
|
|
126
|
+
binding: HostModelBinding
|
|
127
|
+
model_spec_digest: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def revalidate_role_execution(
|
|
131
|
+
role_execution: RoleExecution,
|
|
132
|
+
pool: ModelPool,
|
|
133
|
+
host: AssignmentEnvironment,
|
|
134
|
+
) -> RevalidatedBinding:
|
|
135
|
+
"""Recompute one selected model binding without inspecting unrelated rows."""
|
|
136
|
+
if (
|
|
137
|
+
role_execution.model_ref is None
|
|
138
|
+
or role_execution.binding is None
|
|
139
|
+
or role_execution.model_spec_digest is None
|
|
140
|
+
):
|
|
141
|
+
raise DispatchError("model binding changed: role execution is unbound")
|
|
142
|
+
try:
|
|
143
|
+
assignment = resolve_model_assignment(
|
|
144
|
+
instance=RoleInstance(
|
|
145
|
+
role_execution.role,
|
|
146
|
+
role_execution.role,
|
|
147
|
+
role_execution.ordinal,
|
|
148
|
+
),
|
|
149
|
+
model_ref=role_execution.model_ref,
|
|
150
|
+
pool=pool,
|
|
151
|
+
host=HostSessionContext(
|
|
152
|
+
host_id=host.host_descriptor.id,
|
|
153
|
+
entry_mode="spawn-process",
|
|
154
|
+
available_functions=frozenset(),
|
|
155
|
+
interaction_surface="role-execution-dispatch",
|
|
156
|
+
),
|
|
157
|
+
environment=host,
|
|
158
|
+
)
|
|
159
|
+
binding = assignment.binding
|
|
160
|
+
if binding is None:
|
|
161
|
+
raise AssignmentResolutionError("selected model has no worker binding")
|
|
162
|
+
digest = model_spec_digest(pool.resolve(role_execution.model_ref), binding)
|
|
163
|
+
except (ValueError, RuntimeError) as exc:
|
|
164
|
+
raise DispatchError(f"model binding changed: {exc}") from exc
|
|
165
|
+
identity_matches = (
|
|
166
|
+
assignment.provider_id == role_execution.provider
|
|
167
|
+
and assignment.model_id == role_execution.model_id
|
|
168
|
+
and binding == role_execution.binding
|
|
169
|
+
and digest == role_execution.model_spec_digest
|
|
170
|
+
)
|
|
171
|
+
if not identity_matches:
|
|
172
|
+
raise DispatchError("model binding changed for selected role execution")
|
|
173
|
+
return RevalidatedBinding(role_execution.role_execution_ref, binding, digest)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def dispatch_revalidated_role_execution(
|
|
177
|
+
role_execution: RoleExecution,
|
|
178
|
+
pool: ModelPool,
|
|
179
|
+
host: AssignmentEnvironment,
|
|
180
|
+
dispatch,
|
|
181
|
+
):
|
|
182
|
+
"""Run a dispatch callback only after the pure binding gate succeeds."""
|
|
183
|
+
binding = revalidate_role_execution(role_execution, pool, host)
|
|
184
|
+
return dispatch(binding)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def verify_served_model(
|
|
188
|
+
role_execution: RoleExecution,
|
|
189
|
+
attestation: ServedModelAttestation,
|
|
190
|
+
*,
|
|
191
|
+
pool: ModelPool,
|
|
192
|
+
) -> ServedModelAttestation:
|
|
193
|
+
"""Reject observed substitution while preserving unobservable attempts."""
|
|
194
|
+
if attestation.level == "unknown":
|
|
195
|
+
return attestation
|
|
196
|
+
if role_execution.model_ref is None or not attestation.normalized_model_ref:
|
|
197
|
+
raise DispatchError("served model differs from selected model")
|
|
198
|
+
try:
|
|
199
|
+
selected = pool.resolve(role_execution.model_ref)
|
|
200
|
+
observed = pool.resolve(attestation.normalized_model_ref)
|
|
201
|
+
except ValueError as exc:
|
|
202
|
+
raise DispatchError("served model differs from selected model") from exc
|
|
203
|
+
expected_level = "channel" if observed.version_kind == "channel" else "exact"
|
|
204
|
+
if attestation.level != expected_level:
|
|
205
|
+
raise DispatchError("served model attestation level is inconsistent")
|
|
206
|
+
if selected.version_kind == "pinned":
|
|
207
|
+
if str(selected.model_ref) != str(observed.model_ref):
|
|
208
|
+
raise DispatchError("served model differs from selected model")
|
|
209
|
+
return attestation
|
|
210
|
+
if (
|
|
211
|
+
selected.provider_id != observed.provider_id
|
|
212
|
+
or not selected.channel_family
|
|
213
|
+
or selected.channel_family != observed.channel_family
|
|
214
|
+
):
|
|
215
|
+
raise DispatchError("served model channel family differs")
|
|
216
|
+
return attestation
|
|
217
|
+
|
|
218
|
+
|
|
87
219
|
@dataclass(frozen=True)
|
|
88
220
|
class WorkerHandle:
|
|
89
221
|
job: WorkerJob
|
|
@@ -92,6 +224,11 @@ class WorkerHandle:
|
|
|
92
224
|
status_sidecar_path: Path | None
|
|
93
225
|
degraded_from: str
|
|
94
226
|
running_process: subprocess.Popen[str] | None = None
|
|
227
|
+
mutation_snapshot: MutationSnapshot | None = None
|
|
228
|
+
mutation_policies: tuple[WritePolicy, ...] = ()
|
|
229
|
+
mutation_retry_allowed: bool = True
|
|
230
|
+
write_policy_digest: str = ""
|
|
231
|
+
write_enforcement: WriteEnforcement | None = None
|
|
95
232
|
|
|
96
233
|
|
|
97
234
|
@dataclass(frozen=True)
|
|
@@ -203,6 +340,9 @@ def build_dispatch_plan(
|
|
|
203
340
|
requested_workers,
|
|
204
341
|
options,
|
|
205
342
|
)
|
|
343
|
+
# Ahead of the per-prompt checks: a batch the lead must split is a plan-shape
|
|
344
|
+
# problem, and reporting a prompt-header error for a doomed batch buries it.
|
|
345
|
+
_validate_implementation_phase_order(jobs)
|
|
206
346
|
_validate_dispatch_prompts(manifest, active_context, jobs)
|
|
207
347
|
_reject_stale_schema_excerpt(project_root, manifest, jobs)
|
|
208
348
|
return DispatchPlan(
|
|
@@ -219,12 +359,14 @@ def build_dispatch_plan(
|
|
|
219
359
|
|
|
220
360
|
def dispatch_plan(plan: DispatchPlan, *, wait: bool = True) -> int:
|
|
221
361
|
_validate_report_writer_isolation(plan.jobs)
|
|
362
|
+
_validate_implementation_phase_order(plan.jobs)
|
|
363
|
+
_revalidate_plan_role_executions(plan)
|
|
222
364
|
if wait:
|
|
223
365
|
pane_backends = sorted(
|
|
224
366
|
{
|
|
225
367
|
job.backend
|
|
226
368
|
for job in plan.jobs
|
|
227
|
-
if job.backend
|
|
369
|
+
if job.backend == BACKEND_CMUX_PANE
|
|
228
370
|
}
|
|
229
371
|
)
|
|
230
372
|
if pane_backends:
|
|
@@ -250,12 +392,44 @@ def dispatch_cli_wrapper_plan(plan: DispatchPlan) -> int:
|
|
|
250
392
|
if any(job.backend != BACKEND_CLI_WRAPPER for job in plan.jobs):
|
|
251
393
|
raise DispatchError("concurrent CLI dispatch requires cli-wrapper jobs only")
|
|
252
394
|
_validate_report_writer_isolation(plan.jobs)
|
|
395
|
+
_validate_implementation_phase_order(plan.jobs)
|
|
396
|
+
_revalidate_plan_role_executions(plan)
|
|
253
397
|
_record_dispatch_facts(plan.team_state_path, _dispatch_mode(plan.jobs))
|
|
254
398
|
final_codes = _dispatch_cli_wrapper_batch(plan, plan.jobs)
|
|
255
399
|
return next((final_codes[job.worker_id] for job in plan.jobs
|
|
256
400
|
if final_codes.get(job.worker_id, 0) != 0), 0)
|
|
257
401
|
|
|
258
402
|
|
|
403
|
+
def _revalidate_job_role_execution(plan: DispatchPlan, job: WorkerJob) -> None:
|
|
404
|
+
manifest = read_execution_manifest(plan.manifest_path)
|
|
405
|
+
executions = {
|
|
406
|
+
row.role_execution_ref: row for row in manifest.role_executions
|
|
407
|
+
}
|
|
408
|
+
context = load_assignment_context(
|
|
409
|
+
host_runtime=_require_string(plan.manifest, "leadRuntime"),
|
|
410
|
+
terminal_backend=_require_string(plan.manifest, "terminalBackend"),
|
|
411
|
+
execution_provider_ids=(job.provider,),
|
|
412
|
+
)
|
|
413
|
+
role_execution = executions.get(job.role_execution_ref)
|
|
414
|
+
if (
|
|
415
|
+
role_execution is None
|
|
416
|
+
or role_execution.participant_ref != job.participant_ref
|
|
417
|
+
or role_execution.execution_label != job.execution_label
|
|
418
|
+
):
|
|
419
|
+
raise DispatchError("worker job role execution does not match run manifest")
|
|
420
|
+
revalidate_role_execution(
|
|
421
|
+
role_execution,
|
|
422
|
+
context.pool,
|
|
423
|
+
context.environment,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _revalidate_plan_role_executions(plan: DispatchPlan) -> None:
|
|
428
|
+
for job in plan.jobs:
|
|
429
|
+
if job.has_execution_identity:
|
|
430
|
+
_revalidate_job_role_execution(plan, job)
|
|
431
|
+
|
|
432
|
+
|
|
259
433
|
def _validate_report_writer_isolation(jobs: Sequence[WorkerJob]) -> None:
|
|
260
434
|
if (
|
|
261
435
|
any(job.worker_id == REPORT_WRITER_WORKER_ID for job in jobs)
|
|
@@ -267,6 +441,55 @@ def _validate_report_writer_isolation(jobs: Sequence[WorkerJob]) -> None:
|
|
|
267
441
|
)
|
|
268
442
|
|
|
269
443
|
|
|
444
|
+
def _validate_implementation_phase_order(jobs: Sequence[WorkerJob]) -> None:
|
|
445
|
+
"""Keep the executor and the verifiers in two dispatches, never one batch.
|
|
446
|
+
|
|
447
|
+
A verifier reads the diff the executor wrote, so `implementation.md` binds
|
|
448
|
+
its contract to the window "between Executor stage completion and the first
|
|
449
|
+
verifier dispatch". Nothing enforced that ordering: one
|
|
450
|
+
`okstra team dispatch --workers claude,codex` started both at once and the
|
|
451
|
+
verifier observed base HEAD instead of the stage diff, which is what its
|
|
452
|
+
result then reported on.
|
|
453
|
+
|
|
454
|
+
The refusal names the worker IDs on both sides because the second dispatch
|
|
455
|
+
has to drop the executor's ID: `worker_prompt_policy` re-materializes that
|
|
456
|
+
ID as the executor every time, so a `--workers` list that keeps it is
|
|
457
|
+
refused again and a message saying only "split the batch" would loop.
|
|
458
|
+
|
|
459
|
+
`build_dispatch_plan` is the only call every production dispatch takes, and
|
|
460
|
+
the one `--dry-run` stops at, so it is what makes the preview refuse. The
|
|
461
|
+
two dispatch entry points are public and take a caller-assembled plan, so
|
|
462
|
+
they check it again there rather than trust their input.
|
|
463
|
+
"""
|
|
464
|
+
executors = [
|
|
465
|
+
job.worker_id for job in jobs if job.audience == "implementation-executor"
|
|
466
|
+
]
|
|
467
|
+
verifiers = [
|
|
468
|
+
job.worker_id for job in jobs if job.audience == "implementation-verifier"
|
|
469
|
+
]
|
|
470
|
+
if executors and verifiers:
|
|
471
|
+
executor_ids = ", ".join(executors)
|
|
472
|
+
raise DispatchError(
|
|
473
|
+
"an implementation batch may not hold the executor and a verifier: "
|
|
474
|
+
f"{_named_role(executors, 'executor')}, "
|
|
475
|
+
f"{_named_role(verifiers, 'verifier')}. "
|
|
476
|
+
"Dispatch the executor alone, settle it with `okstra team await`, "
|
|
477
|
+
f"then dispatch the verifiers with `--workers` omitting {executor_ids} "
|
|
478
|
+
"— that worker ID materializes as the executor again, so a second "
|
|
479
|
+
"batch holding it is refused too. The split is what lets a verifier "
|
|
480
|
+
"observe the stage diff instead of base HEAD"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _named_role(worker_ids: Sequence[str], role: str) -> str:
|
|
485
|
+
"""`verifier codex` for one, `verifiers codex, antigravity` for several.
|
|
486
|
+
|
|
487
|
+
A roster that adds antigravity puts two verifiers in the same refusal, and
|
|
488
|
+
a singular label there reads as one worker named "codex, antigravity".
|
|
489
|
+
"""
|
|
490
|
+
return f"{role}{'s' if len(worker_ids) > 1 else ''} {', '.join(worker_ids)}"
|
|
491
|
+
|
|
492
|
+
|
|
270
493
|
def _dispatch_cli_wrapper_batch(
|
|
271
494
|
plan: DispatchPlan,
|
|
272
495
|
jobs: Sequence[WorkerJob],
|
|
@@ -274,15 +497,60 @@ def _dispatch_cli_wrapper_batch(
|
|
|
274
497
|
"""Run one concurrent batch through all retries before the next dependency."""
|
|
275
498
|
pending = [(job, 1) for job in jobs]
|
|
276
499
|
final_codes: dict[str, int] = {}
|
|
500
|
+
baseline_snapshot: MutationSnapshot | None = None
|
|
277
501
|
while pending:
|
|
278
502
|
active: list[tuple[WorkerHandle, int]] = []
|
|
503
|
+
draft_contracts = tuple(
|
|
504
|
+
_canonical_write_contract(plan, job) if job.has_execution_identity else None
|
|
505
|
+
for job, _attempt in pending
|
|
506
|
+
)
|
|
507
|
+
draft_policies = tuple(
|
|
508
|
+
contract[0] for contract in draft_contracts if contract is not None
|
|
509
|
+
)
|
|
510
|
+
if draft_policies:
|
|
511
|
+
assert_compatible_batch(draft_policies)
|
|
512
|
+
prepared = [
|
|
513
|
+
(_prepare_job_attempt(plan, job, attempt), attempt)
|
|
514
|
+
for job, attempt in pending
|
|
515
|
+
]
|
|
516
|
+
contracts = tuple(
|
|
517
|
+
_persisted_write_contract(plan, job) for job, _ in prepared
|
|
518
|
+
)
|
|
519
|
+
policies = tuple(
|
|
520
|
+
contract[0] for contract in contracts if contract is not None
|
|
521
|
+
)
|
|
522
|
+
needs_batch = any(
|
|
523
|
+
contract is not None and contract[1].mutation_audit == "batch"
|
|
524
|
+
for contract in contracts
|
|
525
|
+
)
|
|
526
|
+
if needs_batch and baseline_snapshot is None:
|
|
527
|
+
baseline_snapshot = _mutation_snapshot(
|
|
528
|
+
plan, policies, tuple(job for job, _ in prepared)
|
|
529
|
+
)
|
|
530
|
+
snapshot = baseline_snapshot if needs_batch else None
|
|
279
531
|
starting_job: WorkerJob | None = None
|
|
280
532
|
starting_attempt = 1
|
|
281
533
|
try:
|
|
282
|
-
for job, attempt in
|
|
534
|
+
for (job, attempt), contract in zip(prepared, contracts, strict=True):
|
|
283
535
|
starting_job = job
|
|
284
536
|
starting_attempt = attempt
|
|
285
|
-
|
|
537
|
+
handle = _spawn_cli_job_nonblocking(plan, job, attempt)
|
|
538
|
+
handle = replace(
|
|
539
|
+
handle,
|
|
540
|
+
mutation_snapshot=snapshot,
|
|
541
|
+
mutation_policies=policies,
|
|
542
|
+
write_policy_digest=contract[0].digest if contract else "",
|
|
543
|
+
write_enforcement=contract[1] if contract else None,
|
|
544
|
+
)
|
|
545
|
+
active.append((handle, attempt))
|
|
546
|
+
_record_dispatch(
|
|
547
|
+
plan.team_state_path, handle, attempt, "running", ""
|
|
548
|
+
)
|
|
549
|
+
_append_event(
|
|
550
|
+
plan,
|
|
551
|
+
"worker-dispatched",
|
|
552
|
+
_attempt_details(job, attempt, handle),
|
|
553
|
+
)
|
|
286
554
|
except (OSError, DispatchError) as exc:
|
|
287
555
|
if starting_job is not None:
|
|
288
556
|
_abort_cli_batch_start(
|
|
@@ -290,7 +558,9 @@ def _dispatch_cli_wrapper_batch(
|
|
|
290
558
|
active,
|
|
291
559
|
starting_job,
|
|
292
560
|
starting_attempt,
|
|
293
|
-
|
|
561
|
+
prepared,
|
|
562
|
+
snapshot,
|
|
563
|
+
policies,
|
|
294
564
|
exc,
|
|
295
565
|
)
|
|
296
566
|
raise
|
|
@@ -307,17 +577,24 @@ def _dispatch_cli_wrapper_batch(
|
|
|
307
577
|
next_round: list[tuple[WorkerJob, int]] = []
|
|
308
578
|
for handle, attempt, outcome in outcomes:
|
|
309
579
|
job = handle.job
|
|
310
|
-
_finish_attempt(
|
|
311
|
-
|
|
580
|
+
retry_allowed = _finish_attempt(
|
|
581
|
+
plan,
|
|
582
|
+
job,
|
|
583
|
+
attempt,
|
|
584
|
+
outcome,
|
|
585
|
+
snapshot=handle.mutation_snapshot,
|
|
586
|
+
policies=handle.mutation_policies,
|
|
587
|
+
)
|
|
588
|
+
if _job_terminal_status(plan, job, attempt) == "completed":
|
|
312
589
|
final_codes[job.worker_id] = 0
|
|
313
590
|
continue
|
|
314
|
-
if _should_retry(outcome, attempt):
|
|
591
|
+
if _should_retry(outcome, attempt) and retry_allowed:
|
|
315
592
|
_append_event(
|
|
316
593
|
plan,
|
|
317
594
|
"worker-retry-scheduled",
|
|
318
595
|
_retry_details(job, attempt, outcome),
|
|
319
596
|
)
|
|
320
|
-
next_round.append((job, attempt + 1))
|
|
597
|
+
next_round.append((_job_for_next_attempt(job), attempt + 1))
|
|
321
598
|
continue
|
|
322
599
|
final_codes[job.worker_id] = outcome.returncode or 1
|
|
323
600
|
pending = next_round
|
|
@@ -330,6 +607,8 @@ def _abort_cli_batch_start(
|
|
|
330
607
|
failed_job: WorkerJob,
|
|
331
608
|
failed_attempt: int,
|
|
332
609
|
pending: Sequence[tuple[WorkerJob, int]],
|
|
610
|
+
snapshot: MutationSnapshot | None,
|
|
611
|
+
policies: Sequence[WritePolicy],
|
|
333
612
|
error: Exception,
|
|
334
613
|
) -> None:
|
|
335
614
|
reason = f"concurrent CLI batch launch failed: {error}"
|
|
@@ -341,15 +620,11 @@ def _abort_cli_batch_start(
|
|
|
341
620
|
process = handle.running_process
|
|
342
621
|
if process is not None:
|
|
343
622
|
process.wait()
|
|
344
|
-
|
|
345
|
-
plan.team_state_path, handle.job.worker_id, "error", reason
|
|
346
|
-
)
|
|
623
|
+
_transition_job_status(plan, handle.job, "error", reason)
|
|
347
624
|
_update_dispatch_status(
|
|
348
625
|
plan.team_state_path, handle.job, attempt, "error", reason
|
|
349
626
|
)
|
|
350
|
-
|
|
351
|
-
plan.team_state_path, failed_job.worker_id, "error", reason
|
|
352
|
-
)
|
|
627
|
+
_transition_job_status(plan, failed_job, "error", reason)
|
|
353
628
|
if not _update_dispatch_status(
|
|
354
629
|
plan.team_state_path,
|
|
355
630
|
failed_job,
|
|
@@ -357,12 +632,19 @@ def _abort_cli_batch_start(
|
|
|
357
632
|
"error",
|
|
358
633
|
reason,
|
|
359
634
|
):
|
|
635
|
+
failed_contract = _persisted_write_contract(plan, failed_job)
|
|
360
636
|
failed_handle = WorkerHandle(
|
|
361
637
|
failed_job,
|
|
362
638
|
"",
|
|
363
639
|
None,
|
|
364
640
|
status_path_for_prompt(failed_job.prompt_path),
|
|
365
641
|
"",
|
|
642
|
+
mutation_snapshot=snapshot,
|
|
643
|
+
mutation_policies=tuple(policies),
|
|
644
|
+
write_policy_digest=(
|
|
645
|
+
failed_contract[0].digest if failed_contract else ""
|
|
646
|
+
),
|
|
647
|
+
write_enforcement=(failed_contract[1] if failed_contract else None),
|
|
366
648
|
)
|
|
367
649
|
_record_dispatch(
|
|
368
650
|
plan.team_state_path,
|
|
@@ -378,18 +660,21 @@ def _abort_cli_batch_start(
|
|
|
378
660
|
for job, attempt in pending:
|
|
379
661
|
if (job.worker_id, attempt) in closed:
|
|
380
662
|
continue
|
|
381
|
-
|
|
382
|
-
plan.team_state_path, job.worker_id, "error", reason
|
|
383
|
-
)
|
|
663
|
+
_transition_job_status(plan, job, "error", reason)
|
|
384
664
|
if not _update_dispatch_status(
|
|
385
665
|
plan.team_state_path, job, attempt, "error", reason
|
|
386
666
|
):
|
|
667
|
+
contract = _persisted_write_contract(plan, job)
|
|
387
668
|
handle = WorkerHandle(
|
|
388
669
|
job,
|
|
389
670
|
"",
|
|
390
671
|
None,
|
|
391
672
|
status_path_for_prompt(job.prompt_path),
|
|
392
673
|
"",
|
|
674
|
+
mutation_snapshot=snapshot,
|
|
675
|
+
mutation_policies=tuple(policies),
|
|
676
|
+
write_policy_digest=contract[0].digest if contract else "",
|
|
677
|
+
write_enforcement=contract[1] if contract else None,
|
|
393
678
|
)
|
|
394
679
|
_record_dispatch(
|
|
395
680
|
plan.team_state_path,
|
|
@@ -398,6 +683,20 @@ def _abort_cli_batch_start(
|
|
|
398
683
|
"error",
|
|
399
684
|
reason,
|
|
400
685
|
)
|
|
686
|
+
for job, attempt in pending:
|
|
687
|
+
if not job.has_execution_identity:
|
|
688
|
+
continue
|
|
689
|
+
outcome = WorkerOutcome(
|
|
690
|
+
returncode=1,
|
|
691
|
+
missing_completion_paths=_missing_completion_paths(job),
|
|
692
|
+
pane_id="",
|
|
693
|
+
status_sidecar_path=status_path_for_prompt(job.prompt_path),
|
|
694
|
+
timeout=False,
|
|
695
|
+
terminal_stage="launch-failed",
|
|
696
|
+
degraded_from="",
|
|
697
|
+
)
|
|
698
|
+
mutation = _audit_attempt(job, outcome, snapshot, policies)
|
|
699
|
+
_finish_manifest_attempt(plan, job, attempt, mutation, None)
|
|
401
700
|
|
|
402
701
|
|
|
403
702
|
def await_dispatches(
|
|
@@ -443,6 +742,15 @@ def _jobs_from_roster(
|
|
|
443
742
|
options,
|
|
444
743
|
team_state=team_state,
|
|
445
744
|
)
|
|
745
|
+
if selected == ["translator"]:
|
|
746
|
+
return [_translator_job_from_reservation(
|
|
747
|
+
project_root,
|
|
748
|
+
workspace_root,
|
|
749
|
+
manifest,
|
|
750
|
+
team_state,
|
|
751
|
+
active_context,
|
|
752
|
+
options,
|
|
753
|
+
)]
|
|
446
754
|
_mark_roster_skips(project_root, manifest, team_state, selected, requested_workers, options)
|
|
447
755
|
worker_facts, prompt_paths = _materialize_roster_prompts(
|
|
448
756
|
project_root,
|
|
@@ -479,6 +787,18 @@ def _select_workers(
|
|
|
479
787
|
recommended = _string_list(manifest.get("recommendedWorkers"))
|
|
480
788
|
if not recommended:
|
|
481
789
|
raise DispatchError("run manifest has no recommendedWorkers entries")
|
|
790
|
+
if tuple(requested_workers) == ("translator",):
|
|
791
|
+
provider = _canonical_translator_provider(manifest)
|
|
792
|
+
if provider is None:
|
|
793
|
+
raise DispatchError(
|
|
794
|
+
"translator dispatch requires a canonical translator role execution"
|
|
795
|
+
)
|
|
796
|
+
if provider not in options.supported_worker_wrappers:
|
|
797
|
+
raise DispatchError(
|
|
798
|
+
f"unsupported {options.unsupported_worker_label} translator provider: "
|
|
799
|
+
f"{provider}"
|
|
800
|
+
)
|
|
801
|
+
return ["translator"]
|
|
482
802
|
if options.cli_wrapper_assignments_only:
|
|
483
803
|
return _select_cli_wrapper_assignments(
|
|
484
804
|
recommended,
|
|
@@ -510,6 +830,178 @@ def _select_workers(
|
|
|
510
830
|
return selected
|
|
511
831
|
|
|
512
832
|
|
|
833
|
+
def _canonical_translator_provider(manifest: Mapping[str, Any]) -> str | None:
|
|
834
|
+
execution = _canonical_translator_execution(manifest)
|
|
835
|
+
return _string_value(execution.get("provider")) if execution else None
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _canonical_translator_execution(
|
|
839
|
+
manifest: Mapping[str, Any],
|
|
840
|
+
) -> Mapping[str, Any] | None:
|
|
841
|
+
assignments = manifest.get("invocationAssignments")
|
|
842
|
+
assignment = (
|
|
843
|
+
assignments.get("translator") if isinstance(assignments, Mapping) else None
|
|
844
|
+
)
|
|
845
|
+
if not isinstance(assignment, Mapping):
|
|
846
|
+
return None
|
|
847
|
+
provider = _string_value(assignment.get("provider"))
|
|
848
|
+
model_execution = _string_value(assignment.get("modelExecutionValue"))
|
|
849
|
+
runner = _string_value(assignment.get("runner"))
|
|
850
|
+
if not provider or not model_execution or runner != BACKEND_CLI_WRAPPER:
|
|
851
|
+
return None
|
|
852
|
+
role_executions = manifest.get("roleExecutions")
|
|
853
|
+
matches = [
|
|
854
|
+
row
|
|
855
|
+
for row in role_executions
|
|
856
|
+
if isinstance(row, Mapping)
|
|
857
|
+
and row.get("role") == "translator"
|
|
858
|
+
and row.get("provider") == provider
|
|
859
|
+
and isinstance(row.get("binding"), Mapping)
|
|
860
|
+
and row["binding"].get("runner") == runner
|
|
861
|
+
and row["binding"].get("resolvedExecutionValue") == model_execution
|
|
862
|
+
] if isinstance(role_executions, list) else []
|
|
863
|
+
return matches[0] if len(matches) == 1 else None
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
_TRANSLATOR_RESERVATION_KEYS = {
|
|
867
|
+
"schemaVersion", "executionIdentityVersion", "invocationId",
|
|
868
|
+
"assignmentRef", "audience", "dispatchKind", "promptPath",
|
|
869
|
+
"metadataPath", "invocationRef", "participantRef",
|
|
870
|
+
"roleExecutionRef", "dutyId", "attempt",
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _translator_job_from_reservation(
|
|
875
|
+
project_root: Path,
|
|
876
|
+
workspace_root: Path,
|
|
877
|
+
manifest: Mapping[str, Any],
|
|
878
|
+
team_state: Mapping[str, Any],
|
|
879
|
+
active_context: Mapping[str, Any],
|
|
880
|
+
options: _BuildOptions,
|
|
881
|
+
) -> WorkerJob:
|
|
882
|
+
execution = _canonical_translator_execution(manifest)
|
|
883
|
+
if execution is None:
|
|
884
|
+
raise DispatchError(
|
|
885
|
+
"translator dispatch requires a canonical translator role execution"
|
|
886
|
+
)
|
|
887
|
+
contract = manifest.get("agentContract")
|
|
888
|
+
if not isinstance(contract, Mapping):
|
|
889
|
+
raise DispatchError("translator dispatch requires an agent contract")
|
|
890
|
+
reservation_root = _resolve_required_path(
|
|
891
|
+
project_root, contract, "invocationReservationRootPath"
|
|
892
|
+
)
|
|
893
|
+
dispatched_ids = {
|
|
894
|
+
str(row.get("dispatchId") or "")
|
|
895
|
+
for collection in (
|
|
896
|
+
team_state.get("workerDispatches") or [],
|
|
897
|
+
team_state.get("agentDispatches") or [],
|
|
898
|
+
)
|
|
899
|
+
for row in collection
|
|
900
|
+
if isinstance(row, Mapping)
|
|
901
|
+
}
|
|
902
|
+
candidates: list[Mapping[str, Any]] = []
|
|
903
|
+
for path in sorted(reservation_root.glob("*.json")):
|
|
904
|
+
reservation = _load_json_object(path, "agent invocation reservation")
|
|
905
|
+
if (
|
|
906
|
+
set(reservation) == _TRANSLATOR_RESERVATION_KEYS
|
|
907
|
+
and reservation.get("schemaVersion") == "2.0"
|
|
908
|
+
and reservation.get("executionIdentityVersion") == 2
|
|
909
|
+
and reservation.get("assignmentRef") == "translator"
|
|
910
|
+
and reservation.get("audience") == "translator"
|
|
911
|
+
and reservation.get("dispatchKind") == "translator"
|
|
912
|
+
and reservation.get("dutyId") == "translator"
|
|
913
|
+
and reservation.get("participantRef") == execution.get("participantRef")
|
|
914
|
+
and reservation.get("roleExecutionRef") == execution.get("roleExecutionRef")
|
|
915
|
+
and _build_dispatch_id(
|
|
916
|
+
str(reservation.get("invocationId") or ""),
|
|
917
|
+
int(reservation.get("attempt") or 0),
|
|
918
|
+
) not in dispatched_ids
|
|
919
|
+
):
|
|
920
|
+
candidates.append(reservation)
|
|
921
|
+
if len(candidates) != 1:
|
|
922
|
+
raise DispatchError(
|
|
923
|
+
"translator dispatch requires exactly one canonical invocation reservation"
|
|
924
|
+
)
|
|
925
|
+
reservation = candidates[0]
|
|
926
|
+
prompt_path = _resolve_project_path(
|
|
927
|
+
project_root, _require_string(reservation, "promptPath")
|
|
928
|
+
)
|
|
929
|
+
metadata_path = _resolve_project_path(
|
|
930
|
+
project_root, _require_string(reservation, "metadataPath")
|
|
931
|
+
)
|
|
932
|
+
expected_metadata = prompt_path.with_name(prompt_path.name + ".meta.json")
|
|
933
|
+
if metadata_path != expected_metadata:
|
|
934
|
+
raise DispatchError("translator invocation metadata path is not prompt-adjacent")
|
|
935
|
+
metadata = _load_json_object(metadata_path, "agent invocation metadata")
|
|
936
|
+
invocation = _invocation_job_fields(metadata, prompt_path, manifest)
|
|
937
|
+
expected = {
|
|
938
|
+
"invocationId": invocation["invocation_id"],
|
|
939
|
+
"invocationRef": invocation["invocation_ref"],
|
|
940
|
+
"participantRef": invocation["participant_ref"],
|
|
941
|
+
"roleExecutionRef": invocation["role_execution_ref"],
|
|
942
|
+
"dutyId": invocation["duty_id"],
|
|
943
|
+
"attempt": invocation["attempt"],
|
|
944
|
+
}
|
|
945
|
+
if any(reservation.get(key) != value for key, value in expected.items()):
|
|
946
|
+
raise DispatchError("translator reservation and invocation metadata differ")
|
|
947
|
+
if (
|
|
948
|
+
invocation["audience"] != "translator"
|
|
949
|
+
or invocation["assignment_ref"] != "translator"
|
|
950
|
+
or metadata.get("dispatchKind") != "translator"
|
|
951
|
+
):
|
|
952
|
+
raise DispatchError("translator invocation identity is invalid")
|
|
953
|
+
result_path, worker_result_path = _translator_prompt_outputs(
|
|
954
|
+
project_root, prompt_path
|
|
955
|
+
)
|
|
956
|
+
if result_path == worker_result_path:
|
|
957
|
+
raise DispatchError(
|
|
958
|
+
"translator result and standard worker result paths must differ"
|
|
959
|
+
)
|
|
960
|
+
provider = _require_string(execution, "provider")
|
|
961
|
+
binding = execution.get("binding")
|
|
962
|
+
if not isinstance(binding, Mapping):
|
|
963
|
+
raise DispatchError("translator role execution has no binding")
|
|
964
|
+
return WorkerJob(
|
|
965
|
+
worker_id="translator",
|
|
966
|
+
provider=provider,
|
|
967
|
+
backend=options.default_backend,
|
|
968
|
+
project_root=project_root,
|
|
969
|
+
model_execution_value=_require_string(binding, "resolvedExecutionValue"),
|
|
970
|
+
wrapper_path=_resolve_wrapper(provider, workspace_root, options),
|
|
971
|
+
prompt_path=prompt_path,
|
|
972
|
+
result_path=result_path,
|
|
973
|
+
worker_result_path=worker_result_path,
|
|
974
|
+
completion_paths=(result_path, worker_result_path),
|
|
975
|
+
worktree_path=_worktree_path(manifest, active_context),
|
|
976
|
+
role="translator",
|
|
977
|
+
idle_timeout_seconds=options.idle_timeout_seconds,
|
|
978
|
+
dispatch_kind="translator",
|
|
979
|
+
**invocation,
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def _translator_prompt_outputs(
|
|
984
|
+
project_root: Path, prompt_path: Path,
|
|
985
|
+
) -> tuple[Path, Path]:
|
|
986
|
+
try:
|
|
987
|
+
lines = prompt_path.read_text(encoding="utf-8").splitlines()
|
|
988
|
+
except OSError as exc:
|
|
989
|
+
raise DispatchError(f"cannot read translator prompt: {exc}") from exc
|
|
990
|
+
values: list[str] = []
|
|
991
|
+
for header in ("Result Path", "Worker Result Path"):
|
|
992
|
+
prefix = f"**{header}:** "
|
|
993
|
+
matches = [line[len(prefix):].strip() for line in lines if line.startswith(prefix)]
|
|
994
|
+
if len(matches) != 1 or not matches[0]:
|
|
995
|
+
raise DispatchError(
|
|
996
|
+
f"translator prompt requires exactly one non-empty {prefix.strip()} header"
|
|
997
|
+
)
|
|
998
|
+
values.append(matches[0])
|
|
999
|
+
return (
|
|
1000
|
+
_resolve_project_path(project_root, values[0]),
|
|
1001
|
+
_resolve_project_path(project_root, values[1]),
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
|
|
513
1005
|
def _select_cli_wrapper_assignments(
|
|
514
1006
|
recommended: Sequence[str],
|
|
515
1007
|
requested_workers: Sequence[str],
|
|
@@ -588,7 +1080,7 @@ def _job_from_roster_worker(
|
|
|
588
1080
|
) -> WorkerJob:
|
|
589
1081
|
result_path = _resolve_project_path(project_root, fact.result_path)
|
|
590
1082
|
metadata = _agent_metadata(prompt_path)
|
|
591
|
-
invocation = _invocation_job_fields(metadata, prompt_path)
|
|
1083
|
+
invocation = _invocation_job_fields(metadata, prompt_path, manifest)
|
|
592
1084
|
return WorkerJob(
|
|
593
1085
|
worker_id=fact.worker_id,
|
|
594
1086
|
provider=fact.provider,
|
|
@@ -608,6 +1100,7 @@ def _job_from_roster_worker(
|
|
|
608
1100
|
role=fact.role,
|
|
609
1101
|
idle_timeout_seconds=options.idle_timeout_seconds,
|
|
610
1102
|
dispatch_kind=options.dispatch_kind,
|
|
1103
|
+
session_id=_dispatch_session_id(fact.provider),
|
|
611
1104
|
**invocation,
|
|
612
1105
|
)
|
|
613
1106
|
|
|
@@ -622,6 +1115,7 @@ def _agent_metadata(prompt_path: Path) -> Mapping[str, Any] | None:
|
|
|
622
1115
|
def _invocation_job_fields(
|
|
623
1116
|
metadata: Mapping[str, Any] | None,
|
|
624
1117
|
prompt_path: Path,
|
|
1118
|
+
manifest: Mapping[str, Any],
|
|
625
1119
|
) -> dict[str, Any]:
|
|
626
1120
|
if metadata is None:
|
|
627
1121
|
return {}
|
|
@@ -632,7 +1126,7 @@ def _invocation_job_fields(
|
|
|
632
1126
|
host_model_value = assignment.get("hostModelValue")
|
|
633
1127
|
if host_model_value is not None and not isinstance(host_model_value, str):
|
|
634
1128
|
raise DispatchError("agent invocation hostModelValue is invalid")
|
|
635
|
-
|
|
1129
|
+
fields = {
|
|
636
1130
|
"invocation_id": _require_string(metadata, "invocationId"),
|
|
637
1131
|
"audience": _require_string(metadata, "audience"),
|
|
638
1132
|
"assignment_ref": _require_string(metadata, "assignmentRef"),
|
|
@@ -647,6 +1141,37 @@ def _invocation_job_fields(
|
|
|
647
1141
|
"host_model_value": host_model_value,
|
|
648
1142
|
"enforcement_mode": "core-pre-dispatch",
|
|
649
1143
|
}
|
|
1144
|
+
schema = metadata.get("schemaVersion")
|
|
1145
|
+
identity_version = metadata.get("executionIdentityVersion")
|
|
1146
|
+
if schema == 1 and identity_version is None:
|
|
1147
|
+
return fields
|
|
1148
|
+
if schema != "2.0" or identity_version != 2:
|
|
1149
|
+
raise DispatchError("agent invocation metadata mixes v1 and v2 identity")
|
|
1150
|
+
participant_ref = _require_string(metadata, "participantRef")
|
|
1151
|
+
role_execution_ref = _require_string(metadata, "roleExecutionRef")
|
|
1152
|
+
role_executions = manifest.get("roleExecutions")
|
|
1153
|
+
execution = next((
|
|
1154
|
+
row for row in role_executions
|
|
1155
|
+
if isinstance(row, Mapping)
|
|
1156
|
+
and row.get("roleExecutionRef") == role_execution_ref
|
|
1157
|
+
), None) if isinstance(role_executions, list) else None
|
|
1158
|
+
if execution is None or execution.get("participantRef") != participant_ref:
|
|
1159
|
+
raise DispatchError("agent invocation role execution does not match run manifest")
|
|
1160
|
+
execution_label = _require_string(metadata, "executionLabel")
|
|
1161
|
+
if execution.get("executionLabel") != execution_label:
|
|
1162
|
+
raise DispatchError("agent invocation execution label does not match run manifest")
|
|
1163
|
+
attempt = metadata.get("attempt")
|
|
1164
|
+
if not isinstance(attempt, int) or isinstance(attempt, bool) or attempt < 1:
|
|
1165
|
+
raise DispatchError("agent invocation v2 attempt must be positive")
|
|
1166
|
+
fields.update({
|
|
1167
|
+
"participant_ref": participant_ref,
|
|
1168
|
+
"role_execution_ref": role_execution_ref,
|
|
1169
|
+
"execution_label": execution_label,
|
|
1170
|
+
"duty_id": _require_string(metadata, "dutyId"),
|
|
1171
|
+
"invocation_ref": _require_string(metadata, "invocationRef"),
|
|
1172
|
+
"attempt": attempt,
|
|
1173
|
+
})
|
|
1174
|
+
return fields
|
|
650
1175
|
|
|
651
1176
|
|
|
652
1177
|
def _materialize_roster_prompts(
|
|
@@ -668,7 +1193,7 @@ def _materialize_roster_prompts(
|
|
|
668
1193
|
project_root=project_root,
|
|
669
1194
|
run_manifest_path=manifest_path,
|
|
670
1195
|
runtime_root=workspace_root,
|
|
671
|
-
delivery_mode=
|
|
1196
|
+
delivery_mode=PromptDeliveryMode.EAGER_INCLUDE,
|
|
672
1197
|
workers=tuple(
|
|
673
1198
|
InitialPromptWorkerRequest(fact.worker_id, fact.model)
|
|
674
1199
|
for fact in worker_facts
|
|
@@ -696,12 +1221,6 @@ def _roster_worker_facts(
|
|
|
696
1221
|
)
|
|
697
1222
|
|
|
698
1223
|
|
|
699
|
-
def _delivery_mode(default_backend: str) -> PromptDeliveryMode:
|
|
700
|
-
if default_backend == BACKEND_TMUX_PANE:
|
|
701
|
-
return PromptDeliveryMode.LAZY_PATH_REFERENCE
|
|
702
|
-
return PromptDeliveryMode.EAGER_INCLUDE
|
|
703
|
-
|
|
704
|
-
|
|
705
1224
|
def _jobs_from_file(
|
|
706
1225
|
project_root: Path,
|
|
707
1226
|
workspace_root: Path,
|
|
@@ -726,34 +1245,50 @@ def _jobs_from_file(
|
|
|
726
1245
|
|
|
727
1246
|
|
|
728
1247
|
def _spawn_job(plan: DispatchPlan, job: WorkerJob, attempt: int) -> WorkerHandle:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1248
|
+
job = _prepare_job_attempt(plan, job, attempt)
|
|
1249
|
+
contract = _persisted_write_contract(plan, job)
|
|
1250
|
+
policies = (contract[0],) if contract else ()
|
|
1251
|
+
snapshot = None
|
|
1252
|
+
if contract is not None and contract[1].mutation_audit == "batch":
|
|
1253
|
+
snapshot_path = _mutation_snapshot_path(job)
|
|
1254
|
+
if snapshot_path.is_file():
|
|
1255
|
+
snapshot = MutationSnapshot.from_payload(
|
|
1256
|
+
_load_json_object(snapshot_path, "mutation audit snapshot")
|
|
1257
|
+
)
|
|
1258
|
+
else:
|
|
1259
|
+
snapshot = _mutation_snapshot(plan, policies, (job,))
|
|
1260
|
+
handle = replace(
|
|
1261
|
+
_start_job(plan, job),
|
|
1262
|
+
mutation_snapshot=snapshot,
|
|
1263
|
+
mutation_policies=policies,
|
|
1264
|
+
write_policy_digest=contract[0].digest if contract else "",
|
|
1265
|
+
write_enforcement=contract[1] if contract else None,
|
|
732
1266
|
)
|
|
733
|
-
handle = _start_job(plan, job)
|
|
734
1267
|
_record_dispatch(plan.team_state_path, handle, attempt, "running", "")
|
|
735
1268
|
_append_event(plan, "worker-dispatched", _attempt_details(job, attempt, handle))
|
|
736
1269
|
if handle.completed_process is not None:
|
|
737
1270
|
outcome = _outcome_from_completed(handle)
|
|
738
|
-
_finish_attempt(
|
|
1271
|
+
retry_allowed = _finish_attempt(
|
|
1272
|
+
plan,
|
|
1273
|
+
job,
|
|
1274
|
+
attempt,
|
|
1275
|
+
outcome,
|
|
1276
|
+
snapshot=snapshot,
|
|
1277
|
+
policies=policies,
|
|
1278
|
+
)
|
|
1279
|
+
handle = replace(handle, mutation_retry_allowed=retry_allowed)
|
|
739
1280
|
return handle
|
|
740
1281
|
|
|
741
1282
|
|
|
742
1283
|
def _spawn_cli_job_nonblocking(
|
|
743
1284
|
plan: DispatchPlan, job: WorkerJob, attempt: int,
|
|
744
1285
|
) -> WorkerHandle:
|
|
745
|
-
_transition_worker_status(
|
|
746
|
-
plan.team_state_path, job.worker_id, "in-progress", "",
|
|
747
|
-
model_execution_value=job.model_execution_value,
|
|
748
|
-
)
|
|
749
1286
|
process: subprocess.Popen[str] | None = None
|
|
750
1287
|
try:
|
|
751
1288
|
process = subprocess.Popen(job.command, cwd=plan.project_root, text=True)
|
|
752
1289
|
handle = WorkerHandle(
|
|
753
1290
|
job, "", None, status_path_for_prompt(job.prompt_path), "", process
|
|
754
1291
|
)
|
|
755
|
-
_record_dispatch(plan.team_state_path, handle, attempt, "running", "")
|
|
756
|
-
_append_event(plan, "worker-dispatched", _attempt_details(job, attempt, handle))
|
|
757
1292
|
return handle
|
|
758
1293
|
except (OSError, DispatchError):
|
|
759
1294
|
if process is not None and process.poll() is None:
|
|
@@ -762,16 +1297,219 @@ def _spawn_cli_job_nonblocking(
|
|
|
762
1297
|
raise
|
|
763
1298
|
|
|
764
1299
|
|
|
1300
|
+
def _prepare_job_attempt(
|
|
1301
|
+
plan: DispatchPlan, job: WorkerJob, attempt: int
|
|
1302
|
+
) -> WorkerJob:
|
|
1303
|
+
if job.has_execution_identity:
|
|
1304
|
+
_revalidate_job_role_execution(plan, job)
|
|
1305
|
+
job = _job_for_attempt(job, attempt)
|
|
1306
|
+
_record_execution_attempt(plan, job)
|
|
1307
|
+
_transition_job_status(
|
|
1308
|
+
plan,
|
|
1309
|
+
job,
|
|
1310
|
+
"in-progress",
|
|
1311
|
+
"",
|
|
1312
|
+
model_execution_value=job.model_execution_value,
|
|
1313
|
+
)
|
|
1314
|
+
return job
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def _persisted_write_contract(
|
|
1318
|
+
plan: DispatchPlan, job: WorkerJob
|
|
1319
|
+
) -> tuple[WritePolicy, WriteEnforcement] | None:
|
|
1320
|
+
if not job.has_execution_identity:
|
|
1321
|
+
return None
|
|
1322
|
+
manifest = read_execution_manifest(plan.manifest_path)
|
|
1323
|
+
invocation = next(
|
|
1324
|
+
(
|
|
1325
|
+
row for row in manifest.invocations
|
|
1326
|
+
if row.invocation_ref == job.invocation_ref
|
|
1327
|
+
),
|
|
1328
|
+
None,
|
|
1329
|
+
)
|
|
1330
|
+
if invocation is None:
|
|
1331
|
+
raise DispatchError("worker invocation write policy was not persisted")
|
|
1332
|
+
try:
|
|
1333
|
+
enforcement = write_enforcement_from_payload(
|
|
1334
|
+
invocation.write_enforcement
|
|
1335
|
+
)
|
|
1336
|
+
policy = replace(
|
|
1337
|
+
write_policy_from_payload(invocation.write_policy),
|
|
1338
|
+
maximum_boundary_precision=enforcement.boundary_precision,
|
|
1339
|
+
)
|
|
1340
|
+
except (TypeError, ValueError) as exc:
|
|
1341
|
+
raise DispatchError(f"persisted worker write policy is invalid: {exc}") from exc
|
|
1342
|
+
if policy.digest != invocation.write_policy_digest:
|
|
1343
|
+
raise DispatchError("persisted worker write policy digest changed")
|
|
1344
|
+
return policy, enforcement
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
def _mutation_snapshot(
|
|
1348
|
+
plan: DispatchPlan,
|
|
1349
|
+
policies: Sequence[WritePolicy],
|
|
1350
|
+
jobs: Sequence[WorkerJob],
|
|
1351
|
+
) -> MutationSnapshot:
|
|
1352
|
+
sidecars = tuple(_mutation_snapshot_path(job) for job in jobs)
|
|
1353
|
+
snapshot = ExecutionMutationAudit().snapshot(
|
|
1354
|
+
policies,
|
|
1355
|
+
orchestrator_paths=(
|
|
1356
|
+
plan.manifest_path,
|
|
1357
|
+
plan.team_state_path,
|
|
1358
|
+
Path(f"{plan.team_state_path}.lock"),
|
|
1359
|
+
plan.lead_events_path,
|
|
1360
|
+
plan.lead_events_path.with_name(
|
|
1361
|
+
f".{plan.lead_events_path.name}.lock"
|
|
1362
|
+
),
|
|
1363
|
+
*sidecars,
|
|
1364
|
+
),
|
|
1365
|
+
)
|
|
1366
|
+
for path in sidecars:
|
|
1367
|
+
write_json_atomic(path, snapshot.to_payload())
|
|
1368
|
+
return snapshot
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def _mutation_snapshot_path(job: WorkerJob) -> Path:
|
|
1372
|
+
return job.prompt_path.with_suffix(job.prompt_path.suffix + ".mutation-audit.json")
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def _record_execution_attempt(plan: DispatchPlan, job: WorkerJob) -> None:
|
|
1376
|
+
task_key = _require_string(plan.manifest, "taskKey")
|
|
1377
|
+
write_policy, write_enforcement = _canonical_write_contract(plan, job)
|
|
1378
|
+
record_invocation_attempt(
|
|
1379
|
+
plan.manifest_path,
|
|
1380
|
+
Invocation(
|
|
1381
|
+
invocation_ref=job.invocation_ref,
|
|
1382
|
+
participant_ref=job.participant_ref,
|
|
1383
|
+
role_execution_ref=job.role_execution_ref,
|
|
1384
|
+
source_invocation_ref=None,
|
|
1385
|
+
recovery_ref=None,
|
|
1386
|
+
duty_id=job.duty_id,
|
|
1387
|
+
dispatch_kind=job.dispatch_kind,
|
|
1388
|
+
round=_dispatch_round(job.dispatch_kind),
|
|
1389
|
+
input_digest=job.prompt_digest,
|
|
1390
|
+
write_policy=write_policy.to_payload(),
|
|
1391
|
+
write_policy_digest=write_policy.digest,
|
|
1392
|
+
write_enforcement=write_enforcement.to_payload(),
|
|
1393
|
+
),
|
|
1394
|
+
Attempt(
|
|
1395
|
+
invocation_ref=job.invocation_ref,
|
|
1396
|
+
attempt=job.attempt,
|
|
1397
|
+
started_at=_utc_now(),
|
|
1398
|
+
finished_at=None,
|
|
1399
|
+
status="started",
|
|
1400
|
+
result_path=None,
|
|
1401
|
+
error_path=None,
|
|
1402
|
+
change_summary={},
|
|
1403
|
+
evidence_seal_ref=None,
|
|
1404
|
+
),
|
|
1405
|
+
task_key=task_key,
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def _canonical_write_contract(
|
|
1410
|
+
plan: DispatchPlan, job: WorkerJob
|
|
1411
|
+
) -> tuple[WritePolicy, WriteEnforcement]:
|
|
1412
|
+
manifest = read_execution_manifest(plan.manifest_path)
|
|
1413
|
+
execution = next(
|
|
1414
|
+
(
|
|
1415
|
+
row for row in manifest.role_executions
|
|
1416
|
+
if row.role_execution_ref == job.role_execution_ref
|
|
1417
|
+
),
|
|
1418
|
+
None,
|
|
1419
|
+
)
|
|
1420
|
+
if execution is None or execution.binding is None:
|
|
1421
|
+
raise DispatchError("worker write policy has no role execution binding")
|
|
1422
|
+
capability = execution.binding.worker_write_capability
|
|
1423
|
+
if capability is None:
|
|
1424
|
+
raise DispatchError("worker write policy has no runner write capability")
|
|
1425
|
+
artifacts = _worker_artifact_paths(plan, job)
|
|
1426
|
+
worktree = Path(job.worktree_path) if job.worktree_path else None
|
|
1427
|
+
try:
|
|
1428
|
+
return build_invocation_write_contract(
|
|
1429
|
+
role=execution.role,
|
|
1430
|
+
project_root=plan.project_root,
|
|
1431
|
+
worktree=worktree,
|
|
1432
|
+
artifact_paths=artifacts,
|
|
1433
|
+
maximum_precision=capability.max_boundary_precision,
|
|
1434
|
+
planned_paths=(
|
|
1435
|
+
planned_paths_from_run_manifest(plan.project_root, plan.manifest)
|
|
1436
|
+
if execution.role == "implementer"
|
|
1437
|
+
else ()
|
|
1438
|
+
),
|
|
1439
|
+
auxiliary_roots=verifier_extra_dirs(execution.role),
|
|
1440
|
+
validated_auxiliary_roots=verifier_extra_dirs(execution.role),
|
|
1441
|
+
)
|
|
1442
|
+
except (OSError, ValueError) as exc:
|
|
1443
|
+
raise DispatchError(f"worker write policy is invalid: {exc}") from exc
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def _worker_artifact_paths(plan: DispatchPlan, job: WorkerJob) -> tuple[Path, ...]:
|
|
1447
|
+
active_context = _load_optional_json(
|
|
1448
|
+
plan.project_root, plan.manifest.get("activeRunContextPath")
|
|
1449
|
+
)
|
|
1450
|
+
paths = {
|
|
1451
|
+
job.result_path,
|
|
1452
|
+
job.worker_result_path,
|
|
1453
|
+
*job.completion_paths,
|
|
1454
|
+
Path(audit_sidecar_rel(str(job.worker_result_path))),
|
|
1455
|
+
status_path_for_prompt(job.prompt_path),
|
|
1456
|
+
job.prompt_path.with_suffix(job.prompt_path.suffix + ".log"),
|
|
1457
|
+
}
|
|
1458
|
+
error_logs = active_context.get("errorLogs")
|
|
1459
|
+
if isinstance(error_logs, Mapping):
|
|
1460
|
+
sidecars = error_logs.get("sidecarsByWorkerId")
|
|
1461
|
+
if isinstance(sidecars, Mapping):
|
|
1462
|
+
value = _string_value(sidecars.get(job.worker_id))
|
|
1463
|
+
if value:
|
|
1464
|
+
paths.add(_resolve_project_path(plan.project_root, value))
|
|
1465
|
+
return tuple(sorted(paths, key=str))
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def _job_for_attempt(job: WorkerJob, attempt: int) -> WorkerJob:
|
|
1471
|
+
if attempt == job.attempt:
|
|
1472
|
+
return job
|
|
1473
|
+
try:
|
|
1474
|
+
retry = materialize_retry_invocation(
|
|
1475
|
+
job.prompt_metadata_path,
|
|
1476
|
+
project_root=job.project_root,
|
|
1477
|
+
attempt=attempt,
|
|
1478
|
+
)
|
|
1479
|
+
except AgentInvocationError as exc:
|
|
1480
|
+
raise DispatchError(f"cannot materialize retry invocation: {exc}") from exc
|
|
1481
|
+
if retry.invocation_ref != job.invocation_ref:
|
|
1482
|
+
raise DispatchError("retry invocation reference changed")
|
|
1483
|
+
return replace(
|
|
1484
|
+
job,
|
|
1485
|
+
prompt_path=retry.prompt_path,
|
|
1486
|
+
prompt_metadata_path=retry.metadata_path,
|
|
1487
|
+
invocation_id=retry.invocation_id,
|
|
1488
|
+
attempt=retry.attempt,
|
|
1489
|
+
)
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
def _dispatch_round(dispatch_kind: str) -> int:
|
|
1493
|
+
prefix = "reverify-r"
|
|
1494
|
+
if not dispatch_kind.startswith(prefix):
|
|
1495
|
+
return 1
|
|
1496
|
+
suffix = dispatch_kind[len(prefix):]
|
|
1497
|
+
if not suffix.isdigit() or int(suffix) < 1:
|
|
1498
|
+
raise DispatchError(f"invalid reverify dispatch kind: {dispatch_kind}")
|
|
1499
|
+
return int(suffix)
|
|
1500
|
+
|
|
1501
|
+
|
|
765
1502
|
def _dispatch_job_with_retry(plan: DispatchPlan, job: WorkerJob) -> int:
|
|
766
1503
|
for attempt in range(1, MAX_WORKER_ATTEMPTS + 1):
|
|
767
1504
|
handle = _spawn_job(plan, job, attempt)
|
|
768
1505
|
if handle.completed_process is None:
|
|
769
1506
|
return await_dispatches(plan, timeout_seconds=None)
|
|
770
1507
|
outcome = _outcome_from_completed(handle)
|
|
771
|
-
if
|
|
1508
|
+
if _job_terminal_status(plan, job, attempt) == "completed":
|
|
772
1509
|
return 0
|
|
773
|
-
if _should_retry(outcome, attempt):
|
|
1510
|
+
if _should_retry(outcome, attempt) and handle.mutation_retry_allowed:
|
|
774
1511
|
_append_event(plan, "worker-retry-scheduled", _retry_details(job, attempt, outcome))
|
|
1512
|
+
job = _job_for_next_attempt(job)
|
|
775
1513
|
continue
|
|
776
1514
|
return outcome.returncode or 1
|
|
777
1515
|
return 1
|
|
@@ -780,8 +1518,6 @@ def _dispatch_job_with_retry(plan: DispatchPlan, job: WorkerJob) -> int:
|
|
|
780
1518
|
def _start_job(plan: DispatchPlan, job: WorkerJob) -> WorkerHandle:
|
|
781
1519
|
if job.backend == BACKEND_CLI_WRAPPER:
|
|
782
1520
|
return _run_cli_wrapper(plan, job, "")
|
|
783
|
-
if job.backend == BACKEND_TMUX_PANE:
|
|
784
|
-
return _start_tmux_or_degrade(plan, job)
|
|
785
1521
|
if job.backend == BACKEND_CMUX_PANE:
|
|
786
1522
|
return _start_cmux_or_degrade(plan, job)
|
|
787
1523
|
raise DispatchError(f"unsupported worker backend: {job.backend}")
|
|
@@ -797,7 +1533,11 @@ def _start_cmux_or_degrade(plan: DispatchPlan, job: WorkerJob) -> WorkerHandle:
|
|
|
797
1533
|
workspace=workspace,
|
|
798
1534
|
cwd=plan.project_root,
|
|
799
1535
|
command=job.command,
|
|
800
|
-
title=
|
|
1536
|
+
title=(
|
|
1537
|
+
job.execution_label
|
|
1538
|
+
if job.has_execution_identity
|
|
1539
|
+
else f"{job.worker_id}-worker"
|
|
1540
|
+
),
|
|
801
1541
|
owned_surface_ids=_opened_cmux_surfaces(plan.team_state_path),
|
|
802
1542
|
)
|
|
803
1543
|
except (RuntimeError, OSError, subprocess.SubprocessError):
|
|
@@ -829,23 +1569,6 @@ def _opened_cmux_surfaces(team_state_path: Path) -> tuple[str, ...]:
|
|
|
829
1569
|
return tuple(opened)
|
|
830
1570
|
|
|
831
1571
|
|
|
832
|
-
def _start_tmux_or_degrade(plan: DispatchPlan, job: WorkerJob) -> WorkerHandle:
|
|
833
|
-
lead_pane = tmux.resolve_caller_pane()
|
|
834
|
-
if not lead_pane:
|
|
835
|
-
return _run_cli_wrapper(plan, job, BACKEND_TMUX_PANE)
|
|
836
|
-
try:
|
|
837
|
-
pane_id = tmux.split_worker_pane(
|
|
838
|
-
target_pane=lead_pane,
|
|
839
|
-
cwd=plan.project_root,
|
|
840
|
-
command=job.command,
|
|
841
|
-
title=f"{job.worker_id}-worker",
|
|
842
|
-
run_dir=_run_dir(plan),
|
|
843
|
-
)
|
|
844
|
-
except RuntimeError:
|
|
845
|
-
return _run_cli_wrapper(plan, job, BACKEND_TMUX_PANE)
|
|
846
|
-
return WorkerHandle(job, pane_id, None, status_path_for_prompt(job.prompt_path), "")
|
|
847
|
-
|
|
848
|
-
|
|
849
1572
|
def _refuse_when_cmux_is_walled_off() -> None:
|
|
850
1573
|
"""Stop rather than degrade when a sandbox stands between okstra and cmux.
|
|
851
1574
|
|
|
@@ -933,16 +1656,17 @@ def _advance_running_dispatches(plan: DispatchPlan, records: Sequence[Mapping[st
|
|
|
933
1656
|
if status is None or not status.is_terminal:
|
|
934
1657
|
continue
|
|
935
1658
|
outcome = _outcome_from_status(record, status)
|
|
936
|
-
|
|
1659
|
+
retry_allowed = _finish_record(plan, record, outcome)
|
|
1660
|
+
if (
|
|
1661
|
+
_should_retry(outcome, int(record.get("attempt", 1)))
|
|
1662
|
+
and retry_allowed
|
|
1663
|
+
):
|
|
937
1664
|
_retry_from_record(plan, record, outcome)
|
|
938
|
-
continue
|
|
939
|
-
_finish_record(plan, record, outcome)
|
|
940
1665
|
|
|
941
1666
|
|
|
942
1667
|
def _retry_from_record(
|
|
943
1668
|
plan: DispatchPlan, record: Mapping[str, Any], outcome: WorkerOutcome
|
|
944
1669
|
) -> None:
|
|
945
|
-
worker_id = _require_string(record, "workerId")
|
|
946
1670
|
attempt = int(record.get("attempt", 1))
|
|
947
1671
|
job = _job_from_record(plan.project_root, record)
|
|
948
1672
|
reason = "required worker artifact was not produced"
|
|
@@ -950,22 +1674,47 @@ def _retry_from_record(
|
|
|
950
1674
|
# `team-contract` counts the first attempt's failure as a recorded
|
|
951
1675
|
# `cli-failure`; a retry that succeeds settles `completed` and would
|
|
952
1676
|
# otherwise leave no trace that anything had to be re-run.
|
|
953
|
-
details
|
|
1677
|
+
details = _event_execution_identity(job, attempt)
|
|
954
1678
|
details["errorLogAppend"] = _record_wrapper_failure(
|
|
955
1679
|
plan, job, attempt, outcome, reason
|
|
956
1680
|
)
|
|
957
1681
|
_append_event(plan, "worker-retry-scheduled", details)
|
|
958
|
-
|
|
1682
|
+
# The restored job settles attempt 1 above and must keep its session; only
|
|
1683
|
+
# the attempt being opened here gets a new one.
|
|
1684
|
+
_spawn_job(plan, _job_for_next_attempt(job), attempt + 1)
|
|
959
1685
|
|
|
960
1686
|
|
|
961
|
-
def _finish_attempt(
|
|
1687
|
+
def _finish_attempt(
|
|
1688
|
+
plan: DispatchPlan,
|
|
1689
|
+
job: WorkerJob,
|
|
1690
|
+
attempt: int,
|
|
1691
|
+
outcome: WorkerOutcome,
|
|
1692
|
+
*,
|
|
1693
|
+
snapshot: MutationSnapshot | None = None,
|
|
1694
|
+
policies: Sequence[WritePolicy] = (),
|
|
1695
|
+
) -> bool:
|
|
1696
|
+
mutation = _audit_attempt(job, outcome, snapshot, policies)
|
|
1697
|
+
if mutation is not None and mutation.status in {
|
|
1698
|
+
"contract-failed-unattributed",
|
|
1699
|
+
"mutation-present-unresolved",
|
|
1700
|
+
}:
|
|
1701
|
+
reason = "; ".join(mutation.violations) or mutation.status
|
|
1702
|
+
_transition_job_status(plan, job, "error", reason)
|
|
1703
|
+
_update_dispatch_status(
|
|
1704
|
+
plan.team_state_path, job, attempt, "error", reason
|
|
1705
|
+
)
|
|
1706
|
+
details = _failure_details(job, attempt, outcome, reason)
|
|
1707
|
+
details["mutationAudit"] = mutation.change_summary()
|
|
1708
|
+
_append_event(plan, "worker-failed", details)
|
|
1709
|
+
_finish_manifest_attempt(plan, job, attempt, mutation, None)
|
|
1710
|
+
return False
|
|
962
1711
|
settlement = _settle(plan, job, attempt, outcome)
|
|
963
1712
|
if settlement.completed:
|
|
964
1713
|
result_link = _link_result(plan, job, attempt)
|
|
965
1714
|
post_process = _post_process_report_writer_result(plan, job)
|
|
966
1715
|
if not post_process["ok"]:
|
|
967
1716
|
reason = _require_string(post_process, "reason")
|
|
968
|
-
|
|
1717
|
+
_transition_job_status(plan, job, "error", reason)
|
|
969
1718
|
_update_dispatch_status(
|
|
970
1719
|
plan.team_state_path,
|
|
971
1720
|
job,
|
|
@@ -982,10 +1731,9 @@ def _finish_attempt(plan: DispatchPlan, job: WorkerJob, attempt: int, outcome: W
|
|
|
982
1731
|
)
|
|
983
1732
|
details["postProcessing"] = post_process["steps"]
|
|
984
1733
|
_append_event(plan, "worker-failed", details)
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
)
|
|
1734
|
+
_finish_manifest_attempt(plan, job, attempt, mutation, None)
|
|
1735
|
+
return False
|
|
1736
|
+
_transition_job_status(plan, job, "completed", "")
|
|
989
1737
|
_update_dispatch_status(
|
|
990
1738
|
plan.team_state_path,
|
|
991
1739
|
job,
|
|
@@ -998,16 +1746,148 @@ def _finish_attempt(plan: DispatchPlan, job: WorkerJob, attempt: int, outcome: W
|
|
|
998
1746
|
details["resultLink"] = result_link
|
|
999
1747
|
if settlement.error_log_append is not None:
|
|
1000
1748
|
details["errorLogAppend"] = settlement.error_log_append
|
|
1749
|
+
if mutation is not None:
|
|
1750
|
+
details["mutationAudit"] = mutation.change_summary()
|
|
1001
1751
|
_append_event(plan, "worker-result-collected", details)
|
|
1002
|
-
|
|
1752
|
+
_finish_manifest_attempt(
|
|
1753
|
+
plan, job, attempt, mutation, job.worker_result_path
|
|
1754
|
+
)
|
|
1755
|
+
return False
|
|
1003
1756
|
reason = settlement.reason
|
|
1004
1757
|
status = "timeout" if outcome.timeout else "error"
|
|
1005
|
-
|
|
1758
|
+
_transition_job_status(plan, job, status, reason)
|
|
1006
1759
|
_update_dispatch_status(plan.team_state_path, job, attempt, status, reason)
|
|
1007
1760
|
details = _failure_details(job, attempt, outcome, reason)
|
|
1008
1761
|
if settlement.error_log_append is not None:
|
|
1009
1762
|
details["errorLogAppend"] = settlement.error_log_append
|
|
1763
|
+
if mutation is not None:
|
|
1764
|
+
details["mutationAudit"] = mutation.change_summary()
|
|
1010
1765
|
_append_event(plan, "worker-failed", details)
|
|
1766
|
+
_finish_manifest_attempt(plan, job, attempt, mutation, None)
|
|
1767
|
+
return mutation.retry_allowed if mutation is not None else True
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
def _audit_attempt(
|
|
1771
|
+
job: WorkerJob,
|
|
1772
|
+
outcome: WorkerOutcome,
|
|
1773
|
+
snapshot: MutationSnapshot | None,
|
|
1774
|
+
policies: Sequence[WritePolicy],
|
|
1775
|
+
) -> MutationAuditResult | None:
|
|
1776
|
+
if not job.has_execution_identity or not policies:
|
|
1777
|
+
return None
|
|
1778
|
+
succeeded = outcome.returncode == 0 and not outcome.timeout
|
|
1779
|
+
result_present = not outcome.missing_completion_paths
|
|
1780
|
+
if snapshot is None:
|
|
1781
|
+
status = "ok" if succeeded and result_present else "failed-no-mutation"
|
|
1782
|
+
return MutationAuditResult(
|
|
1783
|
+
status=status,
|
|
1784
|
+
changed_paths=(),
|
|
1785
|
+
source_changed=False,
|
|
1786
|
+
git_changed=False,
|
|
1787
|
+
attribution="call",
|
|
1788
|
+
retry_allowed=status == "failed-no-mutation",
|
|
1789
|
+
violations=(),
|
|
1790
|
+
before_digest="",
|
|
1791
|
+
after_digest="",
|
|
1792
|
+
git_projection={},
|
|
1793
|
+
)
|
|
1794
|
+
return ExecutionMutationAudit().compare(
|
|
1795
|
+
snapshot,
|
|
1796
|
+
policies,
|
|
1797
|
+
out_of_plan_edits=_out_of_plan_edit_paths(job.result_path),
|
|
1798
|
+
attempt_succeeded=succeeded,
|
|
1799
|
+
result_present=result_present,
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
def _out_of_plan_edit_paths(result_path: Path) -> tuple[str, ...]:
|
|
1804
|
+
if not result_path.is_file() or result_path.suffix != ".json":
|
|
1805
|
+
return ()
|
|
1806
|
+
try:
|
|
1807
|
+
payload = json.loads(result_path.read_text(encoding="utf-8"))
|
|
1808
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
1809
|
+
return ()
|
|
1810
|
+
implementation = payload.get("implementation") if isinstance(payload, Mapping) else None
|
|
1811
|
+
rows = implementation.get("outOfPlanEdits") if isinstance(implementation, Mapping) else None
|
|
1812
|
+
if not isinstance(rows, list):
|
|
1813
|
+
return ()
|
|
1814
|
+
return tuple(
|
|
1815
|
+
str(row["file"])
|
|
1816
|
+
for row in rows
|
|
1817
|
+
if isinstance(row, Mapping)
|
|
1818
|
+
and isinstance(row.get("file"), str)
|
|
1819
|
+
and row["file"]
|
|
1820
|
+
)
|
|
1821
|
+
|
|
1822
|
+
|
|
1823
|
+
def _finish_manifest_attempt(
|
|
1824
|
+
plan: DispatchPlan,
|
|
1825
|
+
job: WorkerJob,
|
|
1826
|
+
attempt: int,
|
|
1827
|
+
mutation: MutationAuditResult | None,
|
|
1828
|
+
result_path: Path | None,
|
|
1829
|
+
) -> None:
|
|
1830
|
+
if mutation is None:
|
|
1831
|
+
return
|
|
1832
|
+
manifest = read_execution_manifest(plan.manifest_path)
|
|
1833
|
+
existing = next(
|
|
1834
|
+
(
|
|
1835
|
+
row for row in manifest.attempts
|
|
1836
|
+
if row.invocation_ref == job.invocation_ref
|
|
1837
|
+
and row.attempt == attempt
|
|
1838
|
+
),
|
|
1839
|
+
None,
|
|
1840
|
+
)
|
|
1841
|
+
expected_result = str(result_path) if result_path is not None else None
|
|
1842
|
+
if existing is not None and existing.finished_at is not None:
|
|
1843
|
+
if (
|
|
1844
|
+
existing.status == mutation.status
|
|
1845
|
+
and existing.result_path == expected_result
|
|
1846
|
+
and dict(existing.change_summary) == mutation.change_summary()
|
|
1847
|
+
):
|
|
1848
|
+
return
|
|
1849
|
+
raise DispatchError("attempt terminal mutation result drift")
|
|
1850
|
+
finish_attempt_mutation(
|
|
1851
|
+
plan.manifest_path,
|
|
1852
|
+
invocation_ref=job.invocation_ref,
|
|
1853
|
+
attempt=attempt,
|
|
1854
|
+
finished_at=_utc_now(),
|
|
1855
|
+
status=mutation.status,
|
|
1856
|
+
result_path=expected_result,
|
|
1857
|
+
error_path=None,
|
|
1858
|
+
change_summary=mutation.change_summary(),
|
|
1859
|
+
task_key=_require_string(plan.manifest, "taskKey"),
|
|
1860
|
+
)
|
|
1861
|
+
_seal_finished_attempt(plan, job, attempt, mutation)
|
|
1862
|
+
|
|
1863
|
+
|
|
1864
|
+
def _seal_finished_attempt(plan, job, attempt, mutation) -> None:
|
|
1865
|
+
from .attempt_evidence import TerminalObservation, finalize_attempt
|
|
1866
|
+
|
|
1867
|
+
events = job.prompt_path.with_name(job.prompt_path.name + ".host-events.jsonl")
|
|
1868
|
+
if not events.is_file() or not job.has_execution_identity:
|
|
1869
|
+
return
|
|
1870
|
+
artifacts = tuple(
|
|
1871
|
+
path for path in (job.result_path, job.worker_result_path) if path
|
|
1872
|
+
)
|
|
1873
|
+
try:
|
|
1874
|
+
finalize_attempt(
|
|
1875
|
+
plan.manifest_path,
|
|
1876
|
+
_build_dispatch_id(job.invocation_ref, attempt),
|
|
1877
|
+
TerminalObservation(
|
|
1878
|
+
status=mutation.status,
|
|
1879
|
+
source_diff_digest=mutation.change_summary().get("beforeDigest", ""),
|
|
1880
|
+
git_projection_digest=mutation.change_summary().get("afterDigest", ""),
|
|
1881
|
+
changed_paths=tuple(mutation.changed_paths),
|
|
1882
|
+
artifact_paths=artifacts,
|
|
1883
|
+
host_event_stream_path=events,
|
|
1884
|
+
project_root=plan.project_root,
|
|
1885
|
+
worktree=Path(job.worktree_path) if job.worktree_path else None,
|
|
1886
|
+
),
|
|
1887
|
+
task_key=_require_string(plan.manifest, "taskKey"),
|
|
1888
|
+
)
|
|
1889
|
+
except (OSError, ValueError):
|
|
1890
|
+
return
|
|
1011
1891
|
|
|
1012
1892
|
|
|
1013
1893
|
def _link_result(
|
|
@@ -1038,7 +1918,7 @@ def _link_result(
|
|
|
1038
1918
|
_link_agent_dispatch_result(
|
|
1039
1919
|
project_root=plan.project_root,
|
|
1040
1920
|
run_manifest_path=plan.manifest_path,
|
|
1041
|
-
dispatch_id=
|
|
1921
|
+
dispatch_id=_build_dispatch_id(job.invocation_id, attempt),
|
|
1042
1922
|
result_path=job.worker_result_path,
|
|
1043
1923
|
)
|
|
1044
1924
|
except (DispatchError, OSError) as exc:
|
|
@@ -1047,9 +1927,34 @@ def _link_result(
|
|
|
1047
1927
|
return link
|
|
1048
1928
|
|
|
1049
1929
|
|
|
1050
|
-
def _finish_record(
|
|
1930
|
+
def _finish_record(
|
|
1931
|
+
plan: DispatchPlan,
|
|
1932
|
+
record: Mapping[str, Any],
|
|
1933
|
+
outcome: WorkerOutcome,
|
|
1934
|
+
) -> bool:
|
|
1051
1935
|
job = _job_from_record(plan.project_root, record)
|
|
1052
|
-
|
|
1936
|
+
snapshot = _snapshot_from_record(record)
|
|
1937
|
+
contract = _persisted_write_contract(plan, job)
|
|
1938
|
+
policies = (contract[0],) if contract is not None else ()
|
|
1939
|
+
return _finish_attempt(
|
|
1940
|
+
plan,
|
|
1941
|
+
job,
|
|
1942
|
+
int(record.get("attempt", 1)),
|
|
1943
|
+
outcome,
|
|
1944
|
+
snapshot=snapshot,
|
|
1945
|
+
policies=policies,
|
|
1946
|
+
)
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
def _snapshot_from_record(record: Mapping[str, Any]) -> MutationSnapshot | None:
|
|
1950
|
+
value = _string_value(record.get("mutationAuditSnapshotPath"))
|
|
1951
|
+
if not value:
|
|
1952
|
+
return None
|
|
1953
|
+
payload = _load_json_object(Path(value), "mutation audit snapshot")
|
|
1954
|
+
try:
|
|
1955
|
+
return MutationSnapshot.from_payload(payload)
|
|
1956
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
1957
|
+
raise DispatchError("mutation audit snapshot is invalid") from exc
|
|
1053
1958
|
|
|
1054
1959
|
|
|
1055
1960
|
@dataclass(frozen=True)
|
|
@@ -1087,7 +1992,11 @@ def _settle(
|
|
|
1087
1992
|
reason = _failure_reason(outcome)
|
|
1088
1993
|
completed = False
|
|
1089
1994
|
note = ""
|
|
1090
|
-
if
|
|
1995
|
+
if (
|
|
1996
|
+
outcome.returncode != SERVED_MODEL_MISMATCH_EXIT_CODE
|
|
1997
|
+
and not outcome.timeout
|
|
1998
|
+
and not outcome.missing_completion_paths
|
|
1999
|
+
):
|
|
1091
2000
|
audit_failures = _audit_sidecar_failures(job)
|
|
1092
2001
|
if audit_failures:
|
|
1093
2002
|
reason = (
|
|
@@ -1181,6 +2090,11 @@ def _record_wrapper_failure(
|
|
|
1181
2090
|
message=f"attempt {attempt}: {message}",
|
|
1182
2091
|
stderr_excerpt=_wrapper_log_tail(job),
|
|
1183
2092
|
context=None,
|
|
2093
|
+
identity=(
|
|
2094
|
+
_job_stored_identity(job, attempt)
|
|
2095
|
+
if job.has_execution_identity
|
|
2096
|
+
else None
|
|
2097
|
+
),
|
|
1184
2098
|
)
|
|
1185
2099
|
except (OSError, ValueError, TypeError, WorkerPromptHeaderError) as exc:
|
|
1186
2100
|
result["reason"] = f"{type(exc).__name__}: {exc}"
|
|
@@ -1189,6 +2103,17 @@ def _record_wrapper_failure(
|
|
|
1189
2103
|
return result
|
|
1190
2104
|
|
|
1191
2105
|
|
|
2106
|
+
def _job_stored_identity(job: WorkerJob, attempt: int) -> dict[str, Any]:
|
|
2107
|
+
"""Copy the job's stored execution refs. Do not infer them from labels."""
|
|
2108
|
+
return {
|
|
2109
|
+
"participantRef": job.participant_ref,
|
|
2110
|
+
"roleExecutionRef": job.role_execution_ref,
|
|
2111
|
+
"invocationRef": job.invocation_ref,
|
|
2112
|
+
"attempt": job.attempt or attempt,
|
|
2113
|
+
"executionLabel": job.execution_label,
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
|
|
1192
2117
|
def _error_log_agent(worker_id: str) -> str:
|
|
1193
2118
|
"""The error log's `--agent` enum value for a worker id.
|
|
1194
2119
|
|
|
@@ -1267,9 +2192,87 @@ def _post_process_report_writer_result(
|
|
|
1267
2192
|
return {"ok": False, "reason": str(exc), "steps": []}
|
|
1268
2193
|
|
|
1269
2194
|
|
|
1270
|
-
def
|
|
1271
|
-
|
|
1272
|
-
|
|
2195
|
+
def _projectionless_translator(plan: DispatchPlan, job: WorkerJob) -> bool:
|
|
2196
|
+
if not (
|
|
2197
|
+
job.has_execution_identity
|
|
2198
|
+
and job.role == "translator"
|
|
2199
|
+
and job.worker_id == "translator"
|
|
2200
|
+
and job.audience == "translator"
|
|
2201
|
+
and job.assignment_ref == "translator"
|
|
2202
|
+
and job.duty_id == "translator"
|
|
2203
|
+
and job.dispatch_kind == "translator"
|
|
2204
|
+
):
|
|
2205
|
+
return False
|
|
2206
|
+
execution = _canonical_translator_execution(plan.manifest)
|
|
2207
|
+
return bool(
|
|
2208
|
+
execution
|
|
2209
|
+
and execution.get("roleExecutionRef") == job.role_execution_ref
|
|
2210
|
+
and execution.get("participantRef") == job.participant_ref
|
|
2211
|
+
and execution.get("executionLabel") == job.execution_label
|
|
2212
|
+
and execution.get("provider") == job.provider
|
|
2213
|
+
)
|
|
2214
|
+
|
|
2215
|
+
|
|
2216
|
+
def _transition_job_status(
|
|
2217
|
+
plan: DispatchPlan,
|
|
2218
|
+
job: WorkerJob,
|
|
2219
|
+
status: str,
|
|
2220
|
+
reason: str,
|
|
2221
|
+
*,
|
|
2222
|
+
model_execution_value: str = "",
|
|
2223
|
+
) -> None:
|
|
2224
|
+
if not _projectionless_translator(plan, job):
|
|
2225
|
+
_transition_worker_status(
|
|
2226
|
+
plan.team_state_path,
|
|
2227
|
+
job.worker_id,
|
|
2228
|
+
status,
|
|
2229
|
+
reason,
|
|
2230
|
+
model_execution_value=model_execution_value,
|
|
2231
|
+
)
|
|
2232
|
+
return
|
|
2233
|
+
team_state = _load_json_object(plan.team_state_path, "team-state")
|
|
2234
|
+
workers = team_state.get("workers")
|
|
2235
|
+
if not isinstance(workers, list):
|
|
2236
|
+
raise DispatchError("team-state workers must be an array")
|
|
2237
|
+
if not any(
|
|
2238
|
+
isinstance(row, Mapping) and row.get("workerId") == job.worker_id
|
|
2239
|
+
for row in workers
|
|
2240
|
+
):
|
|
2241
|
+
return
|
|
2242
|
+
_transition_worker_status(
|
|
2243
|
+
plan.team_state_path,
|
|
2244
|
+
job.worker_id,
|
|
2245
|
+
status,
|
|
2246
|
+
reason,
|
|
2247
|
+
model_execution_value=model_execution_value,
|
|
2248
|
+
)
|
|
2249
|
+
|
|
2250
|
+
|
|
2251
|
+
def _job_terminal_status(
|
|
2252
|
+
plan: DispatchPlan, job: WorkerJob, attempt: int,
|
|
2253
|
+
) -> str:
|
|
2254
|
+
team_state = _load_json_object(plan.team_state_path, "team-state")
|
|
2255
|
+
workers = team_state.get("workers")
|
|
2256
|
+
if not isinstance(workers, list):
|
|
2257
|
+
raise DispatchError("team-state workers must be an array")
|
|
2258
|
+
matches = [
|
|
2259
|
+
row for row in workers
|
|
2260
|
+
if isinstance(row, Mapping) and row.get("workerId") == job.worker_id
|
|
2261
|
+
]
|
|
2262
|
+
if len(matches) == 1:
|
|
2263
|
+
return _string_value(matches[0].get("status"))
|
|
2264
|
+
if matches or not _projectionless_translator(plan, job):
|
|
2265
|
+
raise DispatchError(f"team-state has no unique workerId={job.worker_id}")
|
|
2266
|
+
dispatches = team_state.get("workerDispatches")
|
|
2267
|
+
records = [
|
|
2268
|
+
row for row in dispatches
|
|
2269
|
+
if isinstance(row, Mapping)
|
|
2270
|
+
and row.get("promptPath") == str(job.prompt_path)
|
|
2271
|
+
and row.get("attempt") == attempt
|
|
2272
|
+
] if isinstance(dispatches, list) else []
|
|
2273
|
+
if len(records) != 1:
|
|
2274
|
+
raise DispatchError("translator dispatch has no unique status record")
|
|
2275
|
+
return _string_value(records[0].get("status"))
|
|
1273
2276
|
|
|
1274
2277
|
|
|
1275
2278
|
def _record_dispatch(
|
|
@@ -1284,15 +2287,31 @@ def _record_dispatch(
|
|
|
1284
2287
|
handle.pane_id,
|
|
1285
2288
|
handle.degraded_from,
|
|
1286
2289
|
reason,
|
|
2290
|
+
mutation_snapshot_path=(
|
|
2291
|
+
_mutation_snapshot_path(handle.job)
|
|
2292
|
+
if handle.mutation_snapshot is not None
|
|
2293
|
+
else None
|
|
2294
|
+
),
|
|
2295
|
+
write_policy_digest=handle.write_policy_digest,
|
|
2296
|
+
write_enforcement=handle.write_enforcement,
|
|
1287
2297
|
),
|
|
1288
2298
|
)
|
|
1289
2299
|
|
|
1290
2300
|
|
|
1291
2301
|
def _dispatch_record(
|
|
1292
|
-
job: WorkerJob,
|
|
2302
|
+
job: WorkerJob,
|
|
2303
|
+
attempt: int,
|
|
2304
|
+
status: str,
|
|
2305
|
+
pane_id: str,
|
|
2306
|
+
degraded_from: str,
|
|
2307
|
+
reason: str = "",
|
|
2308
|
+
*,
|
|
2309
|
+
mutation_snapshot_path: Path | None = None,
|
|
2310
|
+
write_policy_digest: str = "",
|
|
2311
|
+
write_enforcement: WriteEnforcement | None = None,
|
|
1293
2312
|
) -> dict[str, Any]:
|
|
2313
|
+
has_execution_identity = job.has_execution_identity
|
|
1294
2314
|
record = {
|
|
1295
|
-
"workerId": job.worker_id,
|
|
1296
2315
|
"role": job.role,
|
|
1297
2316
|
"kind": job.dispatch_kind,
|
|
1298
2317
|
"attempt": attempt,
|
|
@@ -1300,6 +2319,10 @@ def _dispatch_record(
|
|
|
1300
2319
|
"provider": job.provider,
|
|
1301
2320
|
"status": status,
|
|
1302
2321
|
"paneId": pane_id,
|
|
2322
|
+
# The only handle on a pane worker's transcript: it runs as its own
|
|
2323
|
+
# `claude -p` process and leaves no `agentName` in its session jsonl, so
|
|
2324
|
+
# token collection resolves its usage by this value or not at all.
|
|
2325
|
+
"sessionId": job.session_id,
|
|
1303
2326
|
"promptPath": str(job.prompt_path),
|
|
1304
2327
|
"resultPath": str(job.result_path),
|
|
1305
2328
|
"workerResultPath": str(job.worker_result_path),
|
|
@@ -1317,7 +2340,7 @@ def _dispatch_record(
|
|
|
1317
2340
|
}
|
|
1318
2341
|
if job.invocation_id:
|
|
1319
2342
|
record.update({
|
|
1320
|
-
"dispatchId":
|
|
2343
|
+
"dispatchId": _build_dispatch_id(job.invocation_id, attempt),
|
|
1321
2344
|
"invocationId": job.invocation_id,
|
|
1322
2345
|
"audience": job.audience,
|
|
1323
2346
|
"assignmentRef": job.assignment_ref,
|
|
@@ -1326,11 +2349,32 @@ def _dispatch_record(
|
|
|
1326
2349
|
"hostModelValue": job.host_model_value,
|
|
1327
2350
|
"enforcementMode": job.enforcement_mode,
|
|
1328
2351
|
})
|
|
2352
|
+
if has_execution_identity:
|
|
2353
|
+
record.update({
|
|
2354
|
+
"schemaVersion": "2.0",
|
|
2355
|
+
"executionIdentityVersion": 2,
|
|
2356
|
+
"participantRef": job.participant_ref,
|
|
2357
|
+
"roleExecutionRef": job.role_execution_ref,
|
|
2358
|
+
"executionLabel": job.execution_label,
|
|
2359
|
+
"dutyId": job.duty_id,
|
|
2360
|
+
"invocationRef": job.invocation_ref,
|
|
2361
|
+
"writePolicyDigest": write_policy_digest,
|
|
2362
|
+
"writeEnforcement": (
|
|
2363
|
+
write_enforcement.to_payload()
|
|
2364
|
+
if write_enforcement is not None
|
|
2365
|
+
else None
|
|
2366
|
+
),
|
|
2367
|
+
"mutationAuditSnapshotPath": (
|
|
2368
|
+
str(mutation_snapshot_path) if mutation_snapshot_path else ""
|
|
2369
|
+
),
|
|
2370
|
+
})
|
|
2371
|
+
else:
|
|
2372
|
+
record["workerId"] = job.worker_id
|
|
1329
2373
|
return record
|
|
1330
2374
|
|
|
1331
2375
|
|
|
1332
2376
|
def _liveness_mode(backend: str) -> str:
|
|
1333
|
-
if backend in (BACKEND_CLI_WRAPPER,
|
|
2377
|
+
if backend in (BACKEND_CLI_WRAPPER, BACKEND_CMUX_PANE):
|
|
1334
2378
|
return LIVENESS_WRAPPER_STATUS
|
|
1335
2379
|
return LIVENESS_AUDIT_HEARTBEAT
|
|
1336
2380
|
|
|
@@ -1463,7 +2507,9 @@ def _relay_to_sidebar(plan: DispatchPlan, event_type: str, details: Mapping[str,
|
|
|
1463
2507
|
if plan.default_backend != BACKEND_CMUX_PANE:
|
|
1464
2508
|
return
|
|
1465
2509
|
workspace = cmux.resolve_lead_workspace()
|
|
1466
|
-
worker_id = str(
|
|
2510
|
+
worker_id = str(
|
|
2511
|
+
details.get("executionLabel") or details.get("workerId") or "worker"
|
|
2512
|
+
)
|
|
1467
2513
|
cmux.sidebar_log(
|
|
1468
2514
|
workspace,
|
|
1469
2515
|
f"{worker_id}: {event_type.removeprefix('worker-')}",
|
|
@@ -1494,13 +2540,16 @@ def _append_event(plan: DispatchPlan, event_type: str, details: Mapping[str, Any
|
|
|
1494
2540
|
|
|
1495
2541
|
|
|
1496
2542
|
def _attempt_details(job: WorkerJob, attempt: int, handle: WorkerHandle) -> dict[str, Any]:
|
|
1497
|
-
return {
|
|
2543
|
+
return {
|
|
2544
|
+
**_event_details(job, handle),
|
|
2545
|
+
**_event_execution_identity(job, attempt),
|
|
2546
|
+
"maxAttempts": MAX_WORKER_ATTEMPTS,
|
|
2547
|
+
}
|
|
1498
2548
|
|
|
1499
2549
|
|
|
1500
2550
|
def _event_details(job: WorkerJob, handle: WorkerHandle) -> dict[str, Any]:
|
|
1501
|
-
|
|
2551
|
+
details = {
|
|
1502
2552
|
"dispatchMode": BACKEND_CLI_WRAPPER if handle.degraded_from else job.backend,
|
|
1503
|
-
"workerId": job.worker_id,
|
|
1504
2553
|
"promptPath": str(job.prompt_path),
|
|
1505
2554
|
"resultPath": str(job.result_path),
|
|
1506
2555
|
"workerResultPath": str(job.worker_result_path),
|
|
@@ -1509,12 +2558,25 @@ def _event_details(job: WorkerJob, handle: WorkerHandle) -> dict[str, Any]:
|
|
|
1509
2558
|
"paneId": handle.pane_id,
|
|
1510
2559
|
"degradedFrom": handle.degraded_from,
|
|
1511
2560
|
}
|
|
2561
|
+
return details
|
|
2562
|
+
|
|
2563
|
+
|
|
2564
|
+
def _event_execution_identity(job: WorkerJob, attempt: int) -> dict[str, Any]:
|
|
2565
|
+
if job.has_execution_identity:
|
|
2566
|
+
return {
|
|
2567
|
+
"participantRef": job.participant_ref,
|
|
2568
|
+
"roleExecutionRef": job.role_execution_ref,
|
|
2569
|
+
"executionLabel": job.execution_label,
|
|
2570
|
+
"dutyId": job.duty_id,
|
|
2571
|
+
"invocationRef": job.invocation_ref,
|
|
2572
|
+
"attempt": attempt,
|
|
2573
|
+
}
|
|
2574
|
+
return {"workerId": job.worker_id, "attempt": attempt}
|
|
1512
2575
|
|
|
1513
2576
|
|
|
1514
2577
|
def _result_details(job: WorkerJob, attempt: int, outcome: WorkerOutcome) -> dict[str, Any]:
|
|
1515
2578
|
return {
|
|
1516
|
-
|
|
1517
|
-
"attempt": attempt,
|
|
2579
|
+
**_event_execution_identity(job, attempt),
|
|
1518
2580
|
"dispatchMode": BACKEND_CLI_WRAPPER if outcome.degraded_from else job.backend,
|
|
1519
2581
|
"missingCompletionPaths": [],
|
|
1520
2582
|
# A collected result can still come from a wrapper that exited non-zero
|
|
@@ -1533,8 +2595,7 @@ def _failure_details(
|
|
|
1533
2595
|
exit_code: int | None = None,
|
|
1534
2596
|
) -> dict[str, Any]:
|
|
1535
2597
|
return {
|
|
1536
|
-
|
|
1537
|
-
"attempt": attempt,
|
|
2598
|
+
**_event_execution_identity(job, attempt),
|
|
1538
2599
|
"maxAttempts": MAX_WORKER_ATTEMPTS,
|
|
1539
2600
|
"dispatchMode": BACKEND_CLI_WRAPPER if outcome.degraded_from else job.backend,
|
|
1540
2601
|
"exitCode": outcome.returncode if exit_code is None else exit_code,
|
|
@@ -1550,8 +2611,7 @@ def _failure_details(
|
|
|
1550
2611
|
|
|
1551
2612
|
def _retry_details(job: WorkerJob, attempt: int, outcome: WorkerOutcome) -> dict[str, Any]:
|
|
1552
2613
|
return {
|
|
1553
|
-
|
|
1554
|
-
"attempt": attempt,
|
|
2614
|
+
**_event_execution_identity(job, attempt),
|
|
1555
2615
|
"missingCompletionPaths": [str(path) for path in outcome.missing_completion_paths],
|
|
1556
2616
|
"reason": "required worker artifact was not produced",
|
|
1557
2617
|
}
|
|
@@ -1570,8 +2630,13 @@ def _outcome_from_status(record: Mapping[str, Any], status) -> WorkerOutcome:
|
|
|
1570
2630
|
|
|
1571
2631
|
|
|
1572
2632
|
def _job_from_record(project_root: Path, record: Mapping[str, Any]) -> WorkerJob:
|
|
2633
|
+
identity = _worker_execution_identity(record)
|
|
1573
2634
|
return WorkerJob(
|
|
1574
|
-
worker_id=
|
|
2635
|
+
worker_id=(
|
|
2636
|
+
_require_string(record, "workerId")
|
|
2637
|
+
if identity is None
|
|
2638
|
+
else _v2_worker_state_key(record)
|
|
2639
|
+
),
|
|
1575
2640
|
provider=_require_string(record, "provider"),
|
|
1576
2641
|
backend=BACKEND_CLI_WRAPPER,
|
|
1577
2642
|
project_root=project_root,
|
|
@@ -1585,6 +2650,10 @@ def _job_from_record(project_root: Path, record: Mapping[str, Any]) -> WorkerJob
|
|
|
1585
2650
|
role=_require_string(record, "role"),
|
|
1586
2651
|
idle_timeout_seconds=600,
|
|
1587
2652
|
dispatch_kind=_require_string(record, "kind"),
|
|
2653
|
+
# Restored, never re-issued: both callers rebuild a job from its record
|
|
2654
|
+
# to settle a dispatch that already ran, and a fresh id would settle it
|
|
2655
|
+
# against a session that never existed.
|
|
2656
|
+
session_id=_string_value(record.get("sessionId")),
|
|
1588
2657
|
invocation_id=_string_value(record.get("invocationId")),
|
|
1589
2658
|
audience=_string_value(record.get("audience")),
|
|
1590
2659
|
assignment_ref=_string_value(record.get("assignmentRef")),
|
|
@@ -1602,6 +2671,7 @@ def _job_from_record(project_root: Path, record: Mapping[str, Any]) -> WorkerJob
|
|
|
1602
2671
|
else None
|
|
1603
2672
|
),
|
|
1604
2673
|
enforcement_mode=_string_value(record.get("enforcementMode")),
|
|
2674
|
+
**(identity or {}),
|
|
1605
2675
|
)
|
|
1606
2676
|
|
|
1607
2677
|
|
|
@@ -1613,6 +2683,20 @@ def _should_retry(outcome: WorkerOutcome, attempt: int) -> bool:
|
|
|
1613
2683
|
return outcome.returncode == 0 and bool(outcome.missing_completion_paths) and attempt < MAX_WORKER_ATTEMPTS
|
|
1614
2684
|
|
|
1615
2685
|
|
|
2686
|
+
def _job_for_next_attempt(job: WorkerJob) -> WorkerJob:
|
|
2687
|
+
"""The same work as a new attempt, which means a new session.
|
|
2688
|
+
|
|
2689
|
+
The first attempt's process is dead; a retry starts another one. Carrying
|
|
2690
|
+
the id over would merge two processes into one attribution unit, and it
|
|
2691
|
+
would hand `claude --session-id` a value that is already taken — behaviour
|
|
2692
|
+
the CLI does not document. Every re-dispatch goes through here so no path
|
|
2693
|
+
can quietly keep the old session.
|
|
2694
|
+
|
|
2695
|
+
A provider with no session flag gets '' back, so this is a no-op for it.
|
|
2696
|
+
"""
|
|
2697
|
+
return replace(job, session_id=_dispatch_session_id(job.provider))
|
|
2698
|
+
|
|
2699
|
+
|
|
1616
2700
|
def _failure_reason(outcome: WorkerOutcome) -> str:
|
|
1617
2701
|
if outcome.timeout:
|
|
1618
2702
|
return "worker wrapper timed out"
|
|
@@ -1709,11 +2793,6 @@ def _reject_stale_schema_excerpt(
|
|
|
1709
2793
|
)
|
|
1710
2794
|
|
|
1711
2795
|
|
|
1712
|
-
def _run_dir(plan: DispatchPlan) -> Path:
|
|
1713
|
-
value = _string_value(plan.manifest.get("runDirectoryPath"))
|
|
1714
|
-
return _resolve_project_path(plan.project_root, value) if value else plan.team_state_path.parent.parent
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
2796
|
def _load_optional_json(project_root: Path, value: Any) -> dict[str, Any]:
|
|
1718
2797
|
if not isinstance(value, str) or not value.strip():
|
|
1719
2798
|
return {}
|