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
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"""Batch-scoped source and Git mutation audit for worker invocations."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .write_policy import WritePolicy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MutationAuditError(ValueError):
|
|
17
|
+
"""Raised before dispatch when policies cannot share one audit batch."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def assert_compatible_batch(policies: Sequence[WritePolicy]) -> None:
|
|
21
|
+
"""Reject a dispatch batch before any attempt is recorded."""
|
|
22
|
+
_validate_batch(tuple(policies))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class MutationSnapshot:
|
|
27
|
+
root: Path
|
|
28
|
+
artifact_root: Path
|
|
29
|
+
policy_digests: tuple[str, ...]
|
|
30
|
+
file_digests: Mapping[str, str]
|
|
31
|
+
artifact_digests: Mapping[str, str]
|
|
32
|
+
scratch_digests: Mapping[str, str]
|
|
33
|
+
git_projection: Mapping[str, Any]
|
|
34
|
+
digest: str
|
|
35
|
+
orchestrator_paths: tuple[str, ...] = ()
|
|
36
|
+
|
|
37
|
+
def to_payload(self) -> dict[str, Any]:
|
|
38
|
+
return {
|
|
39
|
+
"root": str(self.root),
|
|
40
|
+
"artifactRoot": str(self.artifact_root),
|
|
41
|
+
"policyDigests": list(self.policy_digests),
|
|
42
|
+
"fileDigests": dict(self.file_digests),
|
|
43
|
+
"artifactDigests": dict(self.artifact_digests),
|
|
44
|
+
"scratchDigests": dict(self.scratch_digests),
|
|
45
|
+
"gitProjection": dict(self.git_projection),
|
|
46
|
+
"digest": self.digest,
|
|
47
|
+
"orchestratorPaths": list(self.orchestrator_paths),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_payload(cls, payload: Mapping[str, Any]) -> "MutationSnapshot":
|
|
52
|
+
snapshot = cls(
|
|
53
|
+
root=Path(str(payload["root"])),
|
|
54
|
+
artifact_root=Path(str(payload["artifactRoot"])),
|
|
55
|
+
policy_digests=tuple(payload["policyDigests"]),
|
|
56
|
+
file_digests=dict(payload["fileDigests"]),
|
|
57
|
+
artifact_digests=dict(payload["artifactDigests"]),
|
|
58
|
+
scratch_digests=dict(payload["scratchDigests"]),
|
|
59
|
+
git_projection=dict(payload["gitProjection"]),
|
|
60
|
+
digest=str(payload["digest"]),
|
|
61
|
+
orchestrator_paths=tuple(payload.get("orchestratorPaths", ())),
|
|
62
|
+
)
|
|
63
|
+
expected = _snapshot_digest(
|
|
64
|
+
snapshot.root,
|
|
65
|
+
snapshot.artifact_root,
|
|
66
|
+
snapshot.policy_digests,
|
|
67
|
+
snapshot.file_digests,
|
|
68
|
+
snapshot.artifact_digests,
|
|
69
|
+
snapshot.scratch_digests,
|
|
70
|
+
snapshot.git_projection,
|
|
71
|
+
snapshot.orchestrator_paths,
|
|
72
|
+
)
|
|
73
|
+
if snapshot.digest != expected:
|
|
74
|
+
raise MutationAuditError("mutation snapshot digest does not match payload")
|
|
75
|
+
return snapshot
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class MutationAuditResult:
|
|
80
|
+
status: str
|
|
81
|
+
changed_paths: tuple[str, ...]
|
|
82
|
+
source_changed: bool
|
|
83
|
+
git_changed: bool
|
|
84
|
+
attribution: str
|
|
85
|
+
retry_allowed: bool
|
|
86
|
+
violations: tuple[str, ...]
|
|
87
|
+
before_digest: str
|
|
88
|
+
after_digest: str
|
|
89
|
+
git_projection: Mapping[str, Any]
|
|
90
|
+
|
|
91
|
+
def change_summary(self) -> dict[str, Any]:
|
|
92
|
+
return {
|
|
93
|
+
"mutationStatus": self.status,
|
|
94
|
+
"changedPaths": list(self.changed_paths),
|
|
95
|
+
"sourceChanged": self.source_changed,
|
|
96
|
+
"gitChanged": self.git_changed,
|
|
97
|
+
"attribution": self.attribution,
|
|
98
|
+
"retryAllowed": self.retry_allowed,
|
|
99
|
+
"violations": list(self.violations),
|
|
100
|
+
"beforeDigest": self.before_digest,
|
|
101
|
+
"afterDigest": self.after_digest,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ExecutionMutationAudit:
|
|
106
|
+
"""Capture and compare one invocation batch without global state."""
|
|
107
|
+
|
|
108
|
+
def snapshot(
|
|
109
|
+
self,
|
|
110
|
+
policies: Sequence[WritePolicy],
|
|
111
|
+
*,
|
|
112
|
+
orchestrator_paths: Sequence[Path] = (),
|
|
113
|
+
) -> MutationSnapshot:
|
|
114
|
+
rows = tuple(policies)
|
|
115
|
+
root, artifact_root = _validate_batch(rows)
|
|
116
|
+
generated = _generated_paths(rows)
|
|
117
|
+
file_digests = _content_snapshot(root, generated)
|
|
118
|
+
artifact_digests = (
|
|
119
|
+
file_digests
|
|
120
|
+
if artifact_root == root
|
|
121
|
+
else _content_snapshot(artifact_root, generated)
|
|
122
|
+
)
|
|
123
|
+
scratch_digests = _scratch_snapshot(rows)
|
|
124
|
+
git_projection = _git_projection(root)
|
|
125
|
+
orchestrator = tuple(
|
|
126
|
+
sorted(str(path.resolve()) for path in orchestrator_paths)
|
|
127
|
+
)
|
|
128
|
+
policy_digests = tuple(policy.digest for policy in rows)
|
|
129
|
+
digest = _snapshot_digest(
|
|
130
|
+
root,
|
|
131
|
+
artifact_root,
|
|
132
|
+
policy_digests,
|
|
133
|
+
file_digests,
|
|
134
|
+
artifact_digests,
|
|
135
|
+
scratch_digests,
|
|
136
|
+
git_projection,
|
|
137
|
+
orchestrator,
|
|
138
|
+
)
|
|
139
|
+
return MutationSnapshot(
|
|
140
|
+
root=root,
|
|
141
|
+
artifact_root=artifact_root,
|
|
142
|
+
policy_digests=policy_digests,
|
|
143
|
+
file_digests=file_digests,
|
|
144
|
+
artifact_digests=artifact_digests,
|
|
145
|
+
scratch_digests=scratch_digests,
|
|
146
|
+
git_projection=git_projection,
|
|
147
|
+
digest=digest,
|
|
148
|
+
orchestrator_paths=orchestrator,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def compare(
|
|
152
|
+
self,
|
|
153
|
+
before: MutationSnapshot,
|
|
154
|
+
policies: Sequence[WritePolicy],
|
|
155
|
+
*,
|
|
156
|
+
out_of_plan_edits: Sequence[str] = (),
|
|
157
|
+
attempt_succeeded: bool = True,
|
|
158
|
+
result_present: bool = True,
|
|
159
|
+
) -> MutationAuditResult:
|
|
160
|
+
rows = tuple(policies)
|
|
161
|
+
after = self.snapshot(
|
|
162
|
+
rows,
|
|
163
|
+
orchestrator_paths=tuple(Path(path) for path in before.orchestrator_paths),
|
|
164
|
+
)
|
|
165
|
+
_validate_snapshot_authority(before, after, rows)
|
|
166
|
+
changed = _changed_keys(before.file_digests, after.file_digests)
|
|
167
|
+
artifact_changed = _changed_keys(
|
|
168
|
+
before.artifact_digests, after.artifact_digests
|
|
169
|
+
)
|
|
170
|
+
scratch_changed = _changed_keys(
|
|
171
|
+
before.scratch_digests, after.scratch_digests
|
|
172
|
+
)
|
|
173
|
+
source_changes = _source_changes(changed, rows, before)
|
|
174
|
+
git_changed = before.git_projection != after.git_projection
|
|
175
|
+
violations = _policy_violations(
|
|
176
|
+
before,
|
|
177
|
+
after,
|
|
178
|
+
rows,
|
|
179
|
+
source_changes,
|
|
180
|
+
out_of_plan_edits,
|
|
181
|
+
)
|
|
182
|
+
violations.extend(
|
|
183
|
+
_artifact_policy_failures(before, rows, artifact_changed)
|
|
184
|
+
)
|
|
185
|
+
status = _terminal_status(
|
|
186
|
+
rows,
|
|
187
|
+
source_changed=bool(source_changes),
|
|
188
|
+
git_changed=git_changed,
|
|
189
|
+
attempt_succeeded=attempt_succeeded,
|
|
190
|
+
result_present=result_present,
|
|
191
|
+
violations=violations,
|
|
192
|
+
)
|
|
193
|
+
retry_allowed = _retry_allowed(
|
|
194
|
+
status, bool(source_changes), git_changed, bool(scratch_changed)
|
|
195
|
+
)
|
|
196
|
+
return MutationAuditResult(
|
|
197
|
+
status=status,
|
|
198
|
+
changed_paths=tuple(sorted(source_changes)),
|
|
199
|
+
source_changed=bool(source_changes),
|
|
200
|
+
git_changed=git_changed,
|
|
201
|
+
attribution="batch-unattributed",
|
|
202
|
+
retry_allowed=retry_allowed,
|
|
203
|
+
violations=tuple(violations),
|
|
204
|
+
before_digest=before.digest,
|
|
205
|
+
after_digest=after.digest,
|
|
206
|
+
git_projection=after.git_projection,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _validate_batch(policies: tuple[WritePolicy, ...]) -> tuple[Path, Path]:
|
|
211
|
+
if not policies:
|
|
212
|
+
raise MutationAuditError("mutation audit requires at least one policy")
|
|
213
|
+
roots = {Path(str(row.source_policy["allowedRoot"])).resolve() for row in policies}
|
|
214
|
+
artifact_roots = {
|
|
215
|
+
Path(str(row.artifact_policy["allowedRoot"])).resolve()
|
|
216
|
+
for row in policies
|
|
217
|
+
}
|
|
218
|
+
if len(roots) != 1:
|
|
219
|
+
raise MutationAuditError("one mutation batch must use one assigned worktree")
|
|
220
|
+
if len(artifact_roots) != 1:
|
|
221
|
+
raise MutationAuditError("one mutation batch must use one artifact root")
|
|
222
|
+
mutation = any(row.source_mode == "project-mutation" for row in policies)
|
|
223
|
+
non_exact_readonly = any(
|
|
224
|
+
row.source_mode == "source-readonly"
|
|
225
|
+
and _maximum_precision(row) != "exact-path"
|
|
226
|
+
for row in policies
|
|
227
|
+
)
|
|
228
|
+
if mutation and non_exact_readonly:
|
|
229
|
+
raise MutationAuditError(
|
|
230
|
+
"project mutation and non-exact readonly calls must run in separate batches"
|
|
231
|
+
)
|
|
232
|
+
return next(iter(roots)), next(iter(artifact_roots))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _validate_snapshot_authority(
|
|
236
|
+
before: MutationSnapshot,
|
|
237
|
+
after: MutationSnapshot,
|
|
238
|
+
policies: tuple[WritePolicy, ...],
|
|
239
|
+
) -> None:
|
|
240
|
+
expected = tuple(policy.digest for policy in policies)
|
|
241
|
+
if (
|
|
242
|
+
before.root != after.root
|
|
243
|
+
or before.artifact_root != after.artifact_root
|
|
244
|
+
or before.policy_digests != expected
|
|
245
|
+
):
|
|
246
|
+
raise MutationAuditError("mutation snapshot does not match batch policies")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _maximum_precision(policy: WritePolicy) -> str:
|
|
250
|
+
return policy.maximum_boundary_precision
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _content_snapshot(root: Path, generated: frozenset[str]) -> dict[str, str]:
|
|
254
|
+
rows: dict[str, str] = {}
|
|
255
|
+
for current, directories, filenames in os.walk(root, followlinks=False):
|
|
256
|
+
current_path = Path(current)
|
|
257
|
+
traversable: list[str] = []
|
|
258
|
+
for name in sorted(directories):
|
|
259
|
+
path = current_path / name
|
|
260
|
+
relative_path = path.relative_to(root)
|
|
261
|
+
if name == ".git" or _excluded(relative_path, generated):
|
|
262
|
+
continue
|
|
263
|
+
if path.is_symlink():
|
|
264
|
+
rows[relative_path.as_posix()] = _path_digest(path)
|
|
265
|
+
continue
|
|
266
|
+
traversable.append(name)
|
|
267
|
+
directories[:] = traversable
|
|
268
|
+
for name in sorted(filenames):
|
|
269
|
+
path = current_path / name
|
|
270
|
+
relative = path.relative_to(root).as_posix()
|
|
271
|
+
if relative == ".git" or _excluded(Path(relative), generated):
|
|
272
|
+
continue
|
|
273
|
+
rows[relative] = _path_digest(path)
|
|
274
|
+
return rows
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _scratch_snapshot(policies: Sequence[WritePolicy]) -> dict[str, str]:
|
|
278
|
+
roots = {
|
|
279
|
+
Path(value)
|
|
280
|
+
for policy in policies
|
|
281
|
+
for value in policy.auxiliary_policy.get("scratchRoots", ())
|
|
282
|
+
}
|
|
283
|
+
rows: dict[str, str] = {}
|
|
284
|
+
for root in sorted(roots):
|
|
285
|
+
if not root.exists():
|
|
286
|
+
continue
|
|
287
|
+
if root.is_file() or root.is_symlink():
|
|
288
|
+
rows[str(root)] = _path_digest(root)
|
|
289
|
+
continue
|
|
290
|
+
for path in sorted(item for item in root.rglob("*") if not item.is_dir()):
|
|
291
|
+
rows[str(path)] = _path_digest(path)
|
|
292
|
+
return rows
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _path_digest(path: Path) -> str:
|
|
296
|
+
if path.is_symlink():
|
|
297
|
+
value = b"symlink:" + os.readlink(path).encode("utf-8")
|
|
298
|
+
else:
|
|
299
|
+
value = path.read_bytes()
|
|
300
|
+
return "sha256:" + hashlib.sha256(value).hexdigest()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _excluded(path: Path, generated: frozenset[str]) -> bool:
|
|
304
|
+
relative = path.as_posix()
|
|
305
|
+
return any(relative == item or relative.startswith(item + "/") for item in generated)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _generated_paths(policies: Sequence[WritePolicy]) -> frozenset[str]:
|
|
309
|
+
return frozenset(
|
|
310
|
+
path
|
|
311
|
+
for policy in policies
|
|
312
|
+
for path in policy.auxiliary_policy.get("generatedPaths", ())
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _git_projection(root: Path) -> dict[str, Any]:
|
|
317
|
+
probe = subprocess.run(
|
|
318
|
+
["git", "-C", str(root), "rev-parse", "--is-inside-work-tree"],
|
|
319
|
+
capture_output=True,
|
|
320
|
+
text=True,
|
|
321
|
+
check=False,
|
|
322
|
+
)
|
|
323
|
+
if probe.returncode != 0:
|
|
324
|
+
return {"available": False}
|
|
325
|
+
git_dir = Path(_git_output(root, "rev-parse", "--path-format=absolute", "--git-dir"))
|
|
326
|
+
common_dir = Path(
|
|
327
|
+
_git_output(root, "rev-parse", "--path-format=absolute", "--git-common-dir")
|
|
328
|
+
)
|
|
329
|
+
branch_ref = _git_output(root, "symbolic-ref", "-q", "HEAD")
|
|
330
|
+
head = _git_output(root, "rev-parse", "HEAD")
|
|
331
|
+
index_path = Path(_git_output(root, "rev-parse", "--path-format=absolute", "--git-path", "index"))
|
|
332
|
+
return {
|
|
333
|
+
"available": True,
|
|
334
|
+
"head": head,
|
|
335
|
+
"allowedBranchRef": branch_ref,
|
|
336
|
+
"branchCommit": _git_output(root, "rev-parse", branch_ref),
|
|
337
|
+
"gitDir": str(git_dir.resolve()),
|
|
338
|
+
"gitCommonDir": str(common_dir.resolve()),
|
|
339
|
+
"indexDigest": _optional_file_digest(index_path),
|
|
340
|
+
"stagedPaths": sorted(_git_changed_paths(root, "--cached")),
|
|
341
|
+
"worktreeRegistration": _worktree_registration(root),
|
|
342
|
+
"reflogDigest": _reflog_digest(root),
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _worktree_registration(root: Path) -> str:
|
|
347
|
+
output = _git_output(root, "worktree", "list", "--porcelain")
|
|
348
|
+
blocks = output.split("\n\n")
|
|
349
|
+
prefix = f"worktree {root.resolve()}\n"
|
|
350
|
+
block = next((item for item in blocks if (item + "\n").startswith(prefix)), "")
|
|
351
|
+
stable_lines = [
|
|
352
|
+
line for line in block.splitlines()
|
|
353
|
+
if not line.startswith("HEAD ")
|
|
354
|
+
]
|
|
355
|
+
return "\n".join(stable_lines)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _optional_file_digest(path: Path) -> str | None:
|
|
359
|
+
return _path_digest(path) if path.is_file() else None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _git_output(root: Path, *args: str) -> str:
|
|
363
|
+
result = subprocess.run(
|
|
364
|
+
["git", "-C", str(root), *args],
|
|
365
|
+
capture_output=True,
|
|
366
|
+
text=True,
|
|
367
|
+
check=False,
|
|
368
|
+
)
|
|
369
|
+
if result.returncode != 0:
|
|
370
|
+
raise MutationAuditError(result.stderr.strip() or "Git projection failed")
|
|
371
|
+
return result.stdout.strip()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _snapshot_digest(
|
|
375
|
+
root: Path,
|
|
376
|
+
artifact_root: Path,
|
|
377
|
+
policy_digests: Sequence[str],
|
|
378
|
+
files: Mapping[str, str],
|
|
379
|
+
artifacts: Mapping[str, str],
|
|
380
|
+
scratch: Mapping[str, str],
|
|
381
|
+
git_projection: Mapping[str, Any],
|
|
382
|
+
orchestrator_paths: Sequence[str],
|
|
383
|
+
) -> str:
|
|
384
|
+
encoded = json.dumps(
|
|
385
|
+
{
|
|
386
|
+
"root": str(root),
|
|
387
|
+
"artifactRoot": str(artifact_root),
|
|
388
|
+
"policyDigests": list(policy_digests),
|
|
389
|
+
"files": files,
|
|
390
|
+
"artifacts": artifacts,
|
|
391
|
+
"scratch": scratch,
|
|
392
|
+
"gitProjection": git_projection,
|
|
393
|
+
"orchestratorPaths": list(orchestrator_paths),
|
|
394
|
+
},
|
|
395
|
+
sort_keys=True,
|
|
396
|
+
separators=(",", ":"),
|
|
397
|
+
).encode("utf-8")
|
|
398
|
+
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _changed_keys(before: Mapping[str, str], after: Mapping[str, str]) -> set[str]:
|
|
402
|
+
return {
|
|
403
|
+
key for key in set(before) | set(after) if before.get(key) != after.get(key)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _source_changes(
|
|
408
|
+
changed: set[str],
|
|
409
|
+
policies: Sequence[WritePolicy],
|
|
410
|
+
snapshot: MutationSnapshot,
|
|
411
|
+
) -> set[str]:
|
|
412
|
+
artifacts = _allowed_artifact_paths(policies)
|
|
413
|
+
generated = _generated_paths(policies)
|
|
414
|
+
orchestrator = {
|
|
415
|
+
Path(value).relative_to(snapshot.root).as_posix()
|
|
416
|
+
for value in snapshot.orchestrator_paths
|
|
417
|
+
if _is_relative_to(Path(value), snapshot.root)
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
path for path in changed
|
|
421
|
+
if not (
|
|
422
|
+
snapshot.artifact_root == snapshot.root
|
|
423
|
+
and any(_is_within(path, item) for item in artifacts)
|
|
424
|
+
)
|
|
425
|
+
and path not in orchestrator
|
|
426
|
+
and not _excluded(Path(path), generated)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _artifact_policy_failures(
|
|
431
|
+
snapshot: MutationSnapshot,
|
|
432
|
+
policies: Sequence[WritePolicy],
|
|
433
|
+
changed: set[str],
|
|
434
|
+
) -> list[str]:
|
|
435
|
+
allowed = _allowed_artifact_paths(policies)
|
|
436
|
+
orchestrator = {
|
|
437
|
+
Path(value).relative_to(snapshot.artifact_root).as_posix()
|
|
438
|
+
for value in snapshot.orchestrator_paths
|
|
439
|
+
if _is_relative_to(Path(value), snapshot.artifact_root)
|
|
440
|
+
}
|
|
441
|
+
unauthorized = {
|
|
442
|
+
path for path in changed
|
|
443
|
+
if not any(_is_within(path, item) for item in allowed)
|
|
444
|
+
and path not in orchestrator
|
|
445
|
+
}
|
|
446
|
+
if snapshot.artifact_root == snapshot.root:
|
|
447
|
+
return []
|
|
448
|
+
return ["artifact-root change exceeds batch policy union"] if unauthorized else []
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _allowed_artifact_paths(policies: Sequence[WritePolicy]) -> set[str]:
|
|
452
|
+
return {
|
|
453
|
+
path
|
|
454
|
+
for policy in policies
|
|
455
|
+
for path in policy.artifact_policy.get("allowedPaths", ())
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _is_relative_to(path: Path, root: Path) -> bool:
|
|
460
|
+
try:
|
|
461
|
+
path.relative_to(root)
|
|
462
|
+
except ValueError:
|
|
463
|
+
return False
|
|
464
|
+
return True
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _policy_violations(
|
|
468
|
+
before: MutationSnapshot,
|
|
469
|
+
after: MutationSnapshot,
|
|
470
|
+
policies: tuple[WritePolicy, ...],
|
|
471
|
+
source_changes: set[str],
|
|
472
|
+
out_of_plan_edits: Sequence[str],
|
|
473
|
+
) -> list[str]:
|
|
474
|
+
if all(policy.source_mode == "source-readonly" for policy in policies):
|
|
475
|
+
failures = ["readonly source changed"] if source_changes else []
|
|
476
|
+
if before.git_projection != after.git_projection:
|
|
477
|
+
failures.append("gitPolicy disabled but Git projection changed")
|
|
478
|
+
return failures
|
|
479
|
+
policy = next(row for row in policies if row.source_mode == "project-mutation")
|
|
480
|
+
failures = _source_policy_failures(policy, source_changes, out_of_plan_edits)
|
|
481
|
+
failures.extend(_git_policy_failures(before, after, policy, source_changes, out_of_plan_edits))
|
|
482
|
+
return failures
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _source_policy_failures(
|
|
486
|
+
policy: WritePolicy,
|
|
487
|
+
changed: set[str],
|
|
488
|
+
out_of_plan_edits: Sequence[str],
|
|
489
|
+
) -> list[str]:
|
|
490
|
+
planned = set(policy.source_policy.get("plannedPaths", ()))
|
|
491
|
+
declared = set(out_of_plan_edits)
|
|
492
|
+
protected = set(policy.source_policy.get("protectedPaths", ()))
|
|
493
|
+
failures: list[str] = []
|
|
494
|
+
if not changed <= planned | declared:
|
|
495
|
+
failures.append("source changes exceed planned and declared out-of-plan paths")
|
|
496
|
+
if not declared <= changed:
|
|
497
|
+
failures.append("declared out-of-plan path did not change")
|
|
498
|
+
if any(_is_within(path, item) for path in changed for item in protected):
|
|
499
|
+
failures.append("protected path changed")
|
|
500
|
+
return failures
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _git_policy_failures(
|
|
504
|
+
before: MutationSnapshot,
|
|
505
|
+
after: MutationSnapshot,
|
|
506
|
+
policy: WritePolicy,
|
|
507
|
+
changed: set[str],
|
|
508
|
+
out_of_plan_edits: Sequence[str],
|
|
509
|
+
) -> list[str]:
|
|
510
|
+
git = policy.git_policy
|
|
511
|
+
root = before.root
|
|
512
|
+
failures: list[str] = []
|
|
513
|
+
if before.git_projection.get("head") != git.get("expectedBaseCommit"):
|
|
514
|
+
failures.append("audit baseline does not match expectedBaseCommit")
|
|
515
|
+
if after.git_projection.get("allowedBranchRef") != git.get("allowedBranchRef"):
|
|
516
|
+
failures.append("assigned branch changed")
|
|
517
|
+
if after.git_projection.get("gitDir") != git.get("worktreeGitDir"):
|
|
518
|
+
failures.append("assigned worktree Git directory changed")
|
|
519
|
+
if after.git_projection.get("gitCommonDir") != git.get("gitCommonDir"):
|
|
520
|
+
failures.append("assigned Git common directory changed")
|
|
521
|
+
if before.git_projection.get("worktreeRegistration") != after.git_projection.get("worktreeRegistration"):
|
|
522
|
+
failures.append("worktree registration changed")
|
|
523
|
+
if after.git_projection.get("stagedPaths"):
|
|
524
|
+
failures.append("final Git index contains staged changes")
|
|
525
|
+
if not _is_ancestor(root, str(git.get("expectedBaseCommit")), str(after.git_projection.get("head"))):
|
|
526
|
+
failures.append("final HEAD is not a fast-forward descendant")
|
|
527
|
+
allowed = set(policy.source_policy.get("plannedPaths", ())) | set(out_of_plan_edits)
|
|
528
|
+
if any(
|
|
529
|
+
not paths <= allowed
|
|
530
|
+
for paths in _commit_paths_by_commit(
|
|
531
|
+
root,
|
|
532
|
+
str(git.get("expectedBaseCommit")),
|
|
533
|
+
str(after.git_projection.get("head")),
|
|
534
|
+
)
|
|
535
|
+
):
|
|
536
|
+
failures.append("commit chain changed paths outside source policy")
|
|
537
|
+
if not changed <= allowed:
|
|
538
|
+
failures.append("working tree changed paths outside source policy")
|
|
539
|
+
return failures
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _is_ancestor(root: Path, base: str, head: str) -> bool:
|
|
543
|
+
result = subprocess.run(
|
|
544
|
+
["git", "-C", str(root), "merge-base", "--is-ancestor", base, head],
|
|
545
|
+
capture_output=True,
|
|
546
|
+
check=False,
|
|
547
|
+
)
|
|
548
|
+
return result.returncode == 0
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _commit_paths_by_commit(root: Path, base: str, head: str) -> tuple[set[str], ...]:
|
|
552
|
+
if base == head:
|
|
553
|
+
return ()
|
|
554
|
+
commits = _git_output(root, "rev-list", "--reverse", f"{base}..{head}").splitlines()
|
|
555
|
+
return tuple(
|
|
556
|
+
{
|
|
557
|
+
line
|
|
558
|
+
for line in _git_output(
|
|
559
|
+
root, "diff-tree", "--no-commit-id", "--name-only", "-r", commit
|
|
560
|
+
).splitlines()
|
|
561
|
+
if line
|
|
562
|
+
}
|
|
563
|
+
for commit in commits
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _reflog_digest(root: Path) -> str:
|
|
568
|
+
payload = _git_output(root, "reflog", "show", "--no-abbrev", "--format=%H %gs", "HEAD")
|
|
569
|
+
return "sha256:" + hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _git_changed_paths(root: Path, *args: str) -> set[str]:
|
|
573
|
+
result = subprocess.run(
|
|
574
|
+
["git", "-C", str(root), "diff", *args, "--name-only"],
|
|
575
|
+
capture_output=True,
|
|
576
|
+
text=True,
|
|
577
|
+
check=False,
|
|
578
|
+
)
|
|
579
|
+
if result.returncode != 0:
|
|
580
|
+
raise MutationAuditError(result.stderr.strip() or "Git diff failed")
|
|
581
|
+
return {line for line in result.stdout.splitlines() if line}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _is_within(path: str, parent: str) -> bool:
|
|
585
|
+
return path == parent or path.startswith(parent + "/")
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _terminal_status(
|
|
589
|
+
policies: Sequence[WritePolicy],
|
|
590
|
+
*,
|
|
591
|
+
source_changed: bool,
|
|
592
|
+
git_changed: bool,
|
|
593
|
+
attempt_succeeded: bool,
|
|
594
|
+
result_present: bool,
|
|
595
|
+
violations: Sequence[str],
|
|
596
|
+
) -> str:
|
|
597
|
+
project_mutation = any(row.source_mode == "project-mutation" for row in policies)
|
|
598
|
+
if project_mutation and (source_changed or git_changed) and (
|
|
599
|
+
not attempt_succeeded or not result_present
|
|
600
|
+
):
|
|
601
|
+
return "mutation-present-unresolved"
|
|
602
|
+
if violations:
|
|
603
|
+
return "contract-failed-unattributed"
|
|
604
|
+
if not attempt_succeeded or not result_present:
|
|
605
|
+
return "failed-no-mutation"
|
|
606
|
+
return "ok"
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _retry_allowed(
|
|
610
|
+
status: str,
|
|
611
|
+
source_changed: bool,
|
|
612
|
+
git_changed: bool,
|
|
613
|
+
scratch_changed: bool,
|
|
614
|
+
) -> bool:
|
|
615
|
+
if status in {"mutation-present-unresolved", "contract-failed-unattributed"}:
|
|
616
|
+
return False
|
|
617
|
+
if source_changed or git_changed:
|
|
618
|
+
return False
|
|
619
|
+
return status == "failed-no-mutation" and not scratch_changed
|
|
@@ -11,9 +11,10 @@ from .consumers import (
|
|
|
11
11
|
carry_head_commit,
|
|
12
12
|
read_stage_consumer_state,
|
|
13
13
|
)
|
|
14
|
+
from . import next_phase
|
|
14
15
|
from .paths import RunRef, task_manifest_file
|
|
15
16
|
from .stage_map import StageMapError, load_task_stage_map
|
|
16
|
-
from .workflow import
|
|
17
|
+
from .workflow import PHASE_SEQUENCE
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@dataclass(frozen=True)
|
|
@@ -22,7 +23,6 @@ class ImplementationOutcome:
|
|
|
22
23
|
stages: list[int] = field(default_factory=list)
|
|
23
24
|
head_commit: str = ""
|
|
24
25
|
report_path: str = ""
|
|
25
|
-
next_recommended_phase: str = ""
|
|
26
26
|
reason: str = ""
|
|
27
27
|
|
|
28
28
|
|
|
@@ -66,7 +66,6 @@ def derive_implementation_outcome(task_root: Path) -> ImplementationOutcome:
|
|
|
66
66
|
stages=sorted(required_stages),
|
|
67
67
|
head_commit=str(latest_done.get("head_commit") or "") or carry_head_commit(latest_carry),
|
|
68
68
|
report_path=_latest_implementation_report(task_root),
|
|
69
|
-
next_recommended_phase=DEFAULT_NEXT_PHASE["implementation"],
|
|
70
69
|
reason="all implementation stages have pass-grade carry and done rows",
|
|
71
70
|
)
|
|
72
71
|
|
|
@@ -174,7 +173,22 @@ def _promote_workflow(workflow: dict[str, Any], outcome: ImplementationOutcome)
|
|
|
174
173
|
workflow["currentPhaseState"] = "completed"
|
|
175
174
|
phase_states["implementation"] = "completed"
|
|
176
175
|
workflow["lastCompletedPhase"] = "implementation"
|
|
177
|
-
|
|
176
|
+
# 재조정기의 사실은 "모든 stage 가 끝났다"는 상태다. 대상 phase 는 마지막
|
|
177
|
+
# stage 검증이 이미 정했으므로 건드리지 않는다 (ADR-0004).
|
|
178
|
+
#
|
|
179
|
+
# 대상 phase 가 비어 있으면 status 도 올리지 않는다. `ready` 는 "지금 이
|
|
180
|
+
# phase 를 시작할 수 있다"는 뜻인데 가리킬 phase 가 없으면 시작할 대상이
|
|
181
|
+
# 없다. 마지막 stage 리포트에 라우팅 대상이 없으면 이 자리에 빈 phase 의
|
|
182
|
+
# pending 포인터가 들어오므로, 그때는 그대로 둔다.
|
|
183
|
+
pointer = next_phase.promote(workflow.get("nextRecommendedPhase"))
|
|
184
|
+
if pointer["phase"]:
|
|
185
|
+
workflow["nextRecommendedPhase"] = next_phase.make(
|
|
186
|
+
phase=pointer["phase"],
|
|
187
|
+
status=next_phase.STATUS_READY,
|
|
188
|
+
rationale=pointer["rationale"],
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
workflow["nextRecommendedPhase"] = pointer
|
|
178
192
|
after = json.dumps(workflow, sort_keys=True, ensure_ascii=False)
|
|
179
193
|
return before != after
|
|
180
194
|
|
|
@@ -195,7 +209,9 @@ def _update_task_catalog(project_root: Path, manifest: dict[str, Any]) -> bool:
|
|
|
195
209
|
entry["currentPhase"] = workflow.get("currentPhase", "")
|
|
196
210
|
entry["currentPhaseState"] = workflow.get("currentPhaseState", "")
|
|
197
211
|
entry["lastCompletedPhase"] = workflow.get("lastCompletedPhase", "")
|
|
198
|
-
entry["nextRecommendedPhase"] =
|
|
212
|
+
entry["nextRecommendedPhase"] = next_phase.promote(
|
|
213
|
+
workflow.get("nextRecommendedPhase")
|
|
214
|
+
)
|
|
199
215
|
entry["latestReportPath"] = manifest.get("latestReportPath", "")
|
|
200
216
|
entry["latestRunStatus"] = manifest.get("latestRunStatus", entry.get("latestRunStatus", ""))
|
|
201
217
|
entry["currentStatus"] = manifest.get("currentStatus", entry.get("currentStatus", ""))
|
|
@@ -39,7 +39,9 @@ def record_start(home: Path, *, project_id: str, project_root: str,
|
|
|
39
39
|
okstra_version: str = "",
|
|
40
40
|
brief_sha256: str = "",
|
|
41
41
|
initial_status: str = "running",
|
|
42
|
-
final_status_rel: str = ""
|
|
42
|
+
final_status_rel: str = "",
|
|
43
|
+
execution_manifest_path: str = "",
|
|
44
|
+
role_execution_refs: list | None = None) -> str:
|
|
43
45
|
"""run 시작 시점의 인덱스/invocation/meta 를 1 트랜잭션으로 기록.
|
|
44
46
|
반환값: 생성된 runId.
|
|
45
47
|
"""
|
|
@@ -54,7 +56,9 @@ def record_start(home: Path, *, project_id: str, project_root: str,
|
|
|
54
56
|
finished_at=when if is_terminal else None,
|
|
55
57
|
workers=workers, lead_model=lead_model, validation="not-run",
|
|
56
58
|
run_dir_rel=run_dir_rel, final_report_rel=final_report_rel,
|
|
57
|
-
final_status_rel=final_status_rel
|
|
59
|
+
final_status_rel=final_status_rel,
|
|
60
|
+
execution_manifest_path=execution_manifest_path,
|
|
61
|
+
role_execution_refs=role_execution_refs)
|
|
58
62
|
# okstra-ctl 가 미리 'reserving' 예약 row 를 작성했을 수 있다(P1-2). 같은
|
|
59
63
|
# runId 의 예약 row 가 있으면 update; 없으면 append.
|
|
60
64
|
existing_rows = read_jsonl(home / "active.jsonl")
|