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,293 @@
|
|
|
1
|
+
"""Mutation recovery decisions after sealed or unsealed implementer attempts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal, Mapping, Sequence
|
|
7
|
+
|
|
8
|
+
from .attempt_evidence import AttemptEvidenceSeal, _sha
|
|
9
|
+
from .dispatch_state import build_dispatch_id
|
|
10
|
+
from .execution_identity import ExecutionManifestError
|
|
11
|
+
from .execution_manifest import (
|
|
12
|
+
_replace_manifest,
|
|
13
|
+
_write_manifest_over_existing,
|
|
14
|
+
read_execution_manifest,
|
|
15
|
+
)
|
|
16
|
+
from .run_context import task_mutex
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MutationRecoveryError(ValueError):
|
|
20
|
+
"""Raised when a recovery decision cannot be recorded or applied."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class MutationRecoveryInput:
|
|
25
|
+
manifest_path: Path
|
|
26
|
+
source_invocation_ref: str
|
|
27
|
+
source_attempt_ref: str
|
|
28
|
+
source_evidence_seal_ref: str | None
|
|
29
|
+
original_baseline_digest: str
|
|
30
|
+
terminal_source_diff_digest: str
|
|
31
|
+
terminal_git_chain_digest: str
|
|
32
|
+
verifier_role_execution_ref: str | None
|
|
33
|
+
verifier_invocation_ref: str | None
|
|
34
|
+
verifier_participant_ref: str | None
|
|
35
|
+
implementer_participant_ref: str
|
|
36
|
+
recovered_evidence_refs: tuple[Mapping[str, Any], ...]
|
|
37
|
+
recovered_out_of_plan_edits: tuple[Mapping[str, Any], ...]
|
|
38
|
+
current_source_diff_digest: str
|
|
39
|
+
current_git_chain_digest: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class MutationRecoveryDecision:
|
|
44
|
+
recovery_ref: str
|
|
45
|
+
source_invocation_ref: str
|
|
46
|
+
source_attempt_ref: str
|
|
47
|
+
source_evidence_seal_ref: str | None
|
|
48
|
+
original_baseline_digest: str
|
|
49
|
+
terminal_source_diff_digest: str
|
|
50
|
+
terminal_git_chain_digest: str
|
|
51
|
+
verifier_role_execution_ref: str | None
|
|
52
|
+
verifier_invocation_ref: str | None
|
|
53
|
+
recovered_evidence_refs: tuple[Mapping[str, Any], ...]
|
|
54
|
+
recovered_out_of_plan_edits: tuple[Mapping[str, Any], ...]
|
|
55
|
+
unmet_obligations: tuple[str, ...]
|
|
56
|
+
decision: Literal["carry-forward", "reject", "cleanup-confirmed"]
|
|
57
|
+
reason: str = ""
|
|
58
|
+
|
|
59
|
+
def to_payload(self) -> dict[str, Any]:
|
|
60
|
+
return {
|
|
61
|
+
"recoveryRef": self.recovery_ref,
|
|
62
|
+
"sourceInvocationRef": self.source_invocation_ref,
|
|
63
|
+
"sourceAttemptRef": self.source_attempt_ref,
|
|
64
|
+
"sourceEvidenceSealRef": self.source_evidence_seal_ref,
|
|
65
|
+
"originalBaselineDigest": self.original_baseline_digest,
|
|
66
|
+
"terminalSourceDiffDigest": self.terminal_source_diff_digest,
|
|
67
|
+
"terminalGitChainDigest": self.terminal_git_chain_digest,
|
|
68
|
+
"verifierRoleExecutionRef": self.verifier_role_execution_ref,
|
|
69
|
+
"verifierInvocationRef": self.verifier_invocation_ref,
|
|
70
|
+
"recoveredEvidenceRefs": [dict(row) for row in self.recovered_evidence_refs],
|
|
71
|
+
"recoveredOutOfPlanEdits": [
|
|
72
|
+
dict(row) for row in self.recovered_out_of_plan_edits
|
|
73
|
+
],
|
|
74
|
+
"unmetObligations": list(self.unmet_obligations),
|
|
75
|
+
"decision": self.decision,
|
|
76
|
+
"reason": self.reason,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_payload(cls, payload: Mapping[str, Any]) -> "MutationRecoveryDecision":
|
|
81
|
+
return cls(
|
|
82
|
+
recovery_ref=str(payload["recoveryRef"]),
|
|
83
|
+
source_invocation_ref=str(payload["sourceInvocationRef"]),
|
|
84
|
+
source_attempt_ref=str(payload["sourceAttemptRef"]),
|
|
85
|
+
source_evidence_seal_ref=payload.get("sourceEvidenceSealRef"),
|
|
86
|
+
original_baseline_digest=str(payload.get("originalBaselineDigest") or ""),
|
|
87
|
+
terminal_source_diff_digest=str(
|
|
88
|
+
payload.get("terminalSourceDiffDigest") or ""
|
|
89
|
+
),
|
|
90
|
+
terminal_git_chain_digest=str(payload.get("terminalGitChainDigest") or ""),
|
|
91
|
+
verifier_role_execution_ref=payload.get("verifierRoleExecutionRef"),
|
|
92
|
+
verifier_invocation_ref=payload.get("verifierInvocationRef"),
|
|
93
|
+
recovered_evidence_refs=tuple(payload.get("recoveredEvidenceRefs") or ()),
|
|
94
|
+
recovered_out_of_plan_edits=tuple(
|
|
95
|
+
payload.get("recoveredOutOfPlanEdits") or ()
|
|
96
|
+
),
|
|
97
|
+
unmet_obligations=tuple(payload.get("unmetObligations") or ()),
|
|
98
|
+
decision=str(payload["decision"]),
|
|
99
|
+
reason=str(payload.get("reason") or ""),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _seal_for(manifest, seal_ref: str | None) -> AttemptEvidenceSeal | None:
|
|
104
|
+
if not seal_ref:
|
|
105
|
+
return None
|
|
106
|
+
row = next(
|
|
107
|
+
(
|
|
108
|
+
item
|
|
109
|
+
for item in manifest.attempt_evidence_seals
|
|
110
|
+
if item.get("sealRef") == seal_ref
|
|
111
|
+
),
|
|
112
|
+
None,
|
|
113
|
+
)
|
|
114
|
+
return AttemptEvidenceSeal.from_payload(row) if row is not None else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _verify_sealed_bytes(seal: AttemptEvidenceSeal) -> None:
|
|
118
|
+
stream_path = seal.host_event_stream.get("path")
|
|
119
|
+
if isinstance(stream_path, str) and stream_path:
|
|
120
|
+
data = Path(stream_path).read_bytes()
|
|
121
|
+
if _sha(data) != seal.host_event_stream.get("sha256"):
|
|
122
|
+
raise MutationRecoveryError("sealed bytes changed")
|
|
123
|
+
if len(data) != seal.host_event_stream.get("byteLength"):
|
|
124
|
+
raise MutationRecoveryError("sealed bytes changed")
|
|
125
|
+
for artifact in seal.artifacts:
|
|
126
|
+
path = Path(str(artifact["normalizedPath"]))
|
|
127
|
+
data = path.read_bytes() if path.is_file() else b""
|
|
128
|
+
if _sha(data) != artifact.get("sha256"):
|
|
129
|
+
raise MutationRecoveryError("sealed bytes changed")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def evaluate_mutation_recovery(
|
|
133
|
+
recovery: MutationRecoveryInput,
|
|
134
|
+
) -> MutationRecoveryDecision:
|
|
135
|
+
"""Decide carry-forward or reject from sealed evidence."""
|
|
136
|
+
manifest = read_execution_manifest(recovery.manifest_path)
|
|
137
|
+
attempt = next(
|
|
138
|
+
(
|
|
139
|
+
row
|
|
140
|
+
for row in manifest.attempts
|
|
141
|
+
if build_dispatch_id(row.invocation_ref, row.attempt)
|
|
142
|
+
== recovery.source_attempt_ref
|
|
143
|
+
),
|
|
144
|
+
None,
|
|
145
|
+
)
|
|
146
|
+
if attempt is None:
|
|
147
|
+
raise MutationRecoveryError("source attempt is unknown")
|
|
148
|
+
if attempt.status == "evidence-unsealed":
|
|
149
|
+
return MutationRecoveryDecision(
|
|
150
|
+
recovery_ref=f"recovery-reject-{attempt.attempt}",
|
|
151
|
+
source_invocation_ref=recovery.source_invocation_ref,
|
|
152
|
+
source_attempt_ref=recovery.source_attempt_ref,
|
|
153
|
+
source_evidence_seal_ref=None,
|
|
154
|
+
original_baseline_digest=recovery.original_baseline_digest,
|
|
155
|
+
terminal_source_diff_digest=recovery.terminal_source_diff_digest,
|
|
156
|
+
terminal_git_chain_digest=recovery.terminal_git_chain_digest,
|
|
157
|
+
verifier_role_execution_ref=None,
|
|
158
|
+
verifier_invocation_ref=None,
|
|
159
|
+
recovered_evidence_refs=(),
|
|
160
|
+
recovered_out_of_plan_edits=(),
|
|
161
|
+
unmet_obligations=("evidence-unsealed",),
|
|
162
|
+
decision="reject",
|
|
163
|
+
reason="evidence-unsealed",
|
|
164
|
+
)
|
|
165
|
+
unmet: list[str] = []
|
|
166
|
+
if not recovery.source_evidence_seal_ref:
|
|
167
|
+
unmet.append("missing-source-seal")
|
|
168
|
+
if not recovery.recovered_evidence_refs:
|
|
169
|
+
unmet.append("missing-host-tdd-events")
|
|
170
|
+
if (
|
|
171
|
+
recovery.verifier_participant_ref
|
|
172
|
+
and recovery.verifier_participant_ref == recovery.implementer_participant_ref
|
|
173
|
+
):
|
|
174
|
+
unmet.append("verifier-not-independent")
|
|
175
|
+
if not recovery.verifier_invocation_ref:
|
|
176
|
+
unmet.append("missing-verifier-invocation")
|
|
177
|
+
recovered_paths = {
|
|
178
|
+
str(row.get("path"))
|
|
179
|
+
for row in recovery.recovered_out_of_plan_edits
|
|
180
|
+
if isinstance(row, Mapping) and row.get("path")
|
|
181
|
+
}
|
|
182
|
+
sealed = _seal_for(manifest, recovery.source_evidence_seal_ref)
|
|
183
|
+
if sealed is not None:
|
|
184
|
+
_verify_sealed_bytes(sealed)
|
|
185
|
+
if recovered_paths - set(sealed.changed_paths):
|
|
186
|
+
raise MutationRecoveryError("out-of-plan path set differs")
|
|
187
|
+
if recovered_paths:
|
|
188
|
+
raise MutationRecoveryError("out-of-plan path set differs")
|
|
189
|
+
if unmet:
|
|
190
|
+
return MutationRecoveryDecision(
|
|
191
|
+
recovery_ref=f"recovery-reject-{attempt.attempt}",
|
|
192
|
+
source_invocation_ref=recovery.source_invocation_ref,
|
|
193
|
+
source_attempt_ref=recovery.source_attempt_ref,
|
|
194
|
+
source_evidence_seal_ref=recovery.source_evidence_seal_ref,
|
|
195
|
+
original_baseline_digest=recovery.original_baseline_digest,
|
|
196
|
+
terminal_source_diff_digest=recovery.terminal_source_diff_digest,
|
|
197
|
+
terminal_git_chain_digest=recovery.terminal_git_chain_digest,
|
|
198
|
+
verifier_role_execution_ref=recovery.verifier_role_execution_ref,
|
|
199
|
+
verifier_invocation_ref=recovery.verifier_invocation_ref,
|
|
200
|
+
recovered_evidence_refs=recovery.recovered_evidence_refs,
|
|
201
|
+
recovered_out_of_plan_edits=recovery.recovered_out_of_plan_edits,
|
|
202
|
+
unmet_obligations=tuple(unmet),
|
|
203
|
+
decision="reject",
|
|
204
|
+
reason="unmet-obligations",
|
|
205
|
+
)
|
|
206
|
+
return MutationRecoveryDecision(
|
|
207
|
+
recovery_ref=f"recovery-carry-{attempt.attempt}",
|
|
208
|
+
source_invocation_ref=recovery.source_invocation_ref,
|
|
209
|
+
source_attempt_ref=recovery.source_attempt_ref,
|
|
210
|
+
source_evidence_seal_ref=recovery.source_evidence_seal_ref,
|
|
211
|
+
original_baseline_digest=recovery.original_baseline_digest,
|
|
212
|
+
terminal_source_diff_digest=recovery.terminal_source_diff_digest,
|
|
213
|
+
terminal_git_chain_digest=recovery.terminal_git_chain_digest,
|
|
214
|
+
verifier_role_execution_ref=recovery.verifier_role_execution_ref,
|
|
215
|
+
verifier_invocation_ref=recovery.verifier_invocation_ref,
|
|
216
|
+
recovered_evidence_refs=recovery.recovered_evidence_refs,
|
|
217
|
+
recovered_out_of_plan_edits=recovery.recovered_out_of_plan_edits,
|
|
218
|
+
unmet_obligations=(),
|
|
219
|
+
decision="carry-forward",
|
|
220
|
+
reason="",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def confirm_mutation_cleanup(
|
|
225
|
+
manifest_path: Path,
|
|
226
|
+
recovery_ref: str,
|
|
227
|
+
*,
|
|
228
|
+
original_baseline_digest: str,
|
|
229
|
+
current_baseline_digest: str,
|
|
230
|
+
task_key: str,
|
|
231
|
+
) -> MutationRecoveryDecision:
|
|
232
|
+
"""Allow a new invocation after user cleanup matches the original baseline."""
|
|
233
|
+
if current_baseline_digest != original_baseline_digest:
|
|
234
|
+
raise MutationRecoveryError("cleanup does not restore the original baseline")
|
|
235
|
+
with task_mutex(task_key):
|
|
236
|
+
manifest = read_execution_manifest(manifest_path)
|
|
237
|
+
source = next(
|
|
238
|
+
(
|
|
239
|
+
MutationRecoveryDecision.from_payload(row)
|
|
240
|
+
for row in manifest.mutation_recovery_decisions
|
|
241
|
+
if row.get("recoveryRef") == recovery_ref
|
|
242
|
+
),
|
|
243
|
+
None,
|
|
244
|
+
)
|
|
245
|
+
if source is None or source.decision != "reject":
|
|
246
|
+
raise MutationRecoveryError("cleanup requires a prior reject decision")
|
|
247
|
+
confirmed = MutationRecoveryDecision(
|
|
248
|
+
recovery_ref=f"{recovery_ref}-cleanup",
|
|
249
|
+
source_invocation_ref=source.source_invocation_ref,
|
|
250
|
+
source_attempt_ref=source.source_attempt_ref,
|
|
251
|
+
source_evidence_seal_ref=None,
|
|
252
|
+
original_baseline_digest=original_baseline_digest,
|
|
253
|
+
terminal_source_diff_digest="",
|
|
254
|
+
terminal_git_chain_digest="",
|
|
255
|
+
verifier_role_execution_ref=None,
|
|
256
|
+
verifier_invocation_ref=None,
|
|
257
|
+
recovered_evidence_refs=(),
|
|
258
|
+
recovered_out_of_plan_edits=(),
|
|
259
|
+
unmet_obligations=(),
|
|
260
|
+
decision="cleanup-confirmed",
|
|
261
|
+
reason="cleanup-confirmed",
|
|
262
|
+
)
|
|
263
|
+
updated = _replace_manifest(
|
|
264
|
+
manifest,
|
|
265
|
+
mutation_recovery_decisions=(
|
|
266
|
+
*manifest.mutation_recovery_decisions,
|
|
267
|
+
confirmed.to_payload(),
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
_write_manifest_over_existing(manifest_path, updated)
|
|
271
|
+
return confirmed
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def start_new_invocation(recovery: MutationRecoveryInput) -> None:
|
|
275
|
+
"""Refuse a new invocation while an unresolved mutation has no decision."""
|
|
276
|
+
manifest = read_execution_manifest(recovery.manifest_path)
|
|
277
|
+
unresolved = [
|
|
278
|
+
row
|
|
279
|
+
for row in manifest.attempts
|
|
280
|
+
if row.status in {"mutation-present-unresolved", "evidence-unsealed"}
|
|
281
|
+
]
|
|
282
|
+
confirmed = {
|
|
283
|
+
row.get("sourceAttemptRef")
|
|
284
|
+
for row in manifest.mutation_recovery_decisions
|
|
285
|
+
if row.get("decision") in {"carry-forward", "cleanup-confirmed"}
|
|
286
|
+
}
|
|
287
|
+
pending = [
|
|
288
|
+
row
|
|
289
|
+
for row in unresolved
|
|
290
|
+
if build_dispatch_id(row.invocation_ref, row.attempt) not in confirmed
|
|
291
|
+
]
|
|
292
|
+
if pending:
|
|
293
|
+
raise MutationRecoveryError("recovery decision required")
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""다음 Phase 포인터의 shape · 승격 · 투영 SSOT.
|
|
2
|
+
|
|
3
|
+
포인터는 `{phase, status, rationale}` 구조체다. 저작은 리드(report-writer)가
|
|
4
|
+
하고, Phase 7 검증기가 리포트의 Phase 라우팅과 대조해 어긋나면 정정한다.
|
|
5
|
+
이 모듈은 세 가지만 소유한다 — 구조체 판별, 구형 문자열 승격, 리포트 투영.
|
|
6
|
+
다음 phase 를 계산하는 다른 알고리즘을 다시 만들지 않는다.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
STATUS_READY = "ready"
|
|
13
|
+
STATUS_PENDING = "pending"
|
|
14
|
+
STATUS_BLOCKED = "blocked"
|
|
15
|
+
STATUS_TERMINAL = "terminal"
|
|
16
|
+
|
|
17
|
+
POINTER_STATUSES = frozenset(
|
|
18
|
+
{STATUS_READY, STATUS_PENDING, STATUS_BLOCKED, STATUS_TERMINAL}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# 구형 문자열 포인터가 쓰던 센티널 → status. 이 표는 승격에만 쓰이며
|
|
22
|
+
# 새 포인터를 만드는 데는 절대 쓰지 않는다.
|
|
23
|
+
_LEGACY_SENTINELS = {
|
|
24
|
+
"": STATUS_PENDING,
|
|
25
|
+
"unknown": STATUS_PENDING,
|
|
26
|
+
"pending-routing-decision": STATUS_PENDING,
|
|
27
|
+
"pending-release-handoff": STATUS_PENDING,
|
|
28
|
+
"done-or-follow-up": STATUS_TERMINAL,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# 라우팅 필드가 없어 항상 terminal 인 task-type.
|
|
32
|
+
_TERMINAL_TASK_TYPES = frozenset({"release-handoff"})
|
|
33
|
+
|
|
34
|
+
# 라우팅 필드가 없어 항상 pending 인 사이드트랙 task-type.
|
|
35
|
+
_SIDETRACK_TASK_TYPES = frozenset(
|
|
36
|
+
{
|
|
37
|
+
"improvement-discovery",
|
|
38
|
+
"project-analysis",
|
|
39
|
+
"feature-analysis",
|
|
40
|
+
"change-impact-analysis",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# project() 가 명시 분기로 다루는 task-type 전체를 손으로 적은 선언.
|
|
45
|
+
# project() 는 이 집합을 읽지 않는다 — 분기는 아래에 그대로 나열돼 있다.
|
|
46
|
+
#
|
|
47
|
+
# 이 집합을 대조하는 계약 테스트는 PHASE_SEQUENCE 에 있으면서 여기 없는 phase 만
|
|
48
|
+
# 잡아낸다. 선언 두 개를 비교하는 것이므로, 여기에 이름을 적고 project() 에 분기를
|
|
49
|
+
# 안 넣으면 그 phase 는 여전히 pending 폴백으로 조용히 흐른다. 분기의 실재는
|
|
50
|
+
# tests/contract/test_next_phase_projection.py 의
|
|
51
|
+
# test_project_has_a_live_branch_for_every_lifecycle_phase 가 정상 리포트를 실제로
|
|
52
|
+
# 투영해 확인한다.
|
|
53
|
+
HANDLED_TASK_TYPES = frozenset(
|
|
54
|
+
_TERMINAL_TASK_TYPES
|
|
55
|
+
| _SIDETRACK_TASK_TYPES
|
|
56
|
+
| {
|
|
57
|
+
"requirements-discovery",
|
|
58
|
+
"error-analysis",
|
|
59
|
+
"implementation-option-selection",
|
|
60
|
+
"implementation-planning",
|
|
61
|
+
"implementation",
|
|
62
|
+
"final-verification",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# implementation-option-selection 의 routing enum 중 phase 가 아닌 값.
|
|
67
|
+
_OPTION_SELECTION_NON_PHASE = {
|
|
68
|
+
"pending-direction-selection": STATUS_PENDING,
|
|
69
|
+
"blocked": STATUS_BLOCKED,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# final-verification 의 routing enum 중 phase 이름이 아니라 phase 에 붙은 범위
|
|
73
|
+
# 한정자인 값 → 실제로 실행할 phase. `release-handoff(stage-group)` 은 넘길 stage
|
|
74
|
+
# 묶음을 좁힌다는 뜻이지 다른 phase 가 아니다. 범위는 위저드의 handoff_stage_pick
|
|
75
|
+
# 이 따로 묻는다. 토큰을 그대로 phase 에 두면 `autofill_task_type` 이 그 문자열을
|
|
76
|
+
# 셸에 TASK_TYPE 으로 건네는데, 그런 task-type 은 존재하지 않는다.
|
|
77
|
+
_FINAL_VERIFICATION_PHASE_ALIASES = {
|
|
78
|
+
"release-handoff(stage-group)": "release-handoff",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def make(
|
|
83
|
+
phase: str = "", status: str = STATUS_PENDING, rationale: str = ""
|
|
84
|
+
) -> dict[str, str]:
|
|
85
|
+
"""포인터 구조체 하나를 만든다. status 가 허용 집합 밖이면 거부한다."""
|
|
86
|
+
if status not in POINTER_STATUSES:
|
|
87
|
+
raise ValueError(f"unknown next-phase status: {status!r}")
|
|
88
|
+
return {"phase": phase, "status": status, "rationale": rationale}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_pointer(value: Any) -> bool:
|
|
92
|
+
"""value 가 유효한 포인터 구조체인지."""
|
|
93
|
+
return (
|
|
94
|
+
isinstance(value, Mapping)
|
|
95
|
+
and isinstance(value.get("phase"), str)
|
|
96
|
+
and value.get("status") in POINTER_STATUSES
|
|
97
|
+
and isinstance(value.get("rationale"), str)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def promote(value: Any) -> dict[str, str]:
|
|
102
|
+
"""구형 문자열·None 을 포인터로 올린다.
|
|
103
|
+
|
|
104
|
+
이미 포인터인 입력은 같은 값을 담은 새 dict 로 돌려준다 — 입력을 그대로
|
|
105
|
+
돌려주지 않는다. 호출부가 반환값을 그 자리에서 변형해도 manifest 원본이
|
|
106
|
+
오염되지 않아야 하기 때문이다.
|
|
107
|
+
|
|
108
|
+
pre-v1 무호환 원칙의 의도적 예외다. 근거는 업그레이드를 가로지르는
|
|
109
|
+
진행 중 태스크가 영구히 막히는 기존 사례 — ADR-0004 Consequences.
|
|
110
|
+
"""
|
|
111
|
+
if is_pointer(value):
|
|
112
|
+
return {
|
|
113
|
+
"phase": value["phase"],
|
|
114
|
+
"status": value["status"],
|
|
115
|
+
"rationale": value["rationale"],
|
|
116
|
+
}
|
|
117
|
+
if not isinstance(value, str):
|
|
118
|
+
return make()
|
|
119
|
+
text = value.strip()
|
|
120
|
+
if text in _LEGACY_SENTINELS:
|
|
121
|
+
return make(status=_LEGACY_SENTINELS[text])
|
|
122
|
+
return make(phase=text, status=STATUS_READY)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def autofill_task_type(manifest: Mapping[str, Any]) -> str:
|
|
126
|
+
"""매니페스트의 포인터에서 바로 실행 가능한 task-type 을 뽑는다.
|
|
127
|
+
|
|
128
|
+
`ready` 가 아니면 빈 문자열이다. 셸 진입점의 autofill 이 이 함수를 쓴다.
|
|
129
|
+
"""
|
|
130
|
+
workflow = manifest.get("workflow")
|
|
131
|
+
if not isinstance(workflow, Mapping):
|
|
132
|
+
return ""
|
|
133
|
+
pointer = promote(workflow.get("nextRecommendedPhase"))
|
|
134
|
+
return pointer["phase"] if pointer["status"] == STATUS_READY else ""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def project(report_data: Mapping[str, Any]) -> dict[str, str]:
|
|
138
|
+
"""리포트 data.json 하나를 포인터로 투영한다.
|
|
139
|
+
|
|
140
|
+
라우팅 판단의 옳고 그름은 다루지 않는다. 리포트가 이미 내린 판단을
|
|
141
|
+
옮기기만 한다. 규칙표는 이 프로젝트의 구현 계획 문서에 있다.
|
|
142
|
+
"""
|
|
143
|
+
header = report_data.get("header")
|
|
144
|
+
task_type = ""
|
|
145
|
+
if isinstance(header, Mapping):
|
|
146
|
+
task_type = str(header.get("taskType") or "")
|
|
147
|
+
|
|
148
|
+
if task_type in _TERMINAL_TASK_TYPES:
|
|
149
|
+
return make(status=STATUS_TERMINAL)
|
|
150
|
+
if task_type in _SIDETRACK_TASK_TYPES:
|
|
151
|
+
return make(status=STATUS_PENDING)
|
|
152
|
+
|
|
153
|
+
if task_type == "requirements-discovery":
|
|
154
|
+
return _from_nested_routing(report_data, "requirementsDiscovery")
|
|
155
|
+
if task_type == "error-analysis":
|
|
156
|
+
return _from_nested_routing(report_data, "errorAnalysis")
|
|
157
|
+
if task_type == "implementation-option-selection":
|
|
158
|
+
return _from_option_selection(report_data)
|
|
159
|
+
if task_type == "implementation-planning":
|
|
160
|
+
return _from_planning(report_data)
|
|
161
|
+
if task_type == "implementation":
|
|
162
|
+
return _from_target(report_data, "implementation")
|
|
163
|
+
if task_type == "final-verification":
|
|
164
|
+
return _from_final_verification(report_data)
|
|
165
|
+
return make(status=STATUS_PENDING)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _block(report_data: Mapping[str, Any], key: str) -> Mapping[str, Any]:
|
|
169
|
+
block = report_data.get(key)
|
|
170
|
+
return block if isinstance(block, Mapping) else {}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _from_nested(
|
|
174
|
+
report_data: Mapping[str, Any],
|
|
175
|
+
block_key: str,
|
|
176
|
+
routing_key: str,
|
|
177
|
+
target_key: str,
|
|
178
|
+
) -> dict[str, str]:
|
|
179
|
+
"""블록 → 라우팅 → 대상 phase 로 두 단 내려가 읽는 공용 판독부.
|
|
180
|
+
|
|
181
|
+
대상이 문자열이 아니면 결측과 똑같이 취급한다. 문자열이 아닌 값을 str() 로
|
|
182
|
+
강제하면 dict repr 같은 것이 phase 이름 자리에 들어앉고, status 는 ready 가
|
|
183
|
+
되어 소비자에게 "지금 시작 가능한 phase" 로 읽힌다. 결측은 pending 에서
|
|
184
|
+
무해하게 멈추지만 이쪽은 존재하지 않는 phase 로 소비자를 보낸다.
|
|
185
|
+
"""
|
|
186
|
+
routing = _block(report_data, block_key).get(routing_key)
|
|
187
|
+
target = ""
|
|
188
|
+
if isinstance(routing, Mapping):
|
|
189
|
+
raw = routing.get(target_key)
|
|
190
|
+
target = raw.strip() if isinstance(raw, str) else ""
|
|
191
|
+
if not target:
|
|
192
|
+
return make(status=STATUS_PENDING)
|
|
193
|
+
return make(phase=target, status=STATUS_READY)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _from_nested_routing(report_data: Mapping[str, Any], key: str) -> dict[str, str]:
|
|
197
|
+
return _from_nested(report_data, key, "routing", "nextTaskType")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _from_option_selection(report_data: Mapping[str, Any]) -> dict[str, str]:
|
|
201
|
+
# 이 phase 의 routing 은 문자열 enum 이다. 이웃 두 phase 가 쓰는 중첩 dict 가
|
|
202
|
+
# 잘못 들어오면 결측과 똑같이 취급한다 — str() 로 강제하면 dict repr 이 phase
|
|
203
|
+
# 이름이 되어 ready 로 나간다.
|
|
204
|
+
raw = _block(report_data, "implementationOptionSelection").get("routing")
|
|
205
|
+
routing = raw.strip() if isinstance(raw, str) else ""
|
|
206
|
+
if routing in _OPTION_SELECTION_NON_PHASE:
|
|
207
|
+
return make(status=_OPTION_SELECTION_NON_PHASE[routing])
|
|
208
|
+
if not routing:
|
|
209
|
+
return make(status=STATUS_PENDING)
|
|
210
|
+
return make(phase=routing, status=STATUS_READY)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _from_planning(report_data: Mapping[str, Any]) -> dict[str, str]:
|
|
214
|
+
outcome = str(_block(report_data, "implementationPlanning").get("outcome") or "")
|
|
215
|
+
if outcome == "plan-ready":
|
|
216
|
+
return make(phase="implementation", status=STATUS_READY)
|
|
217
|
+
if outcome == "direction-invalidated":
|
|
218
|
+
return make(phase="implementation-option-selection", status=STATUS_READY)
|
|
219
|
+
return make(status=STATUS_PENDING)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _from_target(report_data: Mapping[str, Any], key: str) -> dict[str, str]:
|
|
223
|
+
return _from_nested(report_data, key, "routingRecommendation", "target")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _from_final_verification(report_data: Mapping[str, Any]) -> dict[str, str]:
|
|
227
|
+
pointer = _from_target(report_data, "finalVerification")
|
|
228
|
+
if pointer["phase"] == "done":
|
|
229
|
+
return make(status=STATUS_TERMINAL)
|
|
230
|
+
if pointer["phase"] in _FINAL_VERIFICATION_PHASE_ALIASES:
|
|
231
|
+
return make(
|
|
232
|
+
phase=_FINAL_VERIFICATION_PHASE_ALIASES[pointer["phase"]],
|
|
233
|
+
status=pointer["status"],
|
|
234
|
+
rationale=pointer["rationale"],
|
|
235
|
+
)
|
|
236
|
+
return pointer
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Lease pane titles from stored execution labels."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import secrets
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Protocol
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PaneTitleError(ValueError):
|
|
12
|
+
"""Raised when a title lease cannot be acquired or released."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PaneTitleBackend(Protocol):
|
|
16
|
+
def get_title(self, pane_id: str) -> str: ...
|
|
17
|
+
def set_title(self, pane_id: str, title: str) -> None: ...
|
|
18
|
+
def available(self) -> bool: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class MemoryPaneBackend:
|
|
23
|
+
titles: dict[str, str]
|
|
24
|
+
capable: bool = True
|
|
25
|
+
|
|
26
|
+
def get_title(self, pane_id: str) -> str:
|
|
27
|
+
return self.titles.get(pane_id, "")
|
|
28
|
+
|
|
29
|
+
def set_title(self, pane_id: str, title: str) -> None:
|
|
30
|
+
self.titles[pane_id] = title
|
|
31
|
+
|
|
32
|
+
def available(self) -> bool:
|
|
33
|
+
return self.capable
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class PaneTitleLease:
|
|
38
|
+
run_ref: str
|
|
39
|
+
participant_ref: str
|
|
40
|
+
role_execution_ref: str
|
|
41
|
+
invocation_ref: str | None
|
|
42
|
+
pane_backend: str
|
|
43
|
+
pane_id: str
|
|
44
|
+
previous_title: str
|
|
45
|
+
applied_title: str
|
|
46
|
+
owner_token: str
|
|
47
|
+
status: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class UnavailablePaneBackend:
|
|
51
|
+
def get_title(self, pane_id: str) -> str:
|
|
52
|
+
return ""
|
|
53
|
+
|
|
54
|
+
def set_title(self, pane_id: str, title: str) -> None:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def available(self) -> bool:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def acquire_pane_title(
|
|
62
|
+
*,
|
|
63
|
+
backend: PaneTitleBackend,
|
|
64
|
+
pane_id: str,
|
|
65
|
+
execution_label: str,
|
|
66
|
+
run_ref: str,
|
|
67
|
+
participant_ref: str,
|
|
68
|
+
role_execution_ref: str,
|
|
69
|
+
invocation_ref: str | None = None,
|
|
70
|
+
pane_backend: str = "cmux",
|
|
71
|
+
) -> PaneTitleLease:
|
|
72
|
+
if not backend.available():
|
|
73
|
+
return PaneTitleLease(
|
|
74
|
+
run_ref=run_ref,
|
|
75
|
+
participant_ref=participant_ref,
|
|
76
|
+
role_execution_ref=role_execution_ref,
|
|
77
|
+
invocation_ref=invocation_ref,
|
|
78
|
+
pane_backend=pane_backend,
|
|
79
|
+
pane_id=pane_id,
|
|
80
|
+
previous_title="",
|
|
81
|
+
applied_title=execution_label,
|
|
82
|
+
owner_token="",
|
|
83
|
+
status="not-applicable",
|
|
84
|
+
)
|
|
85
|
+
previous = backend.get_title(pane_id)
|
|
86
|
+
backend.set_title(pane_id, execution_label)
|
|
87
|
+
return PaneTitleLease(
|
|
88
|
+
run_ref=run_ref,
|
|
89
|
+
participant_ref=participant_ref,
|
|
90
|
+
role_execution_ref=role_execution_ref,
|
|
91
|
+
invocation_ref=invocation_ref,
|
|
92
|
+
pane_backend=pane_backend,
|
|
93
|
+
pane_id=pane_id,
|
|
94
|
+
previous_title=previous,
|
|
95
|
+
applied_title=execution_label,
|
|
96
|
+
owner_token=secrets.token_hex(8),
|
|
97
|
+
status="acquired",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def release_pane_title(
|
|
102
|
+
lease: PaneTitleLease,
|
|
103
|
+
backend: PaneTitleBackend,
|
|
104
|
+
*,
|
|
105
|
+
owner_token: str,
|
|
106
|
+
) -> PaneTitleLease:
|
|
107
|
+
if lease.status == "not-applicable":
|
|
108
|
+
return lease
|
|
109
|
+
if owner_token != lease.owner_token:
|
|
110
|
+
raise PaneTitleError("owner token does not match")
|
|
111
|
+
current = backend.get_title(lease.pane_id)
|
|
112
|
+
if current != lease.applied_title:
|
|
113
|
+
return PaneTitleLease(**{**lease.__dict__, "status": "manual-change-preserved"})
|
|
114
|
+
backend.set_title(lease.pane_id, lease.previous_title)
|
|
115
|
+
return PaneTitleLease(**{**lease.__dict__, "status": "released"})
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cleanup_pane_title(
|
|
119
|
+
lease: PaneTitleLease, backend: PaneTitleBackend
|
|
120
|
+
) -> PaneTitleLease:
|
|
121
|
+
if lease.status == "not-applicable":
|
|
122
|
+
return lease
|
|
123
|
+
backend.set_title(lease.pane_id, lease.previous_title)
|
|
124
|
+
return PaneTitleLease(**{**lease.__dict__, "status": "cleaned"})
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main(argv: list[str] | None = None) -> int:
|
|
128
|
+
import argparse
|
|
129
|
+
|
|
130
|
+
parser = argparse.ArgumentParser(prog="okstra pane-title")
|
|
131
|
+
parser.add_argument("action", choices=("acquire", "release", "cleanup"))
|
|
132
|
+
parser.add_argument("--execution-label", default="")
|
|
133
|
+
parser.add_argument("--pane-id", default="")
|
|
134
|
+
parser.add_argument("--json", action="store_true")
|
|
135
|
+
args = parser.parse_args(argv)
|
|
136
|
+
backend = UnavailablePaneBackend()
|
|
137
|
+
lease = acquire_pane_title(
|
|
138
|
+
backend=backend,
|
|
139
|
+
pane_id=args.pane_id or "none",
|
|
140
|
+
execution_label=args.execution_label or "unused",
|
|
141
|
+
run_ref="run",
|
|
142
|
+
participant_ref="participant",
|
|
143
|
+
role_execution_ref="role",
|
|
144
|
+
)
|
|
145
|
+
payload = {"status": lease.status, "appliedTitle": lease.applied_title}
|
|
146
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
raise SystemExit(main())
|