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
|
@@ -43,14 +43,16 @@ from okstra_project.dirs import tasks_root as _okstra_tasks_root # noqa: E402
|
|
|
43
43
|
from okstra_project.resolver import resolve_architecture # noqa: E402
|
|
44
44
|
|
|
45
45
|
from okstra_ctl.conformance import ( # noqa: E402
|
|
46
|
-
CAPABILITY_WHITELIST,
|
|
47
46
|
detect_surfaces,
|
|
48
47
|
evaluate_conformance,
|
|
49
48
|
manifest_required_surfaces,
|
|
49
|
+
normalize_conformance_script as _normalize_conformance_script,
|
|
50
|
+
parse_conformance_tests as _parse_conformance_tests,
|
|
50
51
|
qa_result_from_dict,
|
|
51
52
|
validate_conformance_manifest,
|
|
52
53
|
)
|
|
53
54
|
from okstra_ctl.paths import RunRef # noqa: E402
|
|
55
|
+
from okstra_ctl.report_contract import CURRENT_REPORT_SCHEMA_VERSION # noqa: E402
|
|
54
56
|
from okstra_ctl.domain.host import HostNotRegistered # noqa: E402
|
|
55
57
|
from okstra_ctl.registry.host_registry import default_host_registry # noqa: E402
|
|
56
58
|
from okstra_ctl.build_tools import ( # noqa: E402
|
|
@@ -120,15 +122,21 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
|
|
|
120
122
|
PromptRecord,
|
|
121
123
|
validate_initial_prompt_records,
|
|
122
124
|
)
|
|
125
|
+
from okstra_ctl.domain.role import RoleCatalogError, role_for_duty # noqa: E402
|
|
123
126
|
from okstra_ctl.agent_invocation import ( # noqa: E402
|
|
124
127
|
AgentInvocationError,
|
|
125
128
|
agent_model_assignment_from_payload,
|
|
129
|
+
invocation_execution_identity_from_manifest,
|
|
126
130
|
verify_agent_invocation,
|
|
127
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
|
+
)
|
|
128
136
|
from okstra_ctl.lead_events import LeadEventParseError, read_lead_events # noqa: E402
|
|
129
137
|
from okstra_ctl.worker_audit_ledger import ( # noqa: E402
|
|
130
138
|
READING_CONFIRMATION_HEADING_RE,
|
|
131
|
-
|
|
139
|
+
worker_results_audit_findings,
|
|
132
140
|
)
|
|
133
141
|
from validate_analysis_report import validate_analysis_report # noqa: E402
|
|
134
142
|
from okstra_ctl.convergence_engine import ( # noqa: E402
|
|
@@ -141,7 +149,7 @@ from okstra_ctl.convergence_provenance import ( # noqa: E402
|
|
|
141
149
|
|
|
142
150
|
TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
143
151
|
ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
|
|
144
|
-
WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed"
|
|
152
|
+
WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed"}
|
|
145
153
|
_AGENT_DISPATCH_DIGEST_KEYS = (
|
|
146
154
|
"catalogDigest",
|
|
147
155
|
"assignmentDigest",
|
|
@@ -149,6 +157,30 @@ _AGENT_DISPATCH_DIGEST_KEYS = (
|
|
|
149
157
|
"instructionDigest",
|
|
150
158
|
"promptDigest",
|
|
151
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}")
|
|
152
184
|
|
|
153
185
|
|
|
154
186
|
def _validate_agent_dispatch_contract(
|
|
@@ -167,6 +199,28 @@ def _validate_agent_dispatch_contract(
|
|
|
167
199
|
if not isinstance(assignments, Mapping):
|
|
168
200
|
failures.append("agent invocation metadata is missing: invocationAssignments")
|
|
169
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()
|
|
170
224
|
worker_dispatches = [
|
|
171
225
|
row for row in (team_state.get("workerDispatches") or [])
|
|
172
226
|
if isinstance(row, Mapping) and row.get("invocationId")
|
|
@@ -177,13 +231,15 @@ def _validate_agent_dispatch_contract(
|
|
|
177
231
|
]
|
|
178
232
|
dispatches = [*worker_dispatches, *agent_dispatches]
|
|
179
233
|
ids: dict[str, Mapping[str, Any]] = {}
|
|
234
|
+
seen_dispatch_ids: set[str] = set()
|
|
180
235
|
for row in dispatches:
|
|
181
236
|
dispatch_id = str(row.get("dispatchId") or "").strip()
|
|
182
237
|
missing = [
|
|
183
238
|
key for key in (
|
|
184
|
-
"dispatchId", "
|
|
239
|
+
"dispatchId", "audience", "invocationId",
|
|
185
240
|
"assignmentRef", "promptMetadataPath", "modelExecutionValue",
|
|
186
241
|
"enforcementMode", *_AGENT_DISPATCH_DIGEST_KEYS,
|
|
242
|
+
*(_V2_AGENT_DISPATCH_IDENTITY_KEYS if uses_v2_identity else ("workerId",)),
|
|
187
243
|
)
|
|
188
244
|
if not str(row.get(key) or "").strip()
|
|
189
245
|
]
|
|
@@ -193,10 +249,43 @@ def _validate_agent_dispatch_contract(
|
|
|
193
249
|
+ ", ".join(missing)
|
|
194
250
|
)
|
|
195
251
|
continue
|
|
196
|
-
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:
|
|
197
286
|
failures.append(f"agent dispatch ID is duplicated: {dispatch_id}")
|
|
198
287
|
continue
|
|
199
|
-
|
|
288
|
+
seen_dispatch_ids.add(dispatch_id)
|
|
200
289
|
assignment_ref = str(row["assignmentRef"])
|
|
201
290
|
try:
|
|
202
291
|
assignment = agent_model_assignment_from_payload(
|
|
@@ -205,6 +294,118 @@ def _validate_agent_dispatch_contract(
|
|
|
205
294
|
except AgentInvocationError as exc:
|
|
206
295
|
failures.append(f"agent dispatch {dispatch_id}: {exc}")
|
|
207
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
|
|
208
409
|
metadata_path = _resolve_prompt_record_path(
|
|
209
410
|
project_root, str(row["promptMetadataPath"])
|
|
210
411
|
)
|
|
@@ -214,9 +415,20 @@ def _validate_agent_dispatch_contract(
|
|
|
214
415
|
expected_run_manifest_path=run_manifest_path,
|
|
215
416
|
expected_assignment=assignment,
|
|
216
417
|
expected_invocation_id=str(row["invocationId"]),
|
|
217
|
-
expected_worker_id=
|
|
418
|
+
expected_worker_id=(
|
|
419
|
+
None if uses_v2_identity else str(row["workerId"])
|
|
420
|
+
),
|
|
218
421
|
expected_assignment_ref=assignment_ref,
|
|
219
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
|
+
),
|
|
220
432
|
)
|
|
221
433
|
failures.extend(
|
|
222
434
|
f"agent dispatch {dispatch_id}: {error}" for error in errors
|
|
@@ -256,10 +468,23 @@ def _validate_agent_dispatch_contract(
|
|
|
256
468
|
else "host-native prompt delivery cannot be marked verified"
|
|
257
469
|
)
|
|
258
470
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
}
|
|
263
488
|
if not lead_ids:
|
|
264
489
|
failures.append("accepted lead result has no lead dispatch record")
|
|
265
490
|
|
|
@@ -275,6 +500,22 @@ def _validate_agent_dispatch_contract(
|
|
|
275
500
|
if dispatch_id not in ids or not result_path:
|
|
276
501
|
failures.append("agent result link has no matching dispatch record")
|
|
277
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
|
|
278
519
|
dispatch_link_counts[dispatch_id] = dispatch_link_counts.get(dispatch_id, 0) + 1
|
|
279
520
|
previous = paths.setdefault(result_path, dispatch_id)
|
|
280
521
|
if previous != dispatch_id:
|
|
@@ -294,7 +535,11 @@ def _validate_agent_dispatch_contract(
|
|
|
294
535
|
worker_result = str(worker.get("resultPath") or "").strip()
|
|
295
536
|
worker_dispatch_ids = {
|
|
296
537
|
dispatch_id for dispatch_id, row in ids.items()
|
|
297
|
-
if
|
|
538
|
+
if (
|
|
539
|
+
row.get("assignmentRef") == f"initial/{worker_id}"
|
|
540
|
+
if uses_v2_identity
|
|
541
|
+
else row.get("workerId") == worker_id
|
|
542
|
+
)
|
|
298
543
|
}
|
|
299
544
|
if worker_id and not worker_dispatch_ids:
|
|
300
545
|
failures.append(
|
|
@@ -588,13 +833,16 @@ def update_validation_metadata(
|
|
|
588
833
|
validation_status: str,
|
|
589
834
|
failures: list[str],
|
|
590
835
|
report_data: Mapping[str, Any] | None = None,
|
|
836
|
+
advisories: list[str] | None = None,
|
|
591
837
|
) -> None:
|
|
592
838
|
checked_at = utc_now()
|
|
839
|
+
advisories = list(advisories or [])
|
|
593
840
|
|
|
594
841
|
team_state.setdefault("validator", {})
|
|
595
842
|
team_state["validator"]["status"] = validation_status
|
|
596
843
|
team_state["validator"]["lastValidatedAt"] = checked_at
|
|
597
844
|
team_state["validator"]["failures"] = failures
|
|
845
|
+
team_state["validator"]["advisories"] = advisories
|
|
598
846
|
|
|
599
847
|
run_manifest.setdefault("validation", {})
|
|
600
848
|
run_manifest["validation"]["required"] = True
|
|
@@ -602,6 +850,8 @@ def update_validation_metadata(
|
|
|
602
850
|
run_manifest["validation"]["lastCheckedAt"] = checked_at
|
|
603
851
|
run_manifest["validation"]["passed"] = validation_status == "passed"
|
|
604
852
|
run_manifest["validation"]["failures"] = failures
|
|
853
|
+
# Reported, not blocking — a passing run can carry these.
|
|
854
|
+
run_manifest["validation"]["advisories"] = advisories
|
|
605
855
|
run_manifest["status"] = (
|
|
606
856
|
"completed" if validation_status == "passed" else "contract-violated"
|
|
607
857
|
)
|
|
@@ -707,35 +957,127 @@ def _persisted_prompt_records(
|
|
|
707
957
|
project_root: Path,
|
|
708
958
|
selected_ids: set[str],
|
|
709
959
|
team_state: dict,
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
960
|
+
run_manifest: Mapping[str, Any],
|
|
961
|
+
) -> tuple[list[PromptRecord], list[str]]:
|
|
962
|
+
uses_v2_identity = (
|
|
963
|
+
run_manifest.get("schemaVersion") == "2.0"
|
|
964
|
+
and run_manifest.get("executionIdentityVersion") == 2
|
|
714
965
|
)
|
|
966
|
+
dispatches = team_state.get("workerDispatches")
|
|
967
|
+
if uses_v2_identity:
|
|
968
|
+
source = [
|
|
969
|
+
*(dispatches if isinstance(dispatches, list) else []),
|
|
970
|
+
*(
|
|
971
|
+
team_state.get("agentDispatches")
|
|
972
|
+
if isinstance(team_state.get("agentDispatches"), list)
|
|
973
|
+
else []
|
|
974
|
+
),
|
|
975
|
+
]
|
|
976
|
+
else:
|
|
977
|
+
source = dispatches if isinstance(dispatches, list) and dispatches else (
|
|
978
|
+
team_state.get("workers") or []
|
|
979
|
+
)
|
|
715
980
|
records: list[PromptRecord] = []
|
|
716
981
|
seen: set[tuple[str, str, str]] = set()
|
|
982
|
+
errors: list[str] = []
|
|
983
|
+
role_executions = {
|
|
984
|
+
str(row.get("roleExecutionRef") or ""): row
|
|
985
|
+
for row in run_manifest.get("roleExecutions") or []
|
|
986
|
+
if isinstance(row, Mapping)
|
|
987
|
+
} if uses_v2_identity else {}
|
|
717
988
|
for worker in source:
|
|
718
989
|
if not isinstance(worker, dict):
|
|
719
990
|
continue
|
|
720
|
-
|
|
991
|
+
assignment_ref = str(worker.get("assignmentRef") or "").strip()
|
|
992
|
+
worker_id = (
|
|
993
|
+
assignment_ref.removeprefix("initial/")
|
|
994
|
+
if uses_v2_identity and assignment_ref.startswith("initial/")
|
|
995
|
+
else str(worker.get("workerId") or "").strip()
|
|
996
|
+
)
|
|
721
997
|
prompt_value = str(worker.get("promptPath") or "").strip()
|
|
722
|
-
if worker_id not in selected_ids
|
|
998
|
+
if worker_id not in selected_ids:
|
|
723
999
|
continue
|
|
724
|
-
|
|
1000
|
+
if not prompt_value:
|
|
1001
|
+
errors.append(f"{worker_id}: persisted initial prompt path is missing")
|
|
1002
|
+
continue
|
|
1003
|
+
dispatch_kind = str(
|
|
1004
|
+
worker.get("kind") or worker.get("dispatchKind") or ""
|
|
1005
|
+
).strip()
|
|
725
1006
|
if not dispatch_kind:
|
|
726
1007
|
dispatch_kind = _fallback_dispatch_kind(prompt_value)
|
|
1008
|
+
duty_id = ""
|
|
1009
|
+
if uses_v2_identity:
|
|
1010
|
+
execution = role_executions.get(
|
|
1011
|
+
str(worker.get("roleExecutionRef") or "").strip()
|
|
1012
|
+
)
|
|
1013
|
+
participant_ref = str(worker.get("participantRef") or "").strip()
|
|
1014
|
+
duty_id = str(worker.get("dutyId") or "").strip()
|
|
1015
|
+
audience = str(worker.get("audience") or "").strip()
|
|
1016
|
+
assignments = run_manifest.get("invocationAssignments")
|
|
1017
|
+
assignment = (
|
|
1018
|
+
assignments.get(assignment_ref)
|
|
1019
|
+
if isinstance(assignments, Mapping)
|
|
1020
|
+
else None
|
|
1021
|
+
)
|
|
1022
|
+
if not isinstance(execution, Mapping):
|
|
1023
|
+
errors.append(
|
|
1024
|
+
f"{worker_id}: persisted prompt has unknown roleExecutionRef"
|
|
1025
|
+
)
|
|
1026
|
+
continue
|
|
1027
|
+
try:
|
|
1028
|
+
expected_role = role_for_duty(duty_id)
|
|
1029
|
+
except RoleCatalogError as exc:
|
|
1030
|
+
errors.append(f"{worker_id}: {exc}")
|
|
1031
|
+
continue
|
|
1032
|
+
if (
|
|
1033
|
+
execution.get("participantRef") != participant_ref
|
|
1034
|
+
or execution.get("role") != expected_role
|
|
1035
|
+
or execution.get("executionLabel") != worker.get("executionLabel")
|
|
1036
|
+
or not isinstance(assignment, Mapping)
|
|
1037
|
+
or execution.get("provider") != assignment.get("provider")
|
|
1038
|
+
or not isinstance(execution.get("binding"), Mapping)
|
|
1039
|
+
or execution["binding"].get("resolvedExecutionValue")
|
|
1040
|
+
!= assignment.get("modelExecutionValue")
|
|
1041
|
+
):
|
|
1042
|
+
errors.append(
|
|
1043
|
+
f"{worker_id}: persisted prompt identity does not match role execution"
|
|
1044
|
+
)
|
|
1045
|
+
continue
|
|
1046
|
+
if audience != duty_id:
|
|
1047
|
+
errors.append(
|
|
1048
|
+
f"{worker_id}: persisted prompt audience does not match duty"
|
|
1049
|
+
)
|
|
1050
|
+
continue
|
|
727
1051
|
identity = (worker_id, dispatch_kind, prompt_value)
|
|
728
1052
|
if identity in seen:
|
|
729
1053
|
continue
|
|
730
1054
|
seen.add(identity)
|
|
1055
|
+
metadata_value = str(worker.get("promptMetadataPath") or "").strip()
|
|
731
1056
|
records.append(
|
|
732
1057
|
PromptRecord(
|
|
733
1058
|
worker_id=worker_id,
|
|
734
1059
|
dispatch_kind=dispatch_kind,
|
|
735
1060
|
path=_resolve_prompt_record_path(project_root, prompt_value),
|
|
1061
|
+
expected_model=(
|
|
1062
|
+
str(worker.get("modelExecutionValue") or "").strip() or None
|
|
1063
|
+
if uses_v2_identity else None
|
|
1064
|
+
),
|
|
1065
|
+
metadata_path=(
|
|
1066
|
+
_resolve_prompt_record_path(project_root, metadata_value)
|
|
1067
|
+
if uses_v2_identity and metadata_value else None
|
|
1068
|
+
),
|
|
1069
|
+
expected_duty_audience=(duty_id if uses_v2_identity else None),
|
|
736
1070
|
)
|
|
737
1071
|
)
|
|
738
|
-
|
|
1072
|
+
if uses_v2_identity:
|
|
1073
|
+
recorded_ids = {record.worker_id for record in records}
|
|
1074
|
+
missing_ids = sorted(selected_ids - recorded_ids)
|
|
1075
|
+
if missing_ids:
|
|
1076
|
+
errors.append(
|
|
1077
|
+
"no persisted initial prompt records for selected role executions: "
|
|
1078
|
+
+ ", ".join(missing_ids)
|
|
1079
|
+
)
|
|
1080
|
+
return records, errors
|
|
739
1081
|
|
|
740
1082
|
|
|
741
1083
|
def _markdown_section(text: str, heading: str) -> str:
|
|
@@ -861,15 +1203,16 @@ def _validate_initial_analysis_prompts(
|
|
|
861
1203
|
team_state = data.get("teamState") or {}
|
|
862
1204
|
manifest = dict(run_manifest)
|
|
863
1205
|
manifest["taskType"] = task_type
|
|
864
|
-
records = _persisted_prompt_records(
|
|
1206
|
+
records, collection_errors = _persisted_prompt_records(
|
|
865
1207
|
project_root=project_root,
|
|
866
1208
|
selected_ids=selected_ids,
|
|
867
1209
|
team_state=team_state,
|
|
1210
|
+
run_manifest=run_manifest,
|
|
868
1211
|
)
|
|
869
|
-
errors = validate_initial_prompt_records(
|
|
1212
|
+
errors = [*collection_errors, *validate_initial_prompt_records(
|
|
870
1213
|
manifest=manifest,
|
|
871
1214
|
records=records,
|
|
872
|
-
)
|
|
1215
|
+
)]
|
|
873
1216
|
if task_type == "improvement-discovery":
|
|
874
1217
|
errors.extend(
|
|
875
1218
|
_validate_improvement_primary_assignments(
|
|
@@ -1682,35 +2025,6 @@ def _approved_plan_stage_entry(
|
|
|
1682
2025
|
}
|
|
1683
2026
|
|
|
1684
2027
|
|
|
1685
|
-
_CONFORMANCE_TESTS_RE = re.compile(
|
|
1686
|
-
r"^(?P<script>\S+)\s+\(requires=\[(?P<requires>[^\]]+)\]\)$"
|
|
1687
|
-
)
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
def _normalize_conformance_script(script: str) -> str:
|
|
1691
|
-
prefix = "<task_root>/"
|
|
1692
|
-
return script[len(prefix):] if script.startswith(prefix) else script
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
def _parse_conformance_tests(value: object) -> tuple[str, frozenset[str]] | None:
|
|
1696
|
-
"""Parse `<script> (requires=[cap,...])` from a stage declaration."""
|
|
1697
|
-
if not isinstance(value, str):
|
|
1698
|
-
return None
|
|
1699
|
-
match = _CONFORMANCE_TESTS_RE.fullmatch(value.strip())
|
|
1700
|
-
if match is None:
|
|
1701
|
-
return None
|
|
1702
|
-
script = _normalize_conformance_script(match.group("script"))
|
|
1703
|
-
capabilities = [part.strip() for part in match.group("requires").split(",")]
|
|
1704
|
-
if (
|
|
1705
|
-
not script
|
|
1706
|
-
or any(not capability for capability in capabilities)
|
|
1707
|
-
or len(set(capabilities)) != len(capabilities)
|
|
1708
|
-
or any(capability not in CAPABILITY_WHITELIST for capability in capabilities)
|
|
1709
|
-
):
|
|
1710
|
-
return None
|
|
1711
|
-
return script, frozenset(capabilities)
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
2028
|
def _approved_plan_conformance_manifest(
|
|
1715
2029
|
approved_plan_path: Path,
|
|
1716
2030
|
task_root: Path,
|
|
@@ -2355,7 +2669,7 @@ def _validate_selfmock(report_path: Path, failures: list[str]) -> None:
|
|
|
2355
2669
|
read the flat `self-mock.json`.
|
|
2356
2670
|
|
|
2357
2671
|
Only the implementation template renders the §5.7.3 diff summary
|
|
2358
|
-
(`templates/reports/final-report.template.md:589`); a final-verification report
|
|
2672
|
+
(`templates/reports/final-report-v2.template.md:589`); a final-verification report
|
|
2359
2673
|
quotes the diff as a blockquote instead, so this gate is vacuous there by
|
|
2360
2674
|
design — self-mock is enforced at the implementation stage, and
|
|
2361
2675
|
final-verification is a read-only re-verify that adds no test files.
|
|
@@ -2665,22 +2979,30 @@ def _report_run_seq(report_path: Path) -> str | None:
|
|
|
2665
2979
|
|
|
2666
2980
|
|
|
2667
2981
|
def validate_worker_results_audit(
|
|
2668
|
-
report_path: Path,
|
|
2982
|
+
report_path: Path,
|
|
2983
|
+
task_type: str,
|
|
2984
|
+
failures: list[str],
|
|
2985
|
+
advisories: list[str] | None = None,
|
|
2669
2986
|
) -> None:
|
|
2670
2987
|
"""Enforce the worker audit sidecar contract at Phase 7.
|
|
2671
2988
|
|
|
2672
2989
|
The rules themselves live in `okstra_ctl.worker_audit_ledger` so that
|
|
2673
2990
|
`okstra worker-audit-check` can apply the identical checks mid-run, while
|
|
2674
|
-
the worker session is still alive and can fix its own citations.
|
|
2675
|
-
wrapper adds
|
|
2676
|
-
|
|
2991
|
+
the worker session is still alive and can fix its own citations. This
|
|
2992
|
+
wrapper adds the Phase 7 anchor — the run directory and this run's seq, both
|
|
2993
|
+
read off the report path — and routes the citation-ledger findings to
|
|
2994
|
+
*advisories*, which do not fail the run. Mid-run the worker can still fix a
|
|
2995
|
+
citation; at Phase 7 the only remedy left is to void a finished run over a
|
|
2996
|
+
spelling, so the finding is reported instead.
|
|
2677
2997
|
"""
|
|
2678
|
-
|
|
2998
|
+
blocking, ledger = worker_results_audit_findings(
|
|
2679
2999
|
# `report_path` is `runs/<task-type>/reports/final-report-...md`.
|
|
2680
3000
|
report_path.parent.parent,
|
|
2681
3001
|
task_type,
|
|
2682
3002
|
_report_run_seq(report_path),
|
|
2683
|
-
)
|
|
3003
|
+
)
|
|
3004
|
+
failures.extend(blocking)
|
|
3005
|
+
(advisories if advisories is not None else failures).extend(ledger)
|
|
2684
3006
|
|
|
2685
3007
|
|
|
2686
3008
|
def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
|
|
@@ -2717,65 +3039,6 @@ def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
|
|
|
2717
3039
|
)
|
|
2718
3040
|
|
|
2719
3041
|
|
|
2720
|
-
PLANNING_REQUIRED_SECTIONS = (
|
|
2721
|
-
"Option Candidates",
|
|
2722
|
-
"Trade-off",
|
|
2723
|
-
"Recommended Option",
|
|
2724
|
-
"Stage Map",
|
|
2725
|
-
"Stepwise Execution Order",
|
|
2726
|
-
"Dependency",
|
|
2727
|
-
"Validation Checklist",
|
|
2728
|
-
"Rollback",
|
|
2729
|
-
"Requirement Coverage",
|
|
2730
|
-
"Plan Body Verification",
|
|
2731
|
-
"Cross-Project Dependencies",
|
|
2732
|
-
"Decision Drafts",
|
|
2733
|
-
)
|
|
2734
|
-
|
|
2735
|
-
# §5.7 implementation deliverables — substring scan against report body.
|
|
2736
|
-
IMPLEMENTATION_REQUIRED_SECTIONS = (
|
|
2737
|
-
"Approved Plan Reference",
|
|
2738
|
-
"Commit List",
|
|
2739
|
-
"Diff Summary",
|
|
2740
|
-
"Out-of-plan Edits",
|
|
2741
|
-
"Stage Sidecar Evidence",
|
|
2742
|
-
"Validation Evidence",
|
|
2743
|
-
"Verifier Results",
|
|
2744
|
-
"Rollback Verification",
|
|
2745
|
-
"Manual User Test (Draft)",
|
|
2746
|
-
"Routing Recommendation",
|
|
2747
|
-
)
|
|
2748
|
-
|
|
2749
|
-
# §5.8 final-verification deliverables — substring scan against report body.
|
|
2750
|
-
FINAL_VERIFICATION_REQUIRED_SECTIONS = (
|
|
2751
|
-
"Source Implementation Report",
|
|
2752
|
-
"Acceptance Blockers",
|
|
2753
|
-
"Residual Risk",
|
|
2754
|
-
"Validation Evidence",
|
|
2755
|
-
"Read-only Command Log",
|
|
2756
|
-
"Conditional Acceptance Conditions",
|
|
2757
|
-
"Manual User Test Results",
|
|
2758
|
-
"Routing Recommendation",
|
|
2759
|
-
"Could-Not-Verify Roll-up",
|
|
2760
|
-
)
|
|
2761
|
-
|
|
2762
|
-
# Allowed Verdict Token vocabulary, by task-type. `release-handoff` is
|
|
2763
|
-
# author-tagged but reads its entry gate from final-verification's report
|
|
2764
|
-
# and renders `not-applicable` itself.
|
|
2765
|
-
FINAL_VERIFICATION_VERDICT_TOKENS = (
|
|
2766
|
-
"accepted",
|
|
2767
|
-
"conditional-accept",
|
|
2768
|
-
"blocked",
|
|
2769
|
-
)
|
|
2770
|
-
|
|
2771
|
-
# `## 7. Final Verdict` Verdict Token cell — captures the value between
|
|
2772
|
-
# backticks on the `Verdict Token` row. Tolerant to extra column whitespace
|
|
2773
|
-
# and to leading bold/italic markers in the label cell.
|
|
2774
|
-
_FINAL_VERDICT_TOKEN_RE = re.compile(
|
|
2775
|
-
r"^\|[ \t]*\*{0,2}Verdict Token\*{0,2}[ \t]*\|[ \t]*`(?P<value>[^`\n]*)`",
|
|
2776
|
-
re.MULTILINE,
|
|
2777
|
-
)
|
|
2778
|
-
|
|
2779
3042
|
# Verdict Card Verdict Token row (top-of-report at-a-glance). Same shape
|
|
2780
3043
|
# as `_FINAL_VERDICT_TOKEN_RE` but matched against the first occurrence in
|
|
2781
3044
|
# the Verdict Card block, scoped to the body between `## Verdict Card`
|
|
@@ -2786,23 +3049,6 @@ _VERDICT_CARD_BLOCK_RE = re.compile(
|
|
|
2786
3049
|
re.DOTALL | re.MULTILINE,
|
|
2787
3050
|
)
|
|
2788
3051
|
|
|
2789
|
-
# `## 7. Final Verdict` block scope — used to scope the Verdict Token
|
|
2790
|
-
# regex so that we don't accidentally match a Verdict Token row that
|
|
2791
|
-
# lives in the Verdict Card or anywhere else.
|
|
2792
|
-
_FINAL_VERDICT_BLOCK_RE = re.compile(
|
|
2793
|
-
r"^##[ \t]+7\.[ \t]+Final Verdict" + _HEADING_TAIL
|
|
2794
|
-
+ r"\n(?P<body>.*?)(?=^##[ \t]|\Z)",
|
|
2795
|
-
re.DOTALL | re.MULTILINE,
|
|
2796
|
-
)
|
|
2797
|
-
|
|
2798
|
-
# `## 5.6 Release Handoff Deliverables` and `## 5.6.6 Merge Conflict
|
|
2799
|
-
# Probe` are required when task-type == release-handoff. The probe sub-
|
|
2800
|
-
# section was retro-added to the template; old runs that predate it ship
|
|
2801
|
-
# without it, but new runs must include it.
|
|
2802
|
-
_MERGE_CONFLICT_PROBE_HEADING_RE = re.compile(
|
|
2803
|
-
r"^###[ \t]+5\.6\.6[ \t]+Merge Conflict Probe\b", re.MULTILINE
|
|
2804
|
-
)
|
|
2805
|
-
|
|
2806
3052
|
PLAN_VERIFY_GATE_VALUES = (
|
|
2807
3053
|
"passed",
|
|
2808
3054
|
"passed-with-dissent",
|
|
@@ -2810,207 +3056,10 @@ PLAN_VERIFY_GATE_VALUES = (
|
|
|
2810
3056
|
"aborted-non-result",
|
|
2811
3057
|
)
|
|
2812
3058
|
|
|
2813
|
-
# `Gate result:` line in §5.5.9 of the final report — captures the value
|
|
2814
|
-
# token that follows. Tolerates arbitrary markdown formatting between the
|
|
2815
|
-
# label and the value (backticks for inline code, double-asterisks for
|
|
2816
|
-
# bold, colons, hyphens, whitespace). The captured value is then
|
|
2817
|
-
# validated against `PLAN_VERIFY_GATE_VALUES` below so typo'd or unknown
|
|
2818
|
-
# values surface as their own failure rather than silently no-matching.
|
|
2819
|
-
_GATE_RESULT_RE = re.compile(
|
|
2820
|
-
r"Gate result[^A-Za-z\n]+(?P<value>[a-z][a-z\-]+)",
|
|
2821
|
-
re.IGNORECASE,
|
|
2822
|
-
)
|
|
2823
|
-
|
|
2824
|
-
# §5.5.9 Plan Body Verification section body — scopes the gate-result search
|
|
2825
|
-
# so an earlier prose mention of "Gate result" elsewhere in the report cannot
|
|
2826
|
-
# hijack the authoritative value. Matched from the `Plan Body Verification`
|
|
2827
|
-
# heading to the next heading (or end-of-file).
|
|
2828
|
-
_PLAN_BODY_VERIFICATION_BLOCK_RE = re.compile(
|
|
2829
|
-
r"^#{2,}[ \t][^\n]*\bPlan Body Verification\b[^\n]*\n"
|
|
2830
|
-
r"(?P<body>.*?)(?=^#{2,}[ \t]|\Z)",
|
|
2831
|
-
re.DOTALL | re.MULTILINE,
|
|
2832
|
-
)
|
|
2833
|
-
|
|
2834
|
-
# Frontmatter approval flag — `approved: true|false` line inside the
|
|
2835
|
-
# leading `---` YAML block. Mirrors `APPROVED_FRONTMATTER_PATTERN` in
|
|
2836
|
-
# scripts/okstra_ctl/run.py.
|
|
2837
|
-
_APPROVED_FRONTMATTER_RE = re.compile(
|
|
2838
|
-
r"^approved:[ \t]+(true|false)[ \t]*$",
|
|
2839
|
-
re.IGNORECASE | re.MULTILINE,
|
|
2840
|
-
)
|
|
2841
3059
|
# Tolerate a leading UTF-8 BOM and/or blank lines before the opening `---`
|
|
2842
3060
|
# so a hand-edited or differently-rendered report does not silently bypass
|
|
2843
3061
|
# the approved-frontmatter gate (the `.match` would otherwise return None).
|
|
2844
3062
|
_FRONTMATTER_BLOCK_RE = re.compile(r"\A\ufeff?\s*---\n(.*?)\n---\n", re.DOTALL)
|
|
2845
|
-
_REQUIREMENT_COVERAGE_HEADING_RE = re.compile(
|
|
2846
|
-
r"^###[ \t]+(?:5\.5\.8[ \t]+)?Requirement Coverage\b",
|
|
2847
|
-
re.IGNORECASE | re.MULTILINE,
|
|
2848
|
-
)
|
|
2849
|
-
_NEXT_THIRD_LEVEL_HEADING_RE = re.compile(r"^###[ \t]+", re.MULTILINE)
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
def _extract_final_verdict_token(content: str) -> str | None:
|
|
2853
|
-
"""Return the `Verdict Token` cell value from the `## 7. Final Verdict`
|
|
2854
|
-
block, or None when the row is absent. Scoped to §7 so the Verdict
|
|
2855
|
-
Card row (which has the same shape) does not shadow the authoritative
|
|
2856
|
-
value.
|
|
2857
|
-
"""
|
|
2858
|
-
block = _FINAL_VERDICT_BLOCK_RE.search(content)
|
|
2859
|
-
if block is None:
|
|
2860
|
-
return None
|
|
2861
|
-
match = _FINAL_VERDICT_TOKEN_RE.search(block.group("body"))
|
|
2862
|
-
if match is None:
|
|
2863
|
-
return None
|
|
2864
|
-
return match.group("value")
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
# 렌더러는 ID 정의 셀(`<a id="r-001"></a>R-001`)에도 스크롤 앵커를 넣는다.
|
|
2868
|
-
# 셀 정규화 때 그 빈 앵커를 벗겨야 ID 컬럼이 bare 토큰으로 읽힌다
|
|
2869
|
-
# (clarification_items._CELL_ANCHOR_RE 와 동형).
|
|
2870
|
-
_CELL_ANCHOR_RE = re.compile(r'<a id="[^"]*"></a>')
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
def _split_markdown_row(line: str) -> list[str]:
|
|
2874
|
-
return [
|
|
2875
|
-
_CELL_ANCHOR_RE.sub("", cell).strip().strip("`").strip()
|
|
2876
|
-
for cell in _split_pipe_row(line)
|
|
2877
|
-
]
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
def _append_requirement_coverage_failures(
|
|
2881
|
-
content: str,
|
|
2882
|
-
gate_value: str,
|
|
2883
|
-
failures: list[str],
|
|
2884
|
-
) -> None:
|
|
2885
|
-
"""Validate implementation-planning §5.5.8 requirement coverage.
|
|
2886
|
-
|
|
2887
|
-
The table is intentionally lightweight: it cannot prove semantic truth, but
|
|
2888
|
-
it makes requirement-to-plan mapping explicit and blocks publishable plans
|
|
2889
|
-
that admit uncovered requirements.
|
|
2890
|
-
"""
|
|
2891
|
-
heading = _REQUIREMENT_COVERAGE_HEADING_RE.search(content)
|
|
2892
|
-
if heading is None:
|
|
2893
|
-
failures.append(
|
|
2894
|
-
"implementation-planning report is missing `Requirement Coverage` "
|
|
2895
|
-
"section — every task-brief requirement must map to option/stage/step."
|
|
2896
|
-
)
|
|
2897
|
-
return
|
|
2898
|
-
|
|
2899
|
-
rest = content[heading.end():]
|
|
2900
|
-
next_heading = _NEXT_THIRD_LEVEL_HEADING_RE.search(rest)
|
|
2901
|
-
section = rest[: next_heading.start()] if next_heading else rest
|
|
2902
|
-
lines = section.splitlines()
|
|
2903
|
-
|
|
2904
|
-
header_idx = -1
|
|
2905
|
-
headers: list[str] = []
|
|
2906
|
-
for idx, line in enumerate(lines):
|
|
2907
|
-
if not line.lstrip().startswith("|"):
|
|
2908
|
-
continue
|
|
2909
|
-
cells = [c.lower() for c in _split_markdown_row(line)]
|
|
2910
|
-
if "id" in cells and "requirement" in cells and "status" in cells:
|
|
2911
|
-
header_idx = idx
|
|
2912
|
-
headers = cells
|
|
2913
|
-
break
|
|
2914
|
-
if header_idx < 0:
|
|
2915
|
-
failures.append(
|
|
2916
|
-
"implementation-planning Requirement Coverage section has no table "
|
|
2917
|
-
"with `ID`, `Requirement`, and `Status` columns."
|
|
2918
|
-
)
|
|
2919
|
-
return
|
|
2920
|
-
|
|
2921
|
-
id_col = headers.index("id")
|
|
2922
|
-
status_col = headers.index("status")
|
|
2923
|
-
rows: list[tuple[str, str]] = []
|
|
2924
|
-
body_started = False
|
|
2925
|
-
for line in lines[header_idx + 1:]:
|
|
2926
|
-
if not line.lstrip().startswith("|"):
|
|
2927
|
-
if body_started:
|
|
2928
|
-
break
|
|
2929
|
-
continue
|
|
2930
|
-
if _is_markdown_separator(line):
|
|
2931
|
-
body_started = True
|
|
2932
|
-
continue
|
|
2933
|
-
if not body_started:
|
|
2934
|
-
continue
|
|
2935
|
-
cells = _split_markdown_row(line)
|
|
2936
|
-
if max(id_col, status_col) >= len(cells):
|
|
2937
|
-
failures.append(
|
|
2938
|
-
"implementation-planning Requirement Coverage table has a "
|
|
2939
|
-
f"malformed row: `{line.strip()}`"
|
|
2940
|
-
)
|
|
2941
|
-
continue
|
|
2942
|
-
rows.append((cells[id_col], cells[status_col].lower()))
|
|
2943
|
-
|
|
2944
|
-
if not rows:
|
|
2945
|
-
failures.append(
|
|
2946
|
-
"implementation-planning Requirement Coverage table has no data rows."
|
|
2947
|
-
)
|
|
2948
|
-
return
|
|
2949
|
-
|
|
2950
|
-
for row_id, status in rows:
|
|
2951
|
-
# `status` is already lower-cased at parse time, so the blocker form
|
|
2952
|
-
# arrives as `blocked c-001`; match lower-case here even though the
|
|
2953
|
-
# canonical authored form (and the remedy message) is `blocked C-NNN`.
|
|
2954
|
-
if not re.fullmatch(
|
|
2955
|
-
r"covered|gap|blocked c-\d{3,}|"
|
|
2956
|
-
r"documented-deviation — refs: (?:c-\d{3,}|d-\d{4,})"
|
|
2957
|
-
r"(?:, (?:c-\d{3,}|d-\d{4,}))*; approval: "
|
|
2958
|
-
r"(?:accepted|blocked c-\d{3,})",
|
|
2959
|
-
status,
|
|
2960
|
-
):
|
|
2961
|
-
failures.append(
|
|
2962
|
-
"implementation-planning Requirement Coverage row "
|
|
2963
|
-
f"`{row_id}` has invalid Status `{status}`; expected "
|
|
2964
|
-
"`covered`, `gap`, `blocked C-NNN`, or a rendered "
|
|
2965
|
-
"`documented-deviation` disposition."
|
|
2966
|
-
)
|
|
2967
|
-
|
|
2968
|
-
if gate_value in ("passed", "passed-with-dissent"):
|
|
2969
|
-
uncovered = [
|
|
2970
|
-
f"{row_id} ({status})"
|
|
2971
|
-
for row_id, status in rows
|
|
2972
|
-
if status != "covered"
|
|
2973
|
-
and not status.endswith("; approval: accepted")
|
|
2974
|
-
]
|
|
2975
|
-
if uncovered:
|
|
2976
|
-
failures.append(
|
|
2977
|
-
"implementation-planning Gate result is publishable but "
|
|
2978
|
-
"Requirement Coverage has uncovered row(s): "
|
|
2979
|
-
+ ", ".join(uncovered)
|
|
2980
|
-
)
|
|
2981
|
-
def _extract_verdict_card_token(content: str) -> str | None:
|
|
2982
|
-
"""Return the `Verdict Token` cell from the Verdict Card block."""
|
|
2983
|
-
block = _VERDICT_CARD_BLOCK_RE.search(content)
|
|
2984
|
-
if block is None:
|
|
2985
|
-
return None
|
|
2986
|
-
match = _FINAL_VERDICT_TOKEN_RE.search(block.group("body"))
|
|
2987
|
-
if match is None:
|
|
2988
|
-
return None
|
|
2989
|
-
return match.group("value")
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
def _validate_verdict_card_consistency(content: str, failures: list[str]) -> None:
|
|
2993
|
-
"""Verdict Card is a non-authoritative index of §7. If both blocks
|
|
2994
|
-
carry a Verdict Token row, the values MUST byte-match (modulo case
|
|
2995
|
-
and surrounding whitespace) — divergence is a contract violation per
|
|
2996
|
-
`report-writer` SKILL.md "Authoring Contract".
|
|
2997
|
-
"""
|
|
2998
|
-
card_value = _extract_verdict_card_token(content)
|
|
2999
|
-
final_value = _extract_final_verdict_token(content)
|
|
3000
|
-
if card_value is None or final_value is None:
|
|
3001
|
-
# Missing-Card and missing-§7 are surfaced by other checks; this
|
|
3002
|
-
# function only enforces the consistency contract between the two.
|
|
3003
|
-
return
|
|
3004
|
-
if card_value.strip().lower() != final_value.strip().lower():
|
|
3005
|
-
failures.append(
|
|
3006
|
-
"Verdict Card `Verdict Token` value "
|
|
3007
|
-
f"`{card_value}` does not match `## 7. Final Verdict` value "
|
|
3008
|
-
f"`{final_value}` — the Card is a non-authoritative index and "
|
|
3009
|
-
"MUST byte-match §7. Either fix the Card or update §7; do not "
|
|
3010
|
-
"ship divergent values."
|
|
3011
|
-
)
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
3063
|
def _validate_verdict_card_fields(data: dict, failures: list[str]) -> None:
|
|
3015
3064
|
"""`verdictCard.direction` must byte-match its authoritative home in §7.
|
|
3016
3065
|
|
|
@@ -3337,9 +3386,8 @@ def validate_final_report_data(
|
|
|
3337
3386
|
The data.json is the source-of-truth that the renderer reads to
|
|
3338
3387
|
produce the markdown. If schema validation passes here, the rendered
|
|
3339
3388
|
markdown is guaranteed to contain every section / row the contract
|
|
3340
|
-
requires (the template loops over the data)
|
|
3341
|
-
|
|
3342
|
-
net but are expected to be redundant.
|
|
3389
|
+
requires (the template loops over the data), so the schema is the
|
|
3390
|
+
only place the deliverable contract is enforced.
|
|
3343
3391
|
|
|
3344
3392
|
Missing data.json is reported as a single failure rather than a
|
|
3345
3393
|
cascade of substring failures — that points the writer at the right
|
|
@@ -3381,6 +3429,13 @@ def validate_final_report_data(
|
|
|
3381
3429
|
return data
|
|
3382
3430
|
|
|
3383
3431
|
manifest = run_manifest or {}
|
|
3432
|
+
if data.get("executionIdentityVersion") == 2 or data.get("executionRoles"):
|
|
3433
|
+
try:
|
|
3434
|
+
from okstra_ctl.report_contract import assert_execution_roles_match_manifest
|
|
3435
|
+
|
|
3436
|
+
assert_execution_roles_match_manifest(data, manifest)
|
|
3437
|
+
except ValueError as exc:
|
|
3438
|
+
failures.append(str(exc))
|
|
3384
3439
|
_validate_approval_context(data, manifest, failures, report_path)
|
|
3385
3440
|
_validate_activity_contract_plan_limits(data, manifest, failures)
|
|
3386
3441
|
|
|
@@ -3674,7 +3729,17 @@ _ADVISORY_ONLY_KINDS = {"d"}
|
|
|
3674
3729
|
# Stop reasons that justify promoting a still-broken planner-fixable item to the
|
|
3675
3730
|
# user: the self-fix budget ran out, or a round produced no net resolution so
|
|
3676
3731
|
# further rounds would repeat themselves.
|
|
3677
|
-
|
|
3732
|
+
#
|
|
3733
|
+
# `cause-group-recurrence` is the legacy spelling of that same exhaustion
|
|
3734
|
+
# (plan-body-verification.md "Loop termination"). A pre-activity-contract report
|
|
3735
|
+
# carrying it is a report whose loop stopped because the cause kept recurring —
|
|
3736
|
+
# refusing it here left such a run with no exit at all: the loop may not run
|
|
3737
|
+
# again, and the surviving item may not be promoted either.
|
|
3738
|
+
# `_validate_activity_contract_plan_limits` still forbids a NEW v1 run from
|
|
3739
|
+
# emitting it, so this admits the legacy shape without reopening it.
|
|
3740
|
+
_SELF_FIX_EXHAUSTED_REASONS = frozenset(
|
|
3741
|
+
{"max-rounds-reached", "no-progress", "cause-group-recurrence"}
|
|
3742
|
+
)
|
|
3678
3743
|
|
|
3679
3744
|
|
|
3680
3745
|
def _is_variation_point_item(item: dict) -> bool:
|
|
@@ -5059,10 +5124,11 @@ def _items_resolved_in_round(plan_items: object, round_number: int) -> set[str]:
|
|
|
5059
5124
|
def _detect_self_fix_recurrence(pbv: dict) -> list[str]:
|
|
5060
5125
|
"""Rounds that re-target ground the previous round already worked, unresolved.
|
|
5061
5126
|
|
|
5062
|
-
`no-progress`
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5127
|
+
`no-progress` is judged at the round's end from what it resolved. Repeating
|
|
5128
|
+
the previous round's *unresolved* remainder is the same conclusion reached
|
|
5129
|
+
one dispatch earlier — the observed shape was two rounds spent on one
|
|
5130
|
+
identical seven-item set. This names that shape so the loop can exit on it
|
|
5131
|
+
rather than paying for the round that proves it.
|
|
5066
5132
|
|
|
5067
5133
|
Advisory only. Narrowing onto what the last round genuinely left open is
|
|
5068
5134
|
legitimate progress, and the rule separating that from re-digging the same
|
|
@@ -5091,8 +5157,8 @@ def _detect_self_fix_recurrence(pbv: dict) -> list[str]:
|
|
|
5091
5157
|
f"self-fix round {round_number} re-targets only items round "
|
|
5092
5158
|
f"{round_number - 1} left unresolved ({', '.join(sorted(current))}) "
|
|
5093
5159
|
"— the previous round's correction did not move this cause. "
|
|
5094
|
-
"Consider exiting with `
|
|
5095
|
-
"
|
|
5160
|
+
"Consider exiting with `no-progress` instead of spending the "
|
|
5161
|
+
"remaining budget on the same ground."
|
|
5096
5162
|
)
|
|
5097
5163
|
return warnings
|
|
5098
5164
|
|
|
@@ -7394,7 +7460,7 @@ def _validate_unverified_critic_gaps_recorded(data: dict, failures: list[str]) -
|
|
|
7394
7460
|
|
|
7395
7461
|
|
|
7396
7462
|
# Allowed `fixability` values, mirroring the schema enum
|
|
7397
|
-
# (schemas/final-report-
|
|
7463
|
+
# (schemas/final-report-v2.0.schema.json planItems[].verdicts[].fixability).
|
|
7398
7464
|
_FIXABILITY_VALUES = frozenset({"planner-fixable", "needs-user-input"})
|
|
7399
7465
|
|
|
7400
7466
|
|
|
@@ -8015,14 +8081,13 @@ def _load_stage_validator():
|
|
|
8015
8081
|
def _append_stage_data_failures(data: Mapping[str, Any], failures: list[str]) -> None:
|
|
8016
8082
|
"""Run the stage relationship checks that schema v2 cannot express.
|
|
8017
8083
|
|
|
8018
|
-
|
|
8019
|
-
the
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
owns both modes so the rule vocabulary stays defined once.
|
|
8084
|
+
The depends-on DAG, parallel-stage file safety, RED→GREEN ordering, and
|
|
8085
|
+
the TDD-exemption vocabulary are relationships between stages, which a
|
|
8086
|
+
JSON Schema cannot state. They are enforced here, against the data.json,
|
|
8087
|
+
by the same validator that owns the rule vocabulary.
|
|
8023
8088
|
"""
|
|
8024
|
-
if (data or {}).get("schemaVersion") !=
|
|
8025
|
-
return #
|
|
8089
|
+
if (data or {}).get("schemaVersion") != CURRENT_REPORT_SCHEMA_VERSION:
|
|
8090
|
+
return # Schema validation already rejected an unknown version.
|
|
8026
8091
|
planning = (data or {}).get("implementationPlanning")
|
|
8027
8092
|
if not isinstance(planning, Mapping):
|
|
8028
8093
|
return # Schema validation already reported the missing block.
|
|
@@ -8037,157 +8102,6 @@ def _append_stage_data_failures(data: Mapping[str, Any], failures: list[str]) ->
|
|
|
8037
8102
|
)
|
|
8038
8103
|
|
|
8039
8104
|
|
|
8040
|
-
def _append_stage_structure_failures(content: str, failures: list[str]) -> None:
|
|
8041
|
-
"""Enforce the Stage Map structural contract at the implementation-planning
|
|
8042
|
-
boundary. Without this, a plan missing `## 5.5 Stage Map` passes the
|
|
8043
|
-
planning gate, gets approved, and only fails later at the `implementation`
|
|
8044
|
-
entry (validators/validate-implementation-plan-stages.py via
|
|
8045
|
-
prepare_task_bundle). Running the same validator here moves the failure to
|
|
8046
|
-
produce-time."""
|
|
8047
|
-
mod = _load_stage_validator()
|
|
8048
|
-
if mod is None: # pragma: no cover — repo/runtime always ship the file
|
|
8049
|
-
failures.append(f"cannot load Stage Map validator at {_STAGE_VALIDATOR_PATH}")
|
|
8050
|
-
return
|
|
8051
|
-
for e in mod.collect_validation_errors(content):
|
|
8052
|
-
failures.append(
|
|
8053
|
-
f"implementation-planning Stage Map structure invalid "
|
|
8054
|
-
f"[{e.code} stage={e.stage}]: {e.message}"
|
|
8055
|
-
)
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
def validate_phase_boundary(
|
|
8059
|
-
task_type: str,
|
|
8060
|
-
report_path: Path,
|
|
8061
|
-
failures: list[str],
|
|
8062
|
-
*,
|
|
8063
|
-
report_data: Mapping[str, Any] | None = None,
|
|
8064
|
-
) -> None:
|
|
8065
|
-
"""Phase-specific contract checks.
|
|
8066
|
-
|
|
8067
|
-
For `implementation-planning` runs, the final report must contain the
|
|
8068
|
-
required deliverable sections; absence indicates a planning run that
|
|
8069
|
-
skipped its core outputs (or an implementation run that ran under the
|
|
8070
|
-
wrong task type).
|
|
8071
|
-
|
|
8072
|
-
Additionally enforces the Plan Body Verification gate (§5.5.9):
|
|
8073
|
-
- gate ∈ {passed, passed-with-dissent} → top-level Approval checkbox
|
|
8074
|
-
MUST be present.
|
|
8075
|
-
- gate ∈ {blocked-by-disagreement, aborted-non-result} → checkbox
|
|
8076
|
-
MUST be absent (lead converted findings into Clarification rows
|
|
8077
|
-
instead of opening the gate).
|
|
8078
|
-
"""
|
|
8079
|
-
if (report_data or {}).get("schemaVersion") == "2.0":
|
|
8080
|
-
return
|
|
8081
|
-
if not report_path.exists():
|
|
8082
|
-
return
|
|
8083
|
-
content = report_path.read_text()
|
|
8084
|
-
|
|
8085
|
-
# Verdict Card vs §7. Final Verdict Verdict Token consistency. The Card
|
|
8086
|
-
# is a non-authoritative index; divergence is a contract violation.
|
|
8087
|
-
_validate_verdict_card_consistency(content, failures)
|
|
8088
|
-
|
|
8089
|
-
if task_type == "implementation":
|
|
8090
|
-
for needle in IMPLEMENTATION_REQUIRED_SECTIONS:
|
|
8091
|
-
if needle not in content:
|
|
8092
|
-
failures.append(
|
|
8093
|
-
"implementation report is missing required §5.7 "
|
|
8094
|
-
f"deliverable section: `{needle}`"
|
|
8095
|
-
)
|
|
8096
|
-
|
|
8097
|
-
if task_type == "final-verification":
|
|
8098
|
-
for needle in FINAL_VERIFICATION_REQUIRED_SECTIONS:
|
|
8099
|
-
if needle not in content:
|
|
8100
|
-
failures.append(
|
|
8101
|
-
"final-verification report is missing required §5.8 "
|
|
8102
|
-
f"deliverable section: `{needle}`"
|
|
8103
|
-
)
|
|
8104
|
-
token_value = _extract_final_verdict_token(content)
|
|
8105
|
-
if token_value is None:
|
|
8106
|
-
failures.append(
|
|
8107
|
-
"final-verification report `## 7. Final Verdict` table is "
|
|
8108
|
-
"missing the `Verdict Token` row — required by the release-"
|
|
8109
|
-
"handoff entry gate."
|
|
8110
|
-
)
|
|
8111
|
-
elif token_value.strip().lower() not in FINAL_VERIFICATION_VERDICT_TOKENS:
|
|
8112
|
-
failures.append(
|
|
8113
|
-
"final-verification report `Verdict Token` value "
|
|
8114
|
-
f"`{token_value}` is not one of "
|
|
8115
|
-
f"{', '.join(FINAL_VERIFICATION_VERDICT_TOKENS)}."
|
|
8116
|
-
)
|
|
8117
|
-
|
|
8118
|
-
if task_type == "release-handoff":
|
|
8119
|
-
if _MERGE_CONFLICT_PROBE_HEADING_RE.search(content) is None:
|
|
8120
|
-
failures.append(
|
|
8121
|
-
"release-handoff report is missing `### 5.6.6 Merge Conflict "
|
|
8122
|
-
"Probe` sub-section — required by the release-handoff profile "
|
|
8123
|
-
"(self-review 6, merge-conflict probe audit). When the run is "
|
|
8124
|
-
"`local checkout` / `skip`, record the single line `- Not run "
|
|
8125
|
-
"(user picked local checkout or skip).` under the heading."
|
|
8126
|
-
)
|
|
8127
|
-
|
|
8128
|
-
if task_type != "implementation-planning":
|
|
8129
|
-
return
|
|
8130
|
-
for needle in PLANNING_REQUIRED_SECTIONS:
|
|
8131
|
-
if needle not in content:
|
|
8132
|
-
failures.append(
|
|
8133
|
-
"implementation-planning report is missing required section: "
|
|
8134
|
-
f"`{needle}`"
|
|
8135
|
-
)
|
|
8136
|
-
|
|
8137
|
-
# Scope the gate-result search to the §5.5.9 Plan Body Verification block
|
|
8138
|
-
# so an earlier prose mention of "Gate result" cannot hijack the value.
|
|
8139
|
-
pbv_block = _PLAN_BODY_VERIFICATION_BLOCK_RE.search(content)
|
|
8140
|
-
gate_match = (
|
|
8141
|
-
_GATE_RESULT_RE.search(pbv_block.group("body")) if pbv_block else None
|
|
8142
|
-
)
|
|
8143
|
-
if gate_match is None:
|
|
8144
|
-
# The `Plan Body Verification` heading check above already covers
|
|
8145
|
-
# the wholly-missing case; a heading present without a `Gate result`
|
|
8146
|
-
# line is its own contract violation.
|
|
8147
|
-
if "Plan Body Verification" in content:
|
|
8148
|
-
failures.append(
|
|
8149
|
-
"implementation-planning report has `Plan Body Verification` "
|
|
8150
|
-
"section but no `Gate result:` line — required by §5.5.9."
|
|
8151
|
-
)
|
|
8152
|
-
return
|
|
8153
|
-
gate_value = gate_match.group("value").strip().lower()
|
|
8154
|
-
if gate_value not in PLAN_VERIFY_GATE_VALUES:
|
|
8155
|
-
failures.append(
|
|
8156
|
-
"implementation-planning report `Gate result` value "
|
|
8157
|
-
f"`{gate_value}` is not one of "
|
|
8158
|
-
f"{', '.join(PLAN_VERIFY_GATE_VALUES)}."
|
|
8159
|
-
)
|
|
8160
|
-
return
|
|
8161
|
-
fm_block = _FRONTMATTER_BLOCK_RE.match(content)
|
|
8162
|
-
fm_match = (
|
|
8163
|
-
_APPROVED_FRONTMATTER_RE.search(fm_block.group(1)) if fm_block else None
|
|
8164
|
-
)
|
|
8165
|
-
if gate_value in ("passed", "passed-with-dissent") and fm_match is None:
|
|
8166
|
-
failures.append(
|
|
8167
|
-
"implementation-planning report Gate result is "
|
|
8168
|
-
f"`{gate_value}` but the frontmatter `approved:` field is missing — "
|
|
8169
|
-
"render `approved: false` so the user (or `--approve`) can flip it."
|
|
8170
|
-
)
|
|
8171
|
-
if (
|
|
8172
|
-
gate_value in ("blocked-by-disagreement", "aborted-non-result")
|
|
8173
|
-
and fm_match is not None
|
|
8174
|
-
and fm_match.group(1).lower() == "true"
|
|
8175
|
-
):
|
|
8176
|
-
failures.append(
|
|
8177
|
-
"implementation-planning report Gate result is "
|
|
8178
|
-
f"`{gate_value}` but the frontmatter has `approved: true` — gate "
|
|
8179
|
-
"must NOT publish a pre-approved plan when verification did not pass."
|
|
8180
|
-
)
|
|
8181
|
-
|
|
8182
|
-
_append_requirement_coverage_failures(content, gate_value, failures)
|
|
8183
|
-
|
|
8184
|
-
# Only a publishable plan (gate passed) can be flipped to `approved: true`
|
|
8185
|
-
# and reach the `implementation` entry, so the Stage Map structure is
|
|
8186
|
-
# enforced only here — a blocked/aborted plan may legitimately be incomplete.
|
|
8187
|
-
if gate_value in ("passed", "passed-with-dissent"):
|
|
8188
|
-
_append_stage_structure_failures(content, failures)
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
8105
|
def _brief_path_from_manifest(task_manifest: dict, project_root: Path) -> Path:
|
|
8192
8106
|
"""This run's brief, or a non-existent path when the manifest names none.
|
|
8193
8107
|
|
|
@@ -8351,11 +8265,16 @@ def _validate_session_conformance(
|
|
|
8351
8265
|
task_type: str,
|
|
8352
8266
|
claude_projects_dir: str | None,
|
|
8353
8267
|
failures: list[str],
|
|
8268
|
+
advisories: list[str] | None = None,
|
|
8354
8269
|
) -> None:
|
|
8355
8270
|
"""prompts/lead/okstra-lead-contract.md의 PROGRESS / activity / heartbeat /
|
|
8356
8271
|
implementation entry guard 사후 검사를 위임하고 실패를
|
|
8357
8272
|
``session-conformance: `` 접두로 folding 한다. 설계:
|
|
8358
8273
|
docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md
|
|
8274
|
+
|
|
8275
|
+
PROGRESS 서술 라인 누락은 ``result.advisories`` 로 돌아오며 run 을
|
|
8276
|
+
실패시키지 않는다 — 그 라인을 쓸 세션은 이 검사가 도는 시점에 이미 끝나 있어
|
|
8277
|
+
남은 구제책이 phase 폐기뿐이다.
|
|
8359
8278
|
"""
|
|
8360
8279
|
_validators_dir = Path(__file__).resolve().parent
|
|
8361
8280
|
if str(_validators_dir) not in sys.path:
|
|
@@ -8377,6 +8296,9 @@ def _validate_session_conformance(
|
|
|
8377
8296
|
claude_projects_dir=Path(claude_projects_dir) if claude_projects_dir else None,
|
|
8378
8297
|
)
|
|
8379
8298
|
failures.extend(f"session-conformance: {err}" for err in result.errors)
|
|
8299
|
+
(advisories if advisories is not None else failures).extend(
|
|
8300
|
+
f"session-conformance: {note}" for note in result.advisories
|
|
8301
|
+
)
|
|
8380
8302
|
|
|
8381
8303
|
|
|
8382
8304
|
def _validate_forbidden_actions(
|
|
@@ -9130,8 +9052,21 @@ def main() -> int:
|
|
|
9130
9052
|
print(f"validate-run: Phase 7 auto-recovery skipped — {msg}", file=sys.stderr)
|
|
9131
9053
|
|
|
9132
9054
|
failures: list[str] = []
|
|
9055
|
+
# Findings that are reported but do not fail the run.
|
|
9056
|
+
#
|
|
9057
|
+
# The line is whether a remedy still exists at Phase 7. A defect in the
|
|
9058
|
+
# report is fixable — re-author it and re-render. A defect in what a
|
|
9059
|
+
# *finished session* wrote down is not: the worker that would restate its
|
|
9060
|
+
# citation and the lead pane that would emit its PROGRESS line have both
|
|
9061
|
+
# ended, so the only remaining move is to void the phase. That blocks the
|
|
9062
|
+
# next phase without establishing that any conclusion in the report is
|
|
9063
|
+
# wrong. Those findings are surfaced and carried in the run manifest
|
|
9064
|
+
# instead — currently the worker citation ledger and the PROGRESS narration
|
|
9065
|
+
# lines.
|
|
9066
|
+
advisories: list[str] = []
|
|
9133
9067
|
if autofix_state == "accuracy-failed":
|
|
9134
9068
|
failures.extend(autofix_messages)
|
|
9069
|
+
_validate_execution_identity_v2(run_manifest, failures)
|
|
9135
9070
|
contract = extract_contract(run_manifest, task_manifest, failures)
|
|
9136
9071
|
concurrent_run_authorized = bool(
|
|
9137
9072
|
(run_manifest.get("concurrentRun") or {}).get("detected")
|
|
@@ -9190,19 +9125,13 @@ def main() -> int:
|
|
|
9190
9125
|
)
|
|
9191
9126
|
validate_team_state_usage(team_state, failures)
|
|
9192
9127
|
|
|
9193
|
-
validate_phase_boundary(
|
|
9194
|
-
task_type,
|
|
9195
|
-
report_path,
|
|
9196
|
-
failures,
|
|
9197
|
-
report_data=validation_data,
|
|
9198
|
-
)
|
|
9199
9128
|
_validate_phase_boundary_error_log(
|
|
9200
9129
|
report_path.parent.parent,
|
|
9201
9130
|
task_type,
|
|
9202
9131
|
failures,
|
|
9203
9132
|
)
|
|
9204
9133
|
if task_type:
|
|
9205
|
-
validate_worker_results_audit(report_path, task_type, failures)
|
|
9134
|
+
validate_worker_results_audit(report_path, task_type, failures, advisories)
|
|
9206
9135
|
_validate_session_conformance(
|
|
9207
9136
|
team_state,
|
|
9208
9137
|
team_state_path,
|
|
@@ -9212,6 +9141,7 @@ def main() -> int:
|
|
|
9212
9141
|
task_type,
|
|
9213
9142
|
args.claude_projects_dir,
|
|
9214
9143
|
failures,
|
|
9144
|
+
advisories,
|
|
9215
9145
|
)
|
|
9216
9146
|
_validate_forbidden_actions(
|
|
9217
9147
|
team_state,
|
|
@@ -9309,6 +9239,7 @@ def main() -> int:
|
|
|
9309
9239
|
validation_status,
|
|
9310
9240
|
failures,
|
|
9311
9241
|
report_data=validation_data,
|
|
9242
|
+
advisories=advisories,
|
|
9312
9243
|
)
|
|
9313
9244
|
|
|
9314
9245
|
write_json(team_state_path, team_state)
|
|
@@ -9335,10 +9266,14 @@ def main() -> int:
|
|
|
9335
9266
|
+ "\n"
|
|
9336
9267
|
)
|
|
9337
9268
|
|
|
9269
|
+
for note in advisories:
|
|
9270
|
+
print(f"validate-run: advisory — {note}", file=sys.stderr)
|
|
9271
|
+
|
|
9338
9272
|
result = {
|
|
9339
9273
|
"validationStatus": validation_status,
|
|
9340
9274
|
"finalRunStatus": run_manifest.get("status"),
|
|
9341
9275
|
"failures": failures,
|
|
9276
|
+
"advisories": advisories,
|
|
9342
9277
|
"teamStatePath": str(team_state_path),
|
|
9343
9278
|
"reportPath": str(report_path),
|
|
9344
9279
|
}
|