okstra 0.174.0 → 0.176.0
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/{src → dist}/commands/execute/team.mjs +29 -36
- 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/{src → dist}/commands/execute/worktree-status.mjs +52 -46
- 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/{src → dist}/commands/report/finalize.mjs +25 -32
- 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 +6 -2
- package/docs/architecture.md +29 -20
- package/docs/cli.md +40 -16
- package/docs/contributor-change-matrix.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -3
- package/docs/for-ai/skills/okstra-rollup.md +1 -0
- package/docs/project-structure-overview.md +72 -62
- package/docs/task-process/README.md +12 -8
- package/docs/task-process/common-flow.md +13 -16
- package/docs/task-process/error-analysis.md +9 -10
- package/docs/task-process/final-verification.md +7 -7
- package/docs/task-process/implementation-planning.md +9 -9
- package/docs/task-process/implementation.md +6 -6
- package/docs/task-process/release-handoff.md +8 -7
- package/docs/task-process/requirements-discovery.md +8 -8
- package/package.json +11 -5
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +2 -1
- 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 +3 -0
- package/runtime/bin/lib/okstra/interactive.sh +12 -6
- package/runtime/bin/lib/okstra/usage.sh +7 -6
- package/runtime/bin/okstra-error-log.py +27 -0
- package/runtime/bin/okstra-provider-exec.py +217 -5
- package/runtime/bin/okstra-spawn-followups.py +4 -2
- package/runtime/bin/okstra.sh +10 -0
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/lead/context-loader.md +1 -2
- package/runtime/prompts/lead/convergence.md +27 -18
- package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
- package/runtime/prompts/lead/plan-body-verification.md +10 -3
- package/runtime/prompts/lead/report-writer.md +15 -1
- package/runtime/prompts/lead/team-contract.md +17 -13
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +0 -3
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -7
- package/runtime/prompts/profiles/change-impact-analysis.md +20 -0
- package/runtime/prompts/profiles/error-analysis.md +27 -1
- package/runtime/prompts/profiles/feature-analysis.md +20 -0
- package/runtime/prompts/profiles/final-verification.md +20 -1
- package/runtime/prompts/profiles/implementation-option-selection.md +20 -0
- package/runtime/prompts/profiles/implementation-planning.md +26 -0
- package/runtime/prompts/profiles/implementation.md +19 -0
- package/runtime/prompts/profiles/improvement-discovery.md +21 -1
- package/runtime/prompts/profiles/project-analysis.md +20 -0
- package/runtime/prompts/profiles/release-handoff.md +4 -0
- package/runtime/prompts/profiles/requirements-discovery.md +27 -1
- package/runtime/prompts/wizard/prompts.ko.json +35 -0
- 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/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 +95 -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/adapters/runtime/__init__.py +1 -0
- package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +27 -0
- package/runtime/python/okstra_ctl/adapters/runtime/cli_wrapper.py +49 -0
- package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +74 -0
- 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/open_worker.py +29 -0
- 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/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 +1204 -155
- package/runtime/python/okstra_ctl/dispatch_state.py +696 -11
- package/runtime/python/okstra_ctl/doctor.py +14 -0
- package/runtime/python/okstra_ctl/domain/host.py +34 -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 +18 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +34 -1
- package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +44 -0
- 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/implementation_outcome.py +21 -5
- 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 +578 -0
- package/runtime/python/okstra_ctl/manager_sync.py +4 -1
- 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/next_phase.py +236 -0
- 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/ports/worker_runtime.py +26 -0
- package/runtime/python/okstra_ctl/recap.py +4 -1
- package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
- package/runtime/python/okstra_ctl/render.py +124 -57
- package/runtime/python/okstra_ctl/render_final_report.py +19 -0
- package/runtime/python/okstra_ctl/report_contract.py +39 -0
- package/runtime/python/okstra_ctl/report_finalize.py +16 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -0
- package/runtime/python/okstra_ctl/role_requirements.py +210 -0
- package/runtime/python/okstra_ctl/rollup.py +4 -1
- package/runtime/python/okstra_ctl/run.py +1210 -258
- package/runtime/python/okstra_ctl/run_index_row.py +7 -1
- package/runtime/python/okstra_ctl/stage_fix_carry.py +17 -1
- package/runtime/python/okstra_ctl/team.py +14 -18
- package/runtime/python/okstra_ctl/time_report.py +38 -4
- 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 +1449 -66
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
- package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -2
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +40 -9
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +16 -1
- package/runtime/python/okstra_ctl/worker_request.py +7 -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/workflow.py +18 -32
- package/runtime/python/okstra_ctl/worktree.py +3 -3
- package/runtime/python/okstra_ctl/worktree_registry.py +5 -4
- package/runtime/python/okstra_ctl/write_policy.py +571 -0
- package/runtime/python/okstra_project/state.py +54 -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 +68 -5
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -0
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +15 -13
- package/runtime/skills/okstra-rollup/SKILL.md +1 -0
- package/runtime/skills/okstra-run/SKILL.md +14 -14
- package/runtime/templates/implementation-worker-preamble.md +3 -18
- package/runtime/templates/project-docs/task-index.template.md +0 -1
- 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/html/macros/forms.html +5 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +1 -1
- package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
- package/runtime/templates/worker-prompt-preamble.md +6 -35
- package/runtime/validators/lib/fixtures.sh +49 -16
- package/runtime/validators/validate-run.py +486 -134
- package/runtime/validators/validate_session_conformance.py +25 -7
- package/src/cli-registry.mjs +0 -539
- 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/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 -1170
- 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/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
|
@@ -73,8 +73,8 @@ from okstra_ctl.incremental_scope import ( # noqa: E402
|
|
|
73
73
|
coverage_row_blocked_on,
|
|
74
74
|
stages_for_clarification,
|
|
75
75
|
)
|
|
76
|
+
from okstra_ctl import next_phase # noqa: E402
|
|
76
77
|
from okstra_ctl.workflow import ( # noqa: E402
|
|
77
|
-
DEFAULT_NEXT_PHASE,
|
|
78
78
|
ERROR_ANALYSIS_ROUTING_DIRECTIONS,
|
|
79
79
|
PHASE_SEQUENCE,
|
|
80
80
|
REQUIREMENTS_DISCOVERY_ROUTING_TARGETS,
|
|
@@ -122,15 +122,21 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
|
|
|
122
122
|
PromptRecord,
|
|
123
123
|
validate_initial_prompt_records,
|
|
124
124
|
)
|
|
125
|
+
from okstra_ctl.domain.role import RoleCatalogError, role_for_duty # noqa: E402
|
|
125
126
|
from okstra_ctl.agent_invocation import ( # noqa: E402
|
|
126
127
|
AgentInvocationError,
|
|
127
128
|
agent_model_assignment_from_payload,
|
|
129
|
+
invocation_execution_identity_from_manifest,
|
|
128
130
|
verify_agent_invocation,
|
|
129
131
|
)
|
|
132
|
+
from okstra_ctl.execution_identity import ExecutionManifestError # noqa: E402
|
|
133
|
+
from okstra_ctl.execution_manifest import ( # noqa: E402
|
|
134
|
+
validate_execution_manifest_payload,
|
|
135
|
+
)
|
|
130
136
|
from okstra_ctl.lead_events import LeadEventParseError, read_lead_events # noqa: E402
|
|
131
137
|
from okstra_ctl.worker_audit_ledger import ( # noqa: E402
|
|
132
138
|
READING_CONFIRMATION_HEADING_RE,
|
|
133
|
-
|
|
139
|
+
worker_results_audit_findings,
|
|
134
140
|
)
|
|
135
141
|
from validate_analysis_report import validate_analysis_report # noqa: E402
|
|
136
142
|
from okstra_ctl.convergence_engine import ( # noqa: E402
|
|
@@ -151,6 +157,30 @@ _AGENT_DISPATCH_DIGEST_KEYS = (
|
|
|
151
157
|
"instructionDigest",
|
|
152
158
|
"promptDigest",
|
|
153
159
|
)
|
|
160
|
+
_V2_AGENT_DISPATCH_IDENTITY_KEYS = (
|
|
161
|
+
"participantRef",
|
|
162
|
+
"roleExecutionRef",
|
|
163
|
+
"executionLabel",
|
|
164
|
+
"dutyId",
|
|
165
|
+
"invocationRef",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _validate_execution_identity_v2(
|
|
170
|
+
run_manifest: Mapping[str, Any],
|
|
171
|
+
failures: list[str],
|
|
172
|
+
) -> None:
|
|
173
|
+
version = run_manifest.get("schemaVersion")
|
|
174
|
+
identity_version = run_manifest.get("executionIdentityVersion")
|
|
175
|
+
if version in {1, "1", "1.0"} and identity_version is None:
|
|
176
|
+
return
|
|
177
|
+
if version != "2.0" or identity_version != 2:
|
|
178
|
+
failures.append("execution identity v2: mixed execution identity version")
|
|
179
|
+
return
|
|
180
|
+
try:
|
|
181
|
+
validate_execution_manifest_payload(run_manifest)
|
|
182
|
+
except ExecutionManifestError as exc:
|
|
183
|
+
failures.append(f"execution identity v2: {exc}")
|
|
154
184
|
|
|
155
185
|
|
|
156
186
|
def _validate_agent_dispatch_contract(
|
|
@@ -169,6 +199,28 @@ def _validate_agent_dispatch_contract(
|
|
|
169
199
|
if not isinstance(assignments, Mapping):
|
|
170
200
|
failures.append("agent invocation metadata is missing: invocationAssignments")
|
|
171
201
|
return
|
|
202
|
+
manifest_version = run_manifest.get("schemaVersion")
|
|
203
|
+
execution_identity_version = run_manifest.get("executionIdentityVersion")
|
|
204
|
+
if manifest_version == "2.0" and execution_identity_version == 2:
|
|
205
|
+
uses_v2_identity = True
|
|
206
|
+
elif (
|
|
207
|
+
manifest_version in {None, 1, "1", "1.0"}
|
|
208
|
+
and execution_identity_version is None
|
|
209
|
+
):
|
|
210
|
+
uses_v2_identity = False
|
|
211
|
+
else:
|
|
212
|
+
failures.append("agent dispatch contract mixes v1 and v2 execution identity")
|
|
213
|
+
return
|
|
214
|
+
canonical_invocations = {
|
|
215
|
+
row.get("invocationRef"): row
|
|
216
|
+
for row in run_manifest.get("invocations") or []
|
|
217
|
+
if isinstance(row, Mapping)
|
|
218
|
+
} if uses_v2_identity else {}
|
|
219
|
+
canonical_attempts = {
|
|
220
|
+
(row.get("invocationRef"), row.get("attempt"))
|
|
221
|
+
for row in run_manifest.get("attempts") or []
|
|
222
|
+
if isinstance(row, Mapping)
|
|
223
|
+
} if uses_v2_identity else set()
|
|
172
224
|
worker_dispatches = [
|
|
173
225
|
row for row in (team_state.get("workerDispatches") or [])
|
|
174
226
|
if isinstance(row, Mapping) and row.get("invocationId")
|
|
@@ -179,13 +231,15 @@ def _validate_agent_dispatch_contract(
|
|
|
179
231
|
]
|
|
180
232
|
dispatches = [*worker_dispatches, *agent_dispatches]
|
|
181
233
|
ids: dict[str, Mapping[str, Any]] = {}
|
|
234
|
+
seen_dispatch_ids: set[str] = set()
|
|
182
235
|
for row in dispatches:
|
|
183
236
|
dispatch_id = str(row.get("dispatchId") or "").strip()
|
|
184
237
|
missing = [
|
|
185
238
|
key for key in (
|
|
186
|
-
"dispatchId", "
|
|
239
|
+
"dispatchId", "audience", "invocationId",
|
|
187
240
|
"assignmentRef", "promptMetadataPath", "modelExecutionValue",
|
|
188
241
|
"enforcementMode", *_AGENT_DISPATCH_DIGEST_KEYS,
|
|
242
|
+
*(_V2_AGENT_DISPATCH_IDENTITY_KEYS if uses_v2_identity else ("workerId",)),
|
|
189
243
|
)
|
|
190
244
|
if not str(row.get(key) or "").strip()
|
|
191
245
|
]
|
|
@@ -195,10 +249,43 @@ def _validate_agent_dispatch_contract(
|
|
|
195
249
|
+ ", ".join(missing)
|
|
196
250
|
)
|
|
197
251
|
continue
|
|
198
|
-
if
|
|
252
|
+
if uses_v2_identity:
|
|
253
|
+
if "workerId" in row or row.get("executionIdentityVersion") != 2:
|
|
254
|
+
failures.append(
|
|
255
|
+
f"agent dispatch {dispatch_id} mixes v1 and v2 execution identity"
|
|
256
|
+
)
|
|
257
|
+
continue
|
|
258
|
+
attempt = row.get("attempt")
|
|
259
|
+
if (
|
|
260
|
+
not isinstance(attempt, int)
|
|
261
|
+
or isinstance(attempt, bool)
|
|
262
|
+
or attempt < 1
|
|
263
|
+
):
|
|
264
|
+
failures.append(
|
|
265
|
+
f"agent dispatch {dispatch_id}: v2 attempt must be positive"
|
|
266
|
+
)
|
|
267
|
+
continue
|
|
268
|
+
if dispatch_id != f"{row['invocationId']}:attempt-{attempt}":
|
|
269
|
+
failures.append(
|
|
270
|
+
f"agent dispatch {dispatch_id}: dispatchId does not match v2 attempt"
|
|
271
|
+
)
|
|
272
|
+
continue
|
|
273
|
+
elif any(
|
|
274
|
+
key in row
|
|
275
|
+
for key in (
|
|
276
|
+
"executionIdentityVersion",
|
|
277
|
+
"attempt",
|
|
278
|
+
*_V2_AGENT_DISPATCH_IDENTITY_KEYS,
|
|
279
|
+
)
|
|
280
|
+
):
|
|
281
|
+
failures.append(
|
|
282
|
+
f"agent dispatch {dispatch_id} mixes v1 and v2 execution identity"
|
|
283
|
+
)
|
|
284
|
+
continue
|
|
285
|
+
if dispatch_id in seen_dispatch_ids:
|
|
199
286
|
failures.append(f"agent dispatch ID is duplicated: {dispatch_id}")
|
|
200
287
|
continue
|
|
201
|
-
|
|
288
|
+
seen_dispatch_ids.add(dispatch_id)
|
|
202
289
|
assignment_ref = str(row["assignmentRef"])
|
|
203
290
|
try:
|
|
204
291
|
assignment = agent_model_assignment_from_payload(
|
|
@@ -207,6 +294,118 @@ def _validate_agent_dispatch_contract(
|
|
|
207
294
|
except AgentInvocationError as exc:
|
|
208
295
|
failures.append(f"agent dispatch {dispatch_id}: {exc}")
|
|
209
296
|
continue
|
|
297
|
+
execution_identity = None
|
|
298
|
+
if uses_v2_identity:
|
|
299
|
+
try:
|
|
300
|
+
execution_identity = invocation_execution_identity_from_manifest(
|
|
301
|
+
run_manifest,
|
|
302
|
+
assignment=assignment,
|
|
303
|
+
assignment_ref=assignment_ref,
|
|
304
|
+
duty_id=str(row["dutyId"]),
|
|
305
|
+
)
|
|
306
|
+
except AgentInvocationError as exc:
|
|
307
|
+
failures.append(f"agent dispatch {dispatch_id}: {exc}")
|
|
308
|
+
continue
|
|
309
|
+
if (
|
|
310
|
+
execution_identity is None
|
|
311
|
+
or row.get("participantRef") != execution_identity.participant_ref
|
|
312
|
+
or row.get("roleExecutionRef") != execution_identity.role_execution_ref
|
|
313
|
+
):
|
|
314
|
+
failures.append(
|
|
315
|
+
f"agent dispatch {dispatch_id}: v2 identity does not match "
|
|
316
|
+
"a canonical role execution"
|
|
317
|
+
)
|
|
318
|
+
continue
|
|
319
|
+
if row.get("executionLabel") != execution_identity.execution_label:
|
|
320
|
+
failures.append(
|
|
321
|
+
f"agent dispatch {dispatch_id}: executionLabel does not match "
|
|
322
|
+
"canonical role execution"
|
|
323
|
+
)
|
|
324
|
+
continue
|
|
325
|
+
dispatch_kind = row.get("dispatchKind") or row.get("kind")
|
|
326
|
+
if not isinstance(dispatch_kind, str) or not dispatch_kind:
|
|
327
|
+
failures.append(
|
|
328
|
+
f"agent dispatch {dispatch_id}: dispatchKind is missing"
|
|
329
|
+
)
|
|
330
|
+
continue
|
|
331
|
+
invocation = canonical_invocations.get(row.get("invocationRef"))
|
|
332
|
+
if not isinstance(invocation, Mapping) or any((
|
|
333
|
+
invocation.get("participantRef") != row.get("participantRef"),
|
|
334
|
+
invocation.get("roleExecutionRef") != row.get("roleExecutionRef"),
|
|
335
|
+
invocation.get("dutyId") != row.get("dutyId"),
|
|
336
|
+
invocation.get("dispatchKind") != dispatch_kind,
|
|
337
|
+
invocation.get("inputDigest") != row.get("promptDigest"),
|
|
338
|
+
)):
|
|
339
|
+
failures.append(
|
|
340
|
+
f"agent dispatch {dispatch_id}: does not match canonical invocation"
|
|
341
|
+
)
|
|
342
|
+
continue
|
|
343
|
+
if row.get("writePolicyDigest") != invocation.get("writePolicyDigest"):
|
|
344
|
+
failures.append(
|
|
345
|
+
f"agent dispatch {dispatch_id}: writePolicyDigest does not "
|
|
346
|
+
"match canonical invocation"
|
|
347
|
+
)
|
|
348
|
+
continue
|
|
349
|
+
if row.get("writeEnforcement") != invocation.get("writeEnforcement"):
|
|
350
|
+
failures.append(
|
|
351
|
+
f"agent dispatch {dispatch_id}: writeEnforcement does not "
|
|
352
|
+
"match canonical invocation"
|
|
353
|
+
)
|
|
354
|
+
continue
|
|
355
|
+
mutation_mode = invocation.get("writeEnforcement", {}).get(
|
|
356
|
+
"mutationAudit"
|
|
357
|
+
) if isinstance(invocation.get("writeEnforcement"), Mapping) else None
|
|
358
|
+
snapshot_value = row.get("mutationAuditSnapshotPath")
|
|
359
|
+
if mutation_mode == "batch" and not (
|
|
360
|
+
isinstance(snapshot_value, str) and snapshot_value.strip()
|
|
361
|
+
):
|
|
362
|
+
failures.append(
|
|
363
|
+
f"agent dispatch {dispatch_id}: mutation audit snapshot is missing"
|
|
364
|
+
)
|
|
365
|
+
continue
|
|
366
|
+
if mutation_mode == "none" and snapshot_value not in (None, ""):
|
|
367
|
+
failures.append(
|
|
368
|
+
f"agent dispatch {dispatch_id}: exact-path dispatch has a "
|
|
369
|
+
"mutation audit snapshot"
|
|
370
|
+
)
|
|
371
|
+
continue
|
|
372
|
+
if (row.get("invocationRef"), attempt) not in canonical_attempts:
|
|
373
|
+
failures.append(
|
|
374
|
+
f"agent dispatch {dispatch_id}: has no canonical invocation attempt"
|
|
375
|
+
)
|
|
376
|
+
continue
|
|
377
|
+
status_path_raw = row.get("statusSidecarPath")
|
|
378
|
+
if isinstance(status_path_raw, str) and status_path_raw.strip():
|
|
379
|
+
status_path = _resolve_prompt_record_path(
|
|
380
|
+
project_root, status_path_raw
|
|
381
|
+
)
|
|
382
|
+
try:
|
|
383
|
+
status = json.loads(status_path.read_text(encoding="utf-8"))
|
|
384
|
+
except (OSError, json.JSONDecodeError):
|
|
385
|
+
failures.append(
|
|
386
|
+
f"agent dispatch {dispatch_id}: wrapper status is missing or invalid"
|
|
387
|
+
)
|
|
388
|
+
continue
|
|
389
|
+
status_identity = {
|
|
390
|
+
key: status.get(key)
|
|
391
|
+
for key in (*_V2_AGENT_DISPATCH_IDENTITY_KEYS, "attempt")
|
|
392
|
+
} if isinstance(status, Mapping) else {}
|
|
393
|
+
dispatch_identity = {
|
|
394
|
+
key: row.get(key)
|
|
395
|
+
for key in (*_V2_AGENT_DISPATCH_IDENTITY_KEYS, "attempt")
|
|
396
|
+
}
|
|
397
|
+
if (
|
|
398
|
+
not isinstance(status, Mapping)
|
|
399
|
+
or status.get("schemaVersion") != "2.0"
|
|
400
|
+
or status.get("executionIdentityVersion") != 2
|
|
401
|
+
or status_identity != dispatch_identity
|
|
402
|
+
):
|
|
403
|
+
failures.append(
|
|
404
|
+
f"agent dispatch {dispatch_id}: wrapper status identity "
|
|
405
|
+
"does not match dispatch"
|
|
406
|
+
)
|
|
407
|
+
continue
|
|
408
|
+
ids[dispatch_id] = row
|
|
210
409
|
metadata_path = _resolve_prompt_record_path(
|
|
211
410
|
project_root, str(row["promptMetadataPath"])
|
|
212
411
|
)
|
|
@@ -216,9 +415,20 @@ def _validate_agent_dispatch_contract(
|
|
|
216
415
|
expected_run_manifest_path=run_manifest_path,
|
|
217
416
|
expected_assignment=assignment,
|
|
218
417
|
expected_invocation_id=str(row["invocationId"]),
|
|
219
|
-
expected_worker_id=
|
|
418
|
+
expected_worker_id=(
|
|
419
|
+
None if uses_v2_identity else str(row["workerId"])
|
|
420
|
+
),
|
|
220
421
|
expected_assignment_ref=assignment_ref,
|
|
221
422
|
expected_audience=str(row["audience"]),
|
|
423
|
+
expected_participant_ref=(
|
|
424
|
+
str(row["participantRef"]) if uses_v2_identity else None
|
|
425
|
+
),
|
|
426
|
+
expected_role_execution_ref=(
|
|
427
|
+
str(row["roleExecutionRef"]) if uses_v2_identity else None
|
|
428
|
+
),
|
|
429
|
+
expected_invocation_ref=(
|
|
430
|
+
str(row["invocationRef"]) if uses_v2_identity else None
|
|
431
|
+
),
|
|
222
432
|
)
|
|
223
433
|
failures.extend(
|
|
224
434
|
f"agent dispatch {dispatch_id}: {error}" for error in errors
|
|
@@ -258,10 +468,23 @@ def _validate_agent_dispatch_contract(
|
|
|
258
468
|
else "host-native prompt delivery cannot be marked verified"
|
|
259
469
|
)
|
|
260
470
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
471
|
+
if uses_v2_identity:
|
|
472
|
+
role_executions = {
|
|
473
|
+
row.get("roleExecutionRef"): row
|
|
474
|
+
for row in run_manifest.get("roleExecutions") or []
|
|
475
|
+
if isinstance(row, Mapping)
|
|
476
|
+
}
|
|
477
|
+
lead_ids = {
|
|
478
|
+
dispatch_id for dispatch_id, row in ids.items()
|
|
479
|
+
if row.get("audience") == "lead"
|
|
480
|
+
and isinstance(role_executions.get(row.get("roleExecutionRef")), Mapping)
|
|
481
|
+
and role_executions[row["roleExecutionRef"]].get("role") == "leader"
|
|
482
|
+
}
|
|
483
|
+
else:
|
|
484
|
+
lead_ids = {
|
|
485
|
+
dispatch_id for dispatch_id, row in ids.items()
|
|
486
|
+
if row.get("audience") == "lead" and row.get("workerId") == "lead"
|
|
487
|
+
}
|
|
265
488
|
if not lead_ids:
|
|
266
489
|
failures.append("accepted lead result has no lead dispatch record")
|
|
267
490
|
|
|
@@ -277,6 +500,22 @@ def _validate_agent_dispatch_contract(
|
|
|
277
500
|
if dispatch_id not in ids or not result_path:
|
|
278
501
|
failures.append("agent result link has no matching dispatch record")
|
|
279
502
|
continue
|
|
503
|
+
if uses_v2_identity:
|
|
504
|
+
dispatch = ids[dispatch_id]
|
|
505
|
+
identity_keys = (*_V2_AGENT_DISPATCH_IDENTITY_KEYS, "attempt")
|
|
506
|
+
link_identity = {key: link.get(key) for key in identity_keys}
|
|
507
|
+
dispatch_identity = {key: dispatch.get(key) for key in identity_keys}
|
|
508
|
+
if "workerId" in link or link_identity != dispatch_identity:
|
|
509
|
+
failures.append(
|
|
510
|
+
f"agent result link identity does not match dispatch: {dispatch_id}"
|
|
511
|
+
)
|
|
512
|
+
continue
|
|
513
|
+
elif any(
|
|
514
|
+
key in link
|
|
515
|
+
for key in (*_V2_AGENT_DISPATCH_IDENTITY_KEYS, "attempt")
|
|
516
|
+
):
|
|
517
|
+
failures.append("agent result link mixes v1 and v2 execution identity")
|
|
518
|
+
continue
|
|
280
519
|
dispatch_link_counts[dispatch_id] = dispatch_link_counts.get(dispatch_id, 0) + 1
|
|
281
520
|
previous = paths.setdefault(result_path, dispatch_id)
|
|
282
521
|
if previous != dispatch_id:
|
|
@@ -296,7 +535,11 @@ def _validate_agent_dispatch_contract(
|
|
|
296
535
|
worker_result = str(worker.get("resultPath") or "").strip()
|
|
297
536
|
worker_dispatch_ids = {
|
|
298
537
|
dispatch_id for dispatch_id, row in ids.items()
|
|
299
|
-
if
|
|
538
|
+
if (
|
|
539
|
+
row.get("assignmentRef") == f"initial/{worker_id}"
|
|
540
|
+
if uses_v2_identity
|
|
541
|
+
else row.get("workerId") == worker_id
|
|
542
|
+
)
|
|
300
543
|
}
|
|
301
544
|
if worker_id and not worker_dispatch_ids:
|
|
302
545
|
failures.append(
|
|
@@ -384,68 +627,6 @@ def write_json(path: Path, payload: dict) -> None:
|
|
|
384
627
|
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
385
628
|
|
|
386
629
|
|
|
387
|
-
def default_next_phase(task_type: str) -> str:
|
|
388
|
-
return DEFAULT_NEXT_PHASE.get(task_type, "unknown")
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
def advance_next_phase(
|
|
392
|
-
current_phase: str,
|
|
393
|
-
phase_sequence: list,
|
|
394
|
-
) -> str:
|
|
395
|
-
"""Compute the next recommended phase after `current_phase` completes.
|
|
396
|
-
|
|
397
|
-
Walk `phase_sequence` and return the entry following `current_phase`.
|
|
398
|
-
Falls back to the static `default_next_phase` mapping when the current
|
|
399
|
-
phase is the terminal entry or is absent from the sequence (e.g. custom
|
|
400
|
-
task types that aren't part of the standard 5-phase lifecycle).
|
|
401
|
-
"""
|
|
402
|
-
if isinstance(phase_sequence, list) and current_phase in phase_sequence:
|
|
403
|
-
idx = phase_sequence.index(current_phase)
|
|
404
|
-
if idx + 1 < len(phase_sequence):
|
|
405
|
-
return phase_sequence[idx + 1]
|
|
406
|
-
return "done-or-follow-up"
|
|
407
|
-
return default_next_phase(current_phase)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
def _error_analysis_next_phase(data: Mapping[str, Any]) -> str | None:
|
|
411
|
-
if not isinstance(data, Mapping):
|
|
412
|
-
return None
|
|
413
|
-
header = data.get("header")
|
|
414
|
-
if not isinstance(header, Mapping) or header.get("taskType") != "error-analysis":
|
|
415
|
-
return None
|
|
416
|
-
error_analysis = data.get("errorAnalysis")
|
|
417
|
-
if not isinstance(error_analysis, Mapping):
|
|
418
|
-
return None
|
|
419
|
-
routing = error_analysis.get("routing")
|
|
420
|
-
if not isinstance(routing, Mapping):
|
|
421
|
-
return None
|
|
422
|
-
target = routing.get("nextTaskType")
|
|
423
|
-
if target in ERROR_ANALYSIS_ROUTING_DIRECTIONS:
|
|
424
|
-
return str(target)
|
|
425
|
-
return None
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
def _requirements_discovery_next_phase(data: Mapping[str, Any]) -> str | None:
|
|
429
|
-
if not isinstance(data, Mapping):
|
|
430
|
-
return None
|
|
431
|
-
header = data.get("header")
|
|
432
|
-
if (
|
|
433
|
-
not isinstance(header, Mapping)
|
|
434
|
-
or header.get("taskType") != "requirements-discovery"
|
|
435
|
-
):
|
|
436
|
-
return None
|
|
437
|
-
requirements = data.get("requirementsDiscovery")
|
|
438
|
-
if not isinstance(requirements, Mapping):
|
|
439
|
-
return None
|
|
440
|
-
routing = requirements.get("routing")
|
|
441
|
-
if not isinstance(routing, Mapping):
|
|
442
|
-
return None
|
|
443
|
-
target = routing.get("nextTaskType")
|
|
444
|
-
if target in REQUIREMENTS_DISCOVERY_ROUTING_TARGETS:
|
|
445
|
-
return str(target)
|
|
446
|
-
return None
|
|
447
|
-
|
|
448
|
-
|
|
449
630
|
def update_workflow_metadata(
|
|
450
631
|
run_manifest: dict,
|
|
451
632
|
task_manifest: dict,
|
|
@@ -472,36 +653,49 @@ def update_workflow_metadata(
|
|
|
472
653
|
for phase in phase_sequence:
|
|
473
654
|
phase_states.setdefault(phase, "not-started")
|
|
474
655
|
|
|
656
|
+
# 포인터는 리드가 리포트에 저작하고, 이 검증기는 그것을 계산하지 않는다.
|
|
657
|
+
# 여기서 하는 일은 리포트의 Phase 라우팅 투영과 대조하는 것뿐이다.
|
|
658
|
+
authored = next_phase.promote(workflow.get("nextRecommendedPhase"))
|
|
659
|
+
workflow.pop("nextRecommendedPhaseCorrection", None)
|
|
660
|
+
|
|
475
661
|
if validation_status == "passed":
|
|
476
662
|
current_phase_state = "completed"
|
|
477
663
|
if current_phase:
|
|
478
664
|
phase_states[current_phase] = current_phase_state
|
|
479
665
|
last_completed_phase = current_phase or workflow.get("lastCompletedPhase", "")
|
|
480
|
-
#
|
|
481
|
-
#
|
|
482
|
-
|
|
483
|
-
if
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
666
|
+
# 어긋나면 투영값으로 정정하되 실행은 죽이지 않는다 — 분석이 통과한 run 을
|
|
667
|
+
# 장부 필드 하나 때문에 버리지 않는다 (ADR-0004).
|
|
668
|
+
projected = next_phase.project(report_data or {})
|
|
669
|
+
if (
|
|
670
|
+
authored["phase"] == projected["phase"]
|
|
671
|
+
and authored["status"] == projected["status"]
|
|
672
|
+
):
|
|
673
|
+
next_recommended_phase = authored
|
|
487
674
|
else:
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
675
|
+
# 리드의 근거는 리드가 고른 phase 를 설명하는 문장이다. phase 를 정정한
|
|
676
|
+
# 뒤에도 들고 오면 포인터가 자기 목적지를 설명하지 않는 문장을 달게 된다.
|
|
677
|
+
# 원문은 아래 correction["authored"] 에 그대로 보존된다.
|
|
678
|
+
next_recommended_phase = next_phase.make(
|
|
679
|
+
phase=projected["phase"],
|
|
680
|
+
status=projected["status"],
|
|
681
|
+
rationale=(
|
|
682
|
+
"리포트 라우팅에서 투영됨. 리드가 쓴 값과 근거는 "
|
|
683
|
+
"nextRecommendedPhaseCorrection.authored 에 있다."
|
|
684
|
+
),
|
|
685
|
+
)
|
|
686
|
+
# applied 는 사본이다. 살아 있는 포인터와 같은 dict 를 가리키면
|
|
687
|
+
# 나중에 포인터를 제자리 변형하는 호출부가 감사 기록까지 바꾼다.
|
|
688
|
+
workflow["nextRecommendedPhaseCorrection"] = {
|
|
689
|
+
"authored": authored,
|
|
690
|
+
"applied": dict(next_recommended_phase),
|
|
691
|
+
}
|
|
492
692
|
else:
|
|
493
693
|
current_phase_state = "blocked"
|
|
494
694
|
if current_phase:
|
|
495
695
|
phase_states[current_phase] = current_phase_state
|
|
496
696
|
last_completed_phase = workflow.get("lastCompletedPhase", "")
|
|
497
|
-
next_recommended_phase =
|
|
498
|
-
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
routing_status = workflow.get("routingStatus")
|
|
502
|
-
if not isinstance(routing_status, str) or not routing_status.strip():
|
|
503
|
-
routing_status = (
|
|
504
|
-
"pending" if current_phase == "requirements-discovery" else "not-applicable"
|
|
697
|
+
next_recommended_phase = next_phase.make(
|
|
698
|
+
status=next_phase.STATUS_BLOCKED, rationale=authored["rationale"]
|
|
505
699
|
)
|
|
506
700
|
|
|
507
701
|
awaiting_approval = workflow.get("awaitingApproval")
|
|
@@ -549,6 +743,9 @@ def update_workflow_metadata(
|
|
|
549
743
|
}
|
|
550
744
|
)
|
|
551
745
|
|
|
746
|
+
# routingStatus 는 포인터 status 로 흡수됐다. update() 는 키를 더할 뿐이라
|
|
747
|
+
# 이전 run 이 남긴 값은 명시적으로 지워야 사라진다.
|
|
748
|
+
workflow.pop("routingStatus", None)
|
|
552
749
|
workflow.update(
|
|
553
750
|
{
|
|
554
751
|
"phaseSequence": phase_sequence,
|
|
@@ -558,7 +755,6 @@ def update_workflow_metadata(
|
|
|
558
755
|
"lastCompletedPhase": last_completed_phase,
|
|
559
756
|
"nextRecommendedPhase": next_recommended_phase,
|
|
560
757
|
"awaitingApproval": awaiting_approval,
|
|
561
|
-
"routingStatus": routing_status,
|
|
562
758
|
"lastSafeCheckpoint": last_safe_checkpoint,
|
|
563
759
|
}
|
|
564
760
|
)
|
|
@@ -567,6 +763,10 @@ def update_workflow_metadata(
|
|
|
567
763
|
workflow_snapshot = run_manifest.get("workflowSnapshot", {})
|
|
568
764
|
if not isinstance(workflow_snapshot, dict):
|
|
569
765
|
workflow_snapshot = {}
|
|
766
|
+
# task manifest 의 workflow 는 다음 run 이 덮어쓰므로 정정 기록이 남지 않는다.
|
|
767
|
+
# run 단위 감사 기록은 이 스냅샷이 유일한 보관처다.
|
|
768
|
+
workflow_snapshot.pop("routingStatus", None)
|
|
769
|
+
workflow_snapshot.pop("nextRecommendedPhaseCorrection", None)
|
|
570
770
|
workflow_snapshot.update(
|
|
571
771
|
{
|
|
572
772
|
"phaseSequence": workflow["phaseSequence"],
|
|
@@ -574,12 +774,20 @@ def update_workflow_metadata(
|
|
|
574
774
|
"currentPhaseState": workflow["currentPhaseState"],
|
|
575
775
|
"phaseStates": workflow["phaseStates"],
|
|
576
776
|
"lastCompletedPhase": workflow["lastCompletedPhase"],
|
|
577
|
-
|
|
777
|
+
# 포인터는 이제 문자열이 아니라 dict 다. 두 매니페스트가 같은 객체를
|
|
778
|
+
# 공유하면 한쪽을 제자리 변형할 때 다른 쪽이 조용히 따라 바뀐다.
|
|
779
|
+
"nextRecommendedPhase": dict(workflow["nextRecommendedPhase"]),
|
|
578
780
|
"awaitingApproval": workflow["awaitingApproval"],
|
|
579
|
-
"routingStatus": workflow["routingStatus"],
|
|
580
781
|
"lastSafeCheckpoint": workflow["lastSafeCheckpoint"],
|
|
581
782
|
}
|
|
582
783
|
)
|
|
784
|
+
if "nextRecommendedPhaseCorrection" in workflow:
|
|
785
|
+
# 두 매니페스트는 별개 파일로 나간다 — dict 를 공유시키지 않는다.
|
|
786
|
+
# 겉 dict 만 복사하면 authored·applied 는 여전히 같은 객체다.
|
|
787
|
+
workflow_snapshot["nextRecommendedPhaseCorrection"] = {
|
|
788
|
+
key: dict(value)
|
|
789
|
+
for key, value in workflow["nextRecommendedPhaseCorrection"].items()
|
|
790
|
+
}
|
|
583
791
|
run_manifest["workflowSnapshot"] = workflow_snapshot
|
|
584
792
|
|
|
585
793
|
|
|
@@ -590,13 +798,16 @@ def update_validation_metadata(
|
|
|
590
798
|
validation_status: str,
|
|
591
799
|
failures: list[str],
|
|
592
800
|
report_data: Mapping[str, Any] | None = None,
|
|
801
|
+
advisories: list[str] | None = None,
|
|
593
802
|
) -> None:
|
|
594
803
|
checked_at = utc_now()
|
|
804
|
+
advisories = list(advisories or [])
|
|
595
805
|
|
|
596
806
|
team_state.setdefault("validator", {})
|
|
597
807
|
team_state["validator"]["status"] = validation_status
|
|
598
808
|
team_state["validator"]["lastValidatedAt"] = checked_at
|
|
599
809
|
team_state["validator"]["failures"] = failures
|
|
810
|
+
team_state["validator"]["advisories"] = advisories
|
|
600
811
|
|
|
601
812
|
run_manifest.setdefault("validation", {})
|
|
602
813
|
run_manifest["validation"]["required"] = True
|
|
@@ -604,6 +815,8 @@ def update_validation_metadata(
|
|
|
604
815
|
run_manifest["validation"]["lastCheckedAt"] = checked_at
|
|
605
816
|
run_manifest["validation"]["passed"] = validation_status == "passed"
|
|
606
817
|
run_manifest["validation"]["failures"] = failures
|
|
818
|
+
# Reported, not blocking — a passing run can carry these.
|
|
819
|
+
run_manifest["validation"]["advisories"] = advisories
|
|
607
820
|
run_manifest["status"] = (
|
|
608
821
|
"completed" if validation_status == "passed" else "contract-violated"
|
|
609
822
|
)
|
|
@@ -709,35 +922,127 @@ def _persisted_prompt_records(
|
|
|
709
922
|
project_root: Path,
|
|
710
923
|
selected_ids: set[str],
|
|
711
924
|
team_state: dict,
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
925
|
+
run_manifest: Mapping[str, Any],
|
|
926
|
+
) -> tuple[list[PromptRecord], list[str]]:
|
|
927
|
+
uses_v2_identity = (
|
|
928
|
+
run_manifest.get("schemaVersion") == "2.0"
|
|
929
|
+
and run_manifest.get("executionIdentityVersion") == 2
|
|
716
930
|
)
|
|
931
|
+
dispatches = team_state.get("workerDispatches")
|
|
932
|
+
if uses_v2_identity:
|
|
933
|
+
source = [
|
|
934
|
+
*(dispatches if isinstance(dispatches, list) else []),
|
|
935
|
+
*(
|
|
936
|
+
team_state.get("agentDispatches")
|
|
937
|
+
if isinstance(team_state.get("agentDispatches"), list)
|
|
938
|
+
else []
|
|
939
|
+
),
|
|
940
|
+
]
|
|
941
|
+
else:
|
|
942
|
+
source = dispatches if isinstance(dispatches, list) and dispatches else (
|
|
943
|
+
team_state.get("workers") or []
|
|
944
|
+
)
|
|
717
945
|
records: list[PromptRecord] = []
|
|
718
946
|
seen: set[tuple[str, str, str]] = set()
|
|
947
|
+
errors: list[str] = []
|
|
948
|
+
role_executions = {
|
|
949
|
+
str(row.get("roleExecutionRef") or ""): row
|
|
950
|
+
for row in run_manifest.get("roleExecutions") or []
|
|
951
|
+
if isinstance(row, Mapping)
|
|
952
|
+
} if uses_v2_identity else {}
|
|
719
953
|
for worker in source:
|
|
720
954
|
if not isinstance(worker, dict):
|
|
721
955
|
continue
|
|
722
|
-
|
|
956
|
+
assignment_ref = str(worker.get("assignmentRef") or "").strip()
|
|
957
|
+
worker_id = (
|
|
958
|
+
assignment_ref.removeprefix("initial/")
|
|
959
|
+
if uses_v2_identity and assignment_ref.startswith("initial/")
|
|
960
|
+
else str(worker.get("workerId") or "").strip()
|
|
961
|
+
)
|
|
723
962
|
prompt_value = str(worker.get("promptPath") or "").strip()
|
|
724
|
-
if worker_id not in selected_ids
|
|
963
|
+
if worker_id not in selected_ids:
|
|
725
964
|
continue
|
|
726
|
-
|
|
965
|
+
if not prompt_value:
|
|
966
|
+
errors.append(f"{worker_id}: persisted initial prompt path is missing")
|
|
967
|
+
continue
|
|
968
|
+
dispatch_kind = str(
|
|
969
|
+
worker.get("kind") or worker.get("dispatchKind") or ""
|
|
970
|
+
).strip()
|
|
727
971
|
if not dispatch_kind:
|
|
728
972
|
dispatch_kind = _fallback_dispatch_kind(prompt_value)
|
|
973
|
+
duty_id = ""
|
|
974
|
+
if uses_v2_identity:
|
|
975
|
+
execution = role_executions.get(
|
|
976
|
+
str(worker.get("roleExecutionRef") or "").strip()
|
|
977
|
+
)
|
|
978
|
+
participant_ref = str(worker.get("participantRef") or "").strip()
|
|
979
|
+
duty_id = str(worker.get("dutyId") or "").strip()
|
|
980
|
+
audience = str(worker.get("audience") or "").strip()
|
|
981
|
+
assignments = run_manifest.get("invocationAssignments")
|
|
982
|
+
assignment = (
|
|
983
|
+
assignments.get(assignment_ref)
|
|
984
|
+
if isinstance(assignments, Mapping)
|
|
985
|
+
else None
|
|
986
|
+
)
|
|
987
|
+
if not isinstance(execution, Mapping):
|
|
988
|
+
errors.append(
|
|
989
|
+
f"{worker_id}: persisted prompt has unknown roleExecutionRef"
|
|
990
|
+
)
|
|
991
|
+
continue
|
|
992
|
+
try:
|
|
993
|
+
expected_role = role_for_duty(duty_id)
|
|
994
|
+
except RoleCatalogError as exc:
|
|
995
|
+
errors.append(f"{worker_id}: {exc}")
|
|
996
|
+
continue
|
|
997
|
+
if (
|
|
998
|
+
execution.get("participantRef") != participant_ref
|
|
999
|
+
or execution.get("role") != expected_role
|
|
1000
|
+
or execution.get("executionLabel") != worker.get("executionLabel")
|
|
1001
|
+
or not isinstance(assignment, Mapping)
|
|
1002
|
+
or execution.get("provider") != assignment.get("provider")
|
|
1003
|
+
or not isinstance(execution.get("binding"), Mapping)
|
|
1004
|
+
or execution["binding"].get("resolvedExecutionValue")
|
|
1005
|
+
!= assignment.get("modelExecutionValue")
|
|
1006
|
+
):
|
|
1007
|
+
errors.append(
|
|
1008
|
+
f"{worker_id}: persisted prompt identity does not match role execution"
|
|
1009
|
+
)
|
|
1010
|
+
continue
|
|
1011
|
+
if audience != duty_id:
|
|
1012
|
+
errors.append(
|
|
1013
|
+
f"{worker_id}: persisted prompt audience does not match duty"
|
|
1014
|
+
)
|
|
1015
|
+
continue
|
|
729
1016
|
identity = (worker_id, dispatch_kind, prompt_value)
|
|
730
1017
|
if identity in seen:
|
|
731
1018
|
continue
|
|
732
1019
|
seen.add(identity)
|
|
1020
|
+
metadata_value = str(worker.get("promptMetadataPath") or "").strip()
|
|
733
1021
|
records.append(
|
|
734
1022
|
PromptRecord(
|
|
735
1023
|
worker_id=worker_id,
|
|
736
1024
|
dispatch_kind=dispatch_kind,
|
|
737
1025
|
path=_resolve_prompt_record_path(project_root, prompt_value),
|
|
1026
|
+
expected_model=(
|
|
1027
|
+
str(worker.get("modelExecutionValue") or "").strip() or None
|
|
1028
|
+
if uses_v2_identity else None
|
|
1029
|
+
),
|
|
1030
|
+
metadata_path=(
|
|
1031
|
+
_resolve_prompt_record_path(project_root, metadata_value)
|
|
1032
|
+
if uses_v2_identity and metadata_value else None
|
|
1033
|
+
),
|
|
1034
|
+
expected_duty_audience=(duty_id if uses_v2_identity else None),
|
|
738
1035
|
)
|
|
739
1036
|
)
|
|
740
|
-
|
|
1037
|
+
if uses_v2_identity:
|
|
1038
|
+
recorded_ids = {record.worker_id for record in records}
|
|
1039
|
+
missing_ids = sorted(selected_ids - recorded_ids)
|
|
1040
|
+
if missing_ids:
|
|
1041
|
+
errors.append(
|
|
1042
|
+
"no persisted initial prompt records for selected role executions: "
|
|
1043
|
+
+ ", ".join(missing_ids)
|
|
1044
|
+
)
|
|
1045
|
+
return records, errors
|
|
741
1046
|
|
|
742
1047
|
|
|
743
1048
|
def _markdown_section(text: str, heading: str) -> str:
|
|
@@ -863,15 +1168,16 @@ def _validate_initial_analysis_prompts(
|
|
|
863
1168
|
team_state = data.get("teamState") or {}
|
|
864
1169
|
manifest = dict(run_manifest)
|
|
865
1170
|
manifest["taskType"] = task_type
|
|
866
|
-
records = _persisted_prompt_records(
|
|
1171
|
+
records, collection_errors = _persisted_prompt_records(
|
|
867
1172
|
project_root=project_root,
|
|
868
1173
|
selected_ids=selected_ids,
|
|
869
1174
|
team_state=team_state,
|
|
1175
|
+
run_manifest=run_manifest,
|
|
870
1176
|
)
|
|
871
|
-
errors = validate_initial_prompt_records(
|
|
1177
|
+
errors = [*collection_errors, *validate_initial_prompt_records(
|
|
872
1178
|
manifest=manifest,
|
|
873
1179
|
records=records,
|
|
874
|
-
)
|
|
1180
|
+
)]
|
|
875
1181
|
if task_type == "improvement-discovery":
|
|
876
1182
|
errors.extend(
|
|
877
1183
|
_validate_improvement_primary_assignments(
|
|
@@ -2638,22 +2944,30 @@ def _report_run_seq(report_path: Path) -> str | None:
|
|
|
2638
2944
|
|
|
2639
2945
|
|
|
2640
2946
|
def validate_worker_results_audit(
|
|
2641
|
-
report_path: Path,
|
|
2947
|
+
report_path: Path,
|
|
2948
|
+
task_type: str,
|
|
2949
|
+
failures: list[str],
|
|
2950
|
+
advisories: list[str] | None = None,
|
|
2642
2951
|
) -> None:
|
|
2643
2952
|
"""Enforce the worker audit sidecar contract at Phase 7.
|
|
2644
2953
|
|
|
2645
2954
|
The rules themselves live in `okstra_ctl.worker_audit_ledger` so that
|
|
2646
2955
|
`okstra worker-audit-check` can apply the identical checks mid-run, while
|
|
2647
|
-
the worker session is still alive and can fix its own citations.
|
|
2648
|
-
wrapper adds
|
|
2649
|
-
|
|
2956
|
+
the worker session is still alive and can fix its own citations. This
|
|
2957
|
+
wrapper adds the Phase 7 anchor — the run directory and this run's seq, both
|
|
2958
|
+
read off the report path — and routes the citation-ledger findings to
|
|
2959
|
+
*advisories*, which do not fail the run. Mid-run the worker can still fix a
|
|
2960
|
+
citation; at Phase 7 the only remedy left is to void a finished run over a
|
|
2961
|
+
spelling, so the finding is reported instead.
|
|
2650
2962
|
"""
|
|
2651
|
-
|
|
2963
|
+
blocking, ledger = worker_results_audit_findings(
|
|
2652
2964
|
# `report_path` is `runs/<task-type>/reports/final-report-...md`.
|
|
2653
2965
|
report_path.parent.parent,
|
|
2654
2966
|
task_type,
|
|
2655
2967
|
_report_run_seq(report_path),
|
|
2656
|
-
)
|
|
2968
|
+
)
|
|
2969
|
+
failures.extend(blocking)
|
|
2970
|
+
(advisories if advisories is not None else failures).extend(ledger)
|
|
2657
2971
|
|
|
2658
2972
|
|
|
2659
2973
|
def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
|
|
@@ -3080,6 +3394,13 @@ def validate_final_report_data(
|
|
|
3080
3394
|
return data
|
|
3081
3395
|
|
|
3082
3396
|
manifest = run_manifest or {}
|
|
3397
|
+
if data.get("executionIdentityVersion") == 2 or data.get("executionRoles"):
|
|
3398
|
+
try:
|
|
3399
|
+
from okstra_ctl.report_contract import assert_execution_roles_match_manifest
|
|
3400
|
+
|
|
3401
|
+
assert_execution_roles_match_manifest(data, manifest)
|
|
3402
|
+
except ValueError as exc:
|
|
3403
|
+
failures.append(str(exc))
|
|
3083
3404
|
_validate_approval_context(data, manifest, failures, report_path)
|
|
3084
3405
|
_validate_activity_contract_plan_limits(data, manifest, failures)
|
|
3085
3406
|
|
|
@@ -3373,7 +3694,17 @@ _ADVISORY_ONLY_KINDS = {"d"}
|
|
|
3373
3694
|
# Stop reasons that justify promoting a still-broken planner-fixable item to the
|
|
3374
3695
|
# user: the self-fix budget ran out, or a round produced no net resolution so
|
|
3375
3696
|
# further rounds would repeat themselves.
|
|
3376
|
-
|
|
3697
|
+
#
|
|
3698
|
+
# `cause-group-recurrence` is the legacy spelling of that same exhaustion
|
|
3699
|
+
# (plan-body-verification.md "Loop termination"). A pre-activity-contract report
|
|
3700
|
+
# carrying it is a report whose loop stopped because the cause kept recurring —
|
|
3701
|
+
# refusing it here left such a run with no exit at all: the loop may not run
|
|
3702
|
+
# again, and the surviving item may not be promoted either.
|
|
3703
|
+
# `_validate_activity_contract_plan_limits` still forbids a NEW v1 run from
|
|
3704
|
+
# emitting it, so this admits the legacy shape without reopening it.
|
|
3705
|
+
_SELF_FIX_EXHAUSTED_REASONS = frozenset(
|
|
3706
|
+
{"max-rounds-reached", "no-progress", "cause-group-recurrence"}
|
|
3707
|
+
)
|
|
3377
3708
|
|
|
3378
3709
|
|
|
3379
3710
|
def _is_variation_point_item(item: dict) -> bool:
|
|
@@ -4758,10 +5089,11 @@ def _items_resolved_in_round(plan_items: object, round_number: int) -> set[str]:
|
|
|
4758
5089
|
def _detect_self_fix_recurrence(pbv: dict) -> list[str]:
|
|
4759
5090
|
"""Rounds that re-target ground the previous round already worked, unresolved.
|
|
4760
5091
|
|
|
4761
|
-
`no-progress`
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
5092
|
+
`no-progress` is judged at the round's end from what it resolved. Repeating
|
|
5093
|
+
the previous round's *unresolved* remainder is the same conclusion reached
|
|
5094
|
+
one dispatch earlier — the observed shape was two rounds spent on one
|
|
5095
|
+
identical seven-item set. This names that shape so the loop can exit on it
|
|
5096
|
+
rather than paying for the round that proves it.
|
|
4765
5097
|
|
|
4766
5098
|
Advisory only. Narrowing onto what the last round genuinely left open is
|
|
4767
5099
|
legitimate progress, and the rule separating that from re-digging the same
|
|
@@ -4790,8 +5122,8 @@ def _detect_self_fix_recurrence(pbv: dict) -> list[str]:
|
|
|
4790
5122
|
f"self-fix round {round_number} re-targets only items round "
|
|
4791
5123
|
f"{round_number - 1} left unresolved ({', '.join(sorted(current))}) "
|
|
4792
5124
|
"— the previous round's correction did not move this cause. "
|
|
4793
|
-
"Consider exiting with `
|
|
4794
|
-
"
|
|
5125
|
+
"Consider exiting with `no-progress` instead of spending the "
|
|
5126
|
+
"remaining budget on the same ground."
|
|
4795
5127
|
)
|
|
4796
5128
|
return warnings
|
|
4797
5129
|
|
|
@@ -7582,13 +7914,6 @@ def _validate_stage_has_requirement(data: dict, failures: list[str]) -> None:
|
|
|
7582
7914
|
)
|
|
7583
7915
|
|
|
7584
7916
|
|
|
7585
|
-
_FINAL_VERIFICATION_ROUTING_TOKEN_RE = re.compile(
|
|
7586
|
-
r"(?<![A-Za-z-])(?:release-handoff\(stage-group\)|release-handoff|done|"
|
|
7587
|
-
r"implementation|error-analysis|implementation-option-selection|"
|
|
7588
|
-
r"implementation-planning)(?![A-Za-z-])"
|
|
7589
|
-
)
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
7917
|
def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
|
|
7593
7918
|
"""Enforce verdict ↔ blocker/condition/routing consistency on the
|
|
7594
7919
|
final-verification data.json (SSOT). The schema guarantees field SHAPE;
|
|
@@ -7604,15 +7929,15 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
|
|
|
7604
7929
|
blockers = fv.get("acceptanceBlockers") or []
|
|
7605
7930
|
conditions = verdict.get("conditionalAcceptanceConditions") or []
|
|
7606
7931
|
routing_value = fv.get("routingRecommendation")
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
if routing_token is None:
|
|
7932
|
+
routing_token = ""
|
|
7933
|
+
if isinstance(routing_value, dict):
|
|
7934
|
+
routing_token = str(routing_value.get("target") or "")
|
|
7935
|
+
if not routing_token:
|
|
7612
7936
|
failures.append(
|
|
7613
|
-
"final-verification: routingRecommendation must
|
|
7614
|
-
"supported routing
|
|
7937
|
+
"final-verification: routingRecommendation.target must name exactly one "
|
|
7938
|
+
"supported routing target."
|
|
7615
7939
|
)
|
|
7940
|
+
routing_token = None
|
|
7616
7941
|
|
|
7617
7942
|
if token == "accepted" and blockers:
|
|
7618
7943
|
failures.append(
|
|
@@ -7898,11 +8223,16 @@ def _validate_session_conformance(
|
|
|
7898
8223
|
task_type: str,
|
|
7899
8224
|
claude_projects_dir: str | None,
|
|
7900
8225
|
failures: list[str],
|
|
8226
|
+
advisories: list[str] | None = None,
|
|
7901
8227
|
) -> None:
|
|
7902
8228
|
"""prompts/lead/okstra-lead-contract.md의 PROGRESS / activity / heartbeat /
|
|
7903
8229
|
implementation entry guard 사후 검사를 위임하고 실패를
|
|
7904
8230
|
``session-conformance: `` 접두로 folding 한다. 설계:
|
|
7905
8231
|
docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md
|
|
8232
|
+
|
|
8233
|
+
PROGRESS 서술 라인 누락은 ``result.advisories`` 로 돌아오며 run 을
|
|
8234
|
+
실패시키지 않는다 — 그 라인을 쓸 세션은 이 검사가 도는 시점에 이미 끝나 있어
|
|
8235
|
+
남은 구제책이 phase 폐기뿐이다.
|
|
7906
8236
|
"""
|
|
7907
8237
|
_validators_dir = Path(__file__).resolve().parent
|
|
7908
8238
|
if str(_validators_dir) not in sys.path:
|
|
@@ -7924,6 +8254,9 @@ def _validate_session_conformance(
|
|
|
7924
8254
|
claude_projects_dir=Path(claude_projects_dir) if claude_projects_dir else None,
|
|
7925
8255
|
)
|
|
7926
8256
|
failures.extend(f"session-conformance: {err}" for err in result.errors)
|
|
8257
|
+
(advisories if advisories is not None else failures).extend(
|
|
8258
|
+
f"session-conformance: {note}" for note in result.advisories
|
|
8259
|
+
)
|
|
7927
8260
|
|
|
7928
8261
|
|
|
7929
8262
|
def _validate_forbidden_actions(
|
|
@@ -8677,8 +9010,21 @@ def main() -> int:
|
|
|
8677
9010
|
print(f"validate-run: Phase 7 auto-recovery skipped — {msg}", file=sys.stderr)
|
|
8678
9011
|
|
|
8679
9012
|
failures: list[str] = []
|
|
9013
|
+
# Findings that are reported but do not fail the run.
|
|
9014
|
+
#
|
|
9015
|
+
# The line is whether a remedy still exists at Phase 7. A defect in the
|
|
9016
|
+
# report is fixable — re-author it and re-render. A defect in what a
|
|
9017
|
+
# *finished session* wrote down is not: the worker that would restate its
|
|
9018
|
+
# citation and the lead pane that would emit its PROGRESS line have both
|
|
9019
|
+
# ended, so the only remaining move is to void the phase. That blocks the
|
|
9020
|
+
# next phase without establishing that any conclusion in the report is
|
|
9021
|
+
# wrong. Those findings are surfaced and carried in the run manifest
|
|
9022
|
+
# instead — currently the worker citation ledger and the PROGRESS narration
|
|
9023
|
+
# lines.
|
|
9024
|
+
advisories: list[str] = []
|
|
8680
9025
|
if autofix_state == "accuracy-failed":
|
|
8681
9026
|
failures.extend(autofix_messages)
|
|
9027
|
+
_validate_execution_identity_v2(run_manifest, failures)
|
|
8682
9028
|
contract = extract_contract(run_manifest, task_manifest, failures)
|
|
8683
9029
|
concurrent_run_authorized = bool(
|
|
8684
9030
|
(run_manifest.get("concurrentRun") or {}).get("detected")
|
|
@@ -8743,7 +9089,7 @@ def main() -> int:
|
|
|
8743
9089
|
failures,
|
|
8744
9090
|
)
|
|
8745
9091
|
if task_type:
|
|
8746
|
-
validate_worker_results_audit(report_path, task_type, failures)
|
|
9092
|
+
validate_worker_results_audit(report_path, task_type, failures, advisories)
|
|
8747
9093
|
_validate_session_conformance(
|
|
8748
9094
|
team_state,
|
|
8749
9095
|
team_state_path,
|
|
@@ -8753,6 +9099,7 @@ def main() -> int:
|
|
|
8753
9099
|
task_type,
|
|
8754
9100
|
args.claude_projects_dir,
|
|
8755
9101
|
failures,
|
|
9102
|
+
advisories,
|
|
8756
9103
|
)
|
|
8757
9104
|
_validate_forbidden_actions(
|
|
8758
9105
|
team_state,
|
|
@@ -8850,6 +9197,7 @@ def main() -> int:
|
|
|
8850
9197
|
validation_status,
|
|
8851
9198
|
failures,
|
|
8852
9199
|
report_data=validation_data,
|
|
9200
|
+
advisories=advisories,
|
|
8853
9201
|
)
|
|
8854
9202
|
|
|
8855
9203
|
write_json(team_state_path, team_state)
|
|
@@ -8876,10 +9224,14 @@ def main() -> int:
|
|
|
8876
9224
|
+ "\n"
|
|
8877
9225
|
)
|
|
8878
9226
|
|
|
9227
|
+
for note in advisories:
|
|
9228
|
+
print(f"validate-run: advisory — {note}", file=sys.stderr)
|
|
9229
|
+
|
|
8879
9230
|
result = {
|
|
8880
9231
|
"validationStatus": validation_status,
|
|
8881
9232
|
"finalRunStatus": run_manifest.get("status"),
|
|
8882
9233
|
"failures": failures,
|
|
9234
|
+
"advisories": advisories,
|
|
8883
9235
|
"teamStatePath": str(team_state_path),
|
|
8884
9236
|
"reportPath": str(report_path),
|
|
8885
9237
|
}
|