okstra 0.174.0 → 0.175.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -9
- package/bin/okstra +8 -4
- package/dist/cli-registry.d.mts +16 -0
- package/dist/cli-registry.mjs +560 -0
- package/dist/cli-registry.mjs.map +1 -0
- package/dist/commands/execute/agent-prompt.d.mts +1 -0
- package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
- package/dist/commands/execute/agent-prompt.mjs.map +1 -0
- package/dist/commands/execute/codex-dispatch.d.mts +3 -0
- package/dist/commands/execute/codex-dispatch.mjs +6 -0
- package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
- package/dist/commands/execute/codex-run.d.mts +3 -0
- package/{src → dist}/commands/execute/codex-run.mjs +30 -37
- package/dist/commands/execute/codex-run.mjs.map +1 -0
- package/dist/commands/execute/convergence.d.mts +1 -0
- package/{src → dist}/commands/execute/convergence.mjs +15 -16
- package/dist/commands/execute/convergence.mjs.map +1 -0
- package/dist/commands/execute/error-log.d.mts +1 -0
- package/{src → dist}/commands/execute/error-log.mjs +2 -3
- package/dist/commands/execute/error-log.mjs.map +1 -0
- package/dist/commands/execute/git-reconcile.d.mts +1 -0
- package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
- package/dist/commands/execute/git-reconcile.mjs.map +1 -0
- package/dist/commands/execute/handoff.d.mts +1 -0
- package/{src → dist}/commands/execute/handoff.mjs +10 -11
- package/dist/commands/execute/handoff.mjs.map +1 -0
- package/dist/commands/execute/incremental-carry.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
- package/dist/commands/execute/incremental-carry.mjs.map +1 -0
- package/dist/commands/execute/incremental-scope.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
- package/dist/commands/execute/incremental-scope.mjs.map +1 -0
- package/dist/commands/execute/integrate-stages.d.mts +1 -0
- package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
- package/dist/commands/execute/integrate-stages.mjs.map +1 -0
- package/dist/commands/execute/pane-title.d.mts +1 -0
- package/dist/commands/execute/pane-title.mjs +20 -0
- package/dist/commands/execute/pane-title.mjs.map +1 -0
- package/dist/commands/execute/plan-items.d.mts +1 -0
- package/dist/commands/execute/plan-items.mjs +9 -0
- package/dist/commands/execute/plan-items.mjs.map +1 -0
- package/dist/commands/execute/plan-validate.d.mts +1 -0
- package/dist/commands/execute/plan-validate.mjs +68 -0
- package/dist/commands/execute/plan-validate.mjs.map +1 -0
- package/dist/commands/execute/plan-verify.d.mts +1 -0
- package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
- package/dist/commands/execute/plan-verify.mjs.map +1 -0
- package/dist/commands/execute/render-bundle.d.mts +3 -0
- package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
- package/dist/commands/execute/render-bundle.mjs.map +1 -0
- package/dist/commands/execute/run.d.mts +24 -0
- package/dist/commands/execute/run.mjs +196 -0
- package/dist/commands/execute/run.mjs.map +1 -0
- package/dist/commands/execute/spawn-followups.d.mts +1 -0
- package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
- package/dist/commands/execute/spawn-followups.mjs.map +1 -0
- package/dist/commands/execute/team.d.mts +3 -0
- package/{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 +4 -0
- package/docs/architecture.md +7 -1
- package/docs/cli.md +26 -6
- package/docs/contributor-change-matrix.md +2 -2
- package/docs/project-structure-overview.md +16 -7
- package/docs/task-process/README.md +2 -0
- package/docs/task-process/implementation-planning.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/package.json +11 -5
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +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/usage.sh +4 -4
- package/runtime/bin/okstra-error-log.py +27 -0
- package/runtime/bin/okstra-provider-exec.py +217 -5
- package/runtime/bin/okstra.sh +10 -0
- package/runtime/prompts/lead/convergence.md +27 -18
- package/runtime/prompts/lead/okstra-lead-contract.md +3 -1
- package/runtime/prompts/lead/plan-body-verification.md +10 -3
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/change-impact-analysis.md +16 -0
- package/runtime/prompts/profiles/error-analysis.md +20 -0
- package/runtime/prompts/profiles/feature-analysis.md +16 -0
- package/runtime/prompts/profiles/final-verification.md +16 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +16 -0
- package/runtime/prompts/profiles/implementation-planning.md +20 -0
- package/runtime/prompts/profiles/implementation.md +15 -0
- package/runtime/prompts/profiles/improvement-discovery.md +16 -0
- package/runtime/prompts/profiles/project-analysis.md +16 -0
- package/runtime/prompts/profiles/release-handoff.md +4 -0
- package/runtime/prompts/profiles/requirements-discovery.md +20 -0
- package/runtime/prompts/wizard/prompts.ko.json +18 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/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/agent_activity.py +5 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
- package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
- package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
- package/runtime/python/okstra_ctl/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 +1092 -63
- 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 +32 -1
- package/runtime/python/okstra_ctl/domain/provider.py +212 -6
- package/runtime/python/okstra_ctl/domain/role.py +125 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +13 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +34 -1
- package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
- package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
- package/runtime/python/okstra_ctl/error_log_write.py +9 -0
- package/runtime/python/okstra_ctl/error_report.py +21 -1
- package/runtime/python/okstra_ctl/execution_identity.py +529 -0
- package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
- package/runtime/python/okstra_ctl/index.py +6 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
- package/runtime/python/okstra_ctl/legacy_model_selection.py +490 -0
- package/runtime/python/okstra_ctl/model_cli.py +278 -0
- package/runtime/python/okstra_ctl/model_defaults.py +46 -0
- package/runtime/python/okstra_ctl/model_discovery.py +26 -7
- package/runtime/python/okstra_ctl/model_pool.py +501 -0
- package/runtime/python/okstra_ctl/models.py +47 -27
- package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
- package/runtime/python/okstra_ctl/pane_title.py +151 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
- package/runtime/python/okstra_ctl/render.py +78 -10
- package/runtime/python/okstra_ctl/render_final_report.py +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 +217 -0
- package/runtime/python/okstra_ctl/run.py +1207 -258
- package/runtime/python/okstra_ctl/run_index_row.py +7 -1
- 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 +1253 -62
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
- 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/write_policy.py +571 -0
- package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
- package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
- package/runtime/schemas/final-report-v2.0.schema.json +25 -0
- package/runtime/skills/okstra-run/SKILL.md +9 -9
- package/runtime/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/worker-prompt-preamble.md +6 -2
- package/runtime/validators/lib/fixtures.sh +49 -16
- package/runtime/validators/validate-run.py +429 -35
- 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,571 @@
|
|
|
1
|
+
"""Canonical invocation write policy and truthful runner enforcement."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path, PurePosixPath
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
BoundaryPrecision = Literal["exact-path", "directory-boundary", "none"]
|
|
13
|
+
|
|
14
|
+
_PRECISION_RANK = {"none": 0, "directory-boundary": 1, "exact-path": 2}
|
|
15
|
+
_POLICY_KEYS = {
|
|
16
|
+
"artifactPolicy",
|
|
17
|
+
"sourcePolicy",
|
|
18
|
+
"gitPolicy",
|
|
19
|
+
"auxiliaryPolicy",
|
|
20
|
+
}
|
|
21
|
+
_ENFORCEMENT_KEYS = {
|
|
22
|
+
"boundaryPrecision",
|
|
23
|
+
"mutationAudit",
|
|
24
|
+
"attribution",
|
|
25
|
+
"auditedRoots",
|
|
26
|
+
"observationCoverage",
|
|
27
|
+
"unobservedWriteSurfaces",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WritePolicyError(ValueError):
|
|
32
|
+
"""Raised when a write policy cannot be made canonical or trustworthy."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class WritePolicy:
|
|
37
|
+
artifact_policy: Mapping[str, Any]
|
|
38
|
+
source_policy: Mapping[str, Any]
|
|
39
|
+
git_policy: Mapping[str, Any]
|
|
40
|
+
auxiliary_policy: Mapping[str, Any]
|
|
41
|
+
maximum_boundary_precision: BoundaryPrecision = "none"
|
|
42
|
+
|
|
43
|
+
def to_payload(self) -> dict[str, Any]:
|
|
44
|
+
return {
|
|
45
|
+
"artifactPolicy": dict(self.artifact_policy),
|
|
46
|
+
"sourcePolicy": dict(self.source_policy),
|
|
47
|
+
"gitPolicy": dict(self.git_policy),
|
|
48
|
+
"auxiliaryPolicy": dict(self.auxiliary_policy),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def digest(self) -> str:
|
|
53
|
+
return write_policy_digest(self.to_payload())
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def source_mode(self) -> str:
|
|
57
|
+
return str(self.source_policy["mode"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class WriteEnforcement:
|
|
62
|
+
boundary_precision: BoundaryPrecision
|
|
63
|
+
mutation_audit: Literal["none", "batch"]
|
|
64
|
+
attribution: Literal["call", "batch-unattributed"]
|
|
65
|
+
audited_roots: tuple[Path, ...]
|
|
66
|
+
observation_coverage: Literal["complete", "scoped", "partial"]
|
|
67
|
+
unobserved_write_surfaces: tuple[str, ...]
|
|
68
|
+
|
|
69
|
+
def to_payload(self) -> dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"boundaryPrecision": self.boundary_precision,
|
|
72
|
+
"mutationAudit": self.mutation_audit,
|
|
73
|
+
"attribution": self.attribution,
|
|
74
|
+
"auditedRoots": [str(path) for path in self.audited_roots],
|
|
75
|
+
"observationCoverage": self.observation_coverage,
|
|
76
|
+
"unobservedWriteSurfaces": list(self.unobserved_write_surfaces),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_write_policy(
|
|
81
|
+
invocation: Mapping[str, Any],
|
|
82
|
+
assignment: Mapping[str, Any],
|
|
83
|
+
worktree: Path | None,
|
|
84
|
+
) -> WritePolicy:
|
|
85
|
+
"""Build one provider-neutral policy from invocation and adapter authority."""
|
|
86
|
+
role = _required_text(invocation, "role")
|
|
87
|
+
project_root = Path(_required_text(invocation, "projectRoot")).resolve()
|
|
88
|
+
source_root = _trusted_root(invocation, worktree)
|
|
89
|
+
artifact_paths = _relative_paths(invocation.get("artifactPaths", ()))
|
|
90
|
+
if not artifact_paths:
|
|
91
|
+
raise WritePolicyError("artifactPaths must contain standard worker artifacts")
|
|
92
|
+
source_mode = "project-mutation" if role == "implementer" else "source-readonly"
|
|
93
|
+
source_policy: dict[str, Any] = {
|
|
94
|
+
"mode": source_mode,
|
|
95
|
+
"allowedRoot": str(source_root),
|
|
96
|
+
"plannedPaths": list(_relative_paths(invocation.get("plannedPaths", ()))),
|
|
97
|
+
"protectedPaths": list(
|
|
98
|
+
_relative_paths(invocation.get("protectedPaths", (".okstra", ".git")))
|
|
99
|
+
),
|
|
100
|
+
}
|
|
101
|
+
git_policy = _git_policy(invocation, source_root, source_mode)
|
|
102
|
+
external_roots = _validated_auxiliary_roots(invocation, assignment)
|
|
103
|
+
auxiliary_policy = {
|
|
104
|
+
"scratchRoots": [
|
|
105
|
+
str(path) for path in _absolute_roots(invocation.get("scratchRoots", ()))
|
|
106
|
+
],
|
|
107
|
+
"generatedPaths": list(
|
|
108
|
+
_relative_paths(invocation.get("generatedPaths", ()))
|
|
109
|
+
),
|
|
110
|
+
"externalRoots": [str(path) for path in external_roots],
|
|
111
|
+
}
|
|
112
|
+
policy = WritePolicy(
|
|
113
|
+
artifact_policy={
|
|
114
|
+
"allowedRoot": str(project_root),
|
|
115
|
+
"allowedPaths": list(artifact_paths),
|
|
116
|
+
},
|
|
117
|
+
source_policy=source_policy,
|
|
118
|
+
git_policy=git_policy,
|
|
119
|
+
auxiliary_policy=auxiliary_policy,
|
|
120
|
+
maximum_boundary_precision=_assignment_precision(assignment),
|
|
121
|
+
)
|
|
122
|
+
validate_write_policy_payload(policy.to_payload())
|
|
123
|
+
return policy
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_invocation_write_contract(
|
|
127
|
+
*,
|
|
128
|
+
role: str,
|
|
129
|
+
project_root: Path,
|
|
130
|
+
worktree: Path | None,
|
|
131
|
+
artifact_paths: Sequence[Path],
|
|
132
|
+
maximum_precision: BoundaryPrecision,
|
|
133
|
+
planned_paths: Sequence[str] = (),
|
|
134
|
+
scratch_roots: Sequence[Path] = (),
|
|
135
|
+
generated_paths: Sequence[str] = (),
|
|
136
|
+
auxiliary_roots: Sequence[Path] = (),
|
|
137
|
+
validated_auxiliary_roots: Sequence[Path] = (),
|
|
138
|
+
) -> tuple[WritePolicy, WriteEnforcement]:
|
|
139
|
+
"""Build the single canonical contract used by every v2 dispatcher."""
|
|
140
|
+
root = project_root.resolve()
|
|
141
|
+
relative_artifacts = tuple(
|
|
142
|
+
_relative_to_root(path, root, "artifact") for path in artifact_paths
|
|
143
|
+
)
|
|
144
|
+
invocation: dict[str, Any] = {
|
|
145
|
+
"role": role,
|
|
146
|
+
"projectRoot": str(root),
|
|
147
|
+
"artifactPaths": list(relative_artifacts),
|
|
148
|
+
"plannedPaths": list(planned_paths),
|
|
149
|
+
"protectedPaths": [".okstra", ".git"],
|
|
150
|
+
"generatedPaths": list(generated_paths),
|
|
151
|
+
"scratchRoots": [str(path) for path in scratch_roots],
|
|
152
|
+
"auxiliaryRoots": [str(path) for path in auxiliary_roots],
|
|
153
|
+
}
|
|
154
|
+
source_root = worktree.resolve() if worktree is not None else root
|
|
155
|
+
if role == "implementer":
|
|
156
|
+
invocation.update({
|
|
157
|
+
"expectedBaseCommit": _git_output(source_root, "rev-parse", "HEAD"),
|
|
158
|
+
"allowedBranchRef": _git_output(
|
|
159
|
+
source_root, "symbolic-ref", "-q", "HEAD"
|
|
160
|
+
),
|
|
161
|
+
})
|
|
162
|
+
assignment = {
|
|
163
|
+
"workerWriteCapability": {
|
|
164
|
+
"maxBoundaryPrecision": maximum_precision,
|
|
165
|
+
},
|
|
166
|
+
"validatedAuxiliaryRoots": [str(path) for path in validated_auxiliary_roots],
|
|
167
|
+
}
|
|
168
|
+
policy = build_write_policy(invocation, assignment, source_root)
|
|
169
|
+
return policy, derive_write_enforcement(policy, maximum_precision, role=role)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def planned_paths_from_run_manifest(
|
|
173
|
+
project_root: Path, manifest: Mapping[str, Any]
|
|
174
|
+
) -> tuple[str, ...]:
|
|
175
|
+
active_value = manifest.get("activeRunContextPath")
|
|
176
|
+
if not isinstance(active_value, str) or not active_value:
|
|
177
|
+
raise WritePolicyError("implementer write policy has no active run context")
|
|
178
|
+
active = _read_json(_rooted(project_root, active_value), "active run context")
|
|
179
|
+
source = active.get("sourceArtifacts")
|
|
180
|
+
run_inputs_value = source.get("runInputsPath") if isinstance(source, Mapping) else None
|
|
181
|
+
run = active.get("run")
|
|
182
|
+
stage_value = run.get("stage") if isinstance(run, Mapping) else None
|
|
183
|
+
if not isinstance(run_inputs_value, str) or not str(stage_value).isdigit():
|
|
184
|
+
raise WritePolicyError("implementer write policy has no approved stage authority")
|
|
185
|
+
inputs_payload = _read_json(_rooted(project_root, run_inputs_value), "run inputs")
|
|
186
|
+
inputs = inputs_payload.get("inputs")
|
|
187
|
+
approved = inputs.get("approvedPlanPath") if isinstance(inputs, Mapping) else None
|
|
188
|
+
if not isinstance(approved, str) or not approved:
|
|
189
|
+
raise WritePolicyError("implementer write policy has no approved plan path")
|
|
190
|
+
return _planned_paths_from_report(Path(approved), int(stage_value))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def derive_write_enforcement(
|
|
194
|
+
policy: WritePolicy,
|
|
195
|
+
maximum_precision: BoundaryPrecision,
|
|
196
|
+
*,
|
|
197
|
+
role: str | None = None,
|
|
198
|
+
) -> WriteEnforcement:
|
|
199
|
+
"""Lower runner capability when policy paths cannot be enforced exactly."""
|
|
200
|
+
if role == "leader":
|
|
201
|
+
enforcement = WriteEnforcement(
|
|
202
|
+
boundary_precision="none",
|
|
203
|
+
mutation_audit="none",
|
|
204
|
+
attribution="batch-unattributed",
|
|
205
|
+
audited_roots=_audited_roots(policy),
|
|
206
|
+
observation_coverage="partial",
|
|
207
|
+
unobserved_write_surfaces=(
|
|
208
|
+
"lead-session-host-writes",
|
|
209
|
+
"other-stage-refs",
|
|
210
|
+
"shared-git-object-store",
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
validate_write_enforcement_payload(enforcement.to_payload())
|
|
214
|
+
return enforcement
|
|
215
|
+
if maximum_precision not in _PRECISION_RANK:
|
|
216
|
+
raise WritePolicyError("worker write capability is invalid")
|
|
217
|
+
exact_policy = (
|
|
218
|
+
policy.source_mode == "source-readonly"
|
|
219
|
+
and not policy.auxiliary_policy.get("externalRoots")
|
|
220
|
+
and not policy.auxiliary_policy.get("scratchRoots")
|
|
221
|
+
and not policy.auxiliary_policy.get("generatedPaths")
|
|
222
|
+
)
|
|
223
|
+
if maximum_precision == "exact-path" and exact_policy:
|
|
224
|
+
boundary: BoundaryPrecision = "exact-path"
|
|
225
|
+
elif _PRECISION_RANK[maximum_precision] >= _PRECISION_RANK["directory-boundary"]:
|
|
226
|
+
boundary = "directory-boundary"
|
|
227
|
+
else:
|
|
228
|
+
boundary = "none"
|
|
229
|
+
exact = boundary == "exact-path" and exact_policy
|
|
230
|
+
external_roots = tuple(policy.auxiliary_policy.get("externalRoots", ()))
|
|
231
|
+
unobserved: list[str] = []
|
|
232
|
+
if boundary == "none":
|
|
233
|
+
unobserved.append("provider-process-outside-audited-roots")
|
|
234
|
+
if external_roots:
|
|
235
|
+
unobserved.append("shared-tool-cache")
|
|
236
|
+
if policy.source_mode == "project-mutation":
|
|
237
|
+
unobserved.extend(("other-stage-refs", "shared-git-object-store"))
|
|
238
|
+
coverage: Literal["complete", "scoped", "partial"]
|
|
239
|
+
if exact:
|
|
240
|
+
coverage = "complete"
|
|
241
|
+
elif unobserved:
|
|
242
|
+
coverage = "partial"
|
|
243
|
+
else:
|
|
244
|
+
coverage = "scoped"
|
|
245
|
+
enforcement = WriteEnforcement(
|
|
246
|
+
boundary_precision=boundary,
|
|
247
|
+
mutation_audit="none" if exact else "batch",
|
|
248
|
+
attribution="call" if exact else "batch-unattributed",
|
|
249
|
+
audited_roots=_audited_roots(policy),
|
|
250
|
+
observation_coverage=coverage,
|
|
251
|
+
unobserved_write_surfaces=tuple(unobserved),
|
|
252
|
+
)
|
|
253
|
+
validate_write_enforcement_payload(enforcement.to_payload())
|
|
254
|
+
return enforcement
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def write_policy_from_payload(payload: Mapping[str, Any]) -> WritePolicy:
|
|
258
|
+
validate_write_policy_payload(payload)
|
|
259
|
+
return WritePolicy(
|
|
260
|
+
artifact_policy=dict(payload["artifactPolicy"]),
|
|
261
|
+
source_policy=dict(payload["sourcePolicy"]),
|
|
262
|
+
git_policy=dict(payload["gitPolicy"]),
|
|
263
|
+
auxiliary_policy=dict(payload["auxiliaryPolicy"]),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def write_enforcement_from_payload(
|
|
268
|
+
payload: Mapping[str, Any],
|
|
269
|
+
) -> WriteEnforcement:
|
|
270
|
+
validate_write_enforcement_payload(payload)
|
|
271
|
+
return WriteEnforcement(
|
|
272
|
+
boundary_precision=payload["boundaryPrecision"],
|
|
273
|
+
mutation_audit=payload["mutationAudit"],
|
|
274
|
+
attribution=payload["attribution"],
|
|
275
|
+
audited_roots=tuple(Path(value) for value in payload["auditedRoots"]),
|
|
276
|
+
observation_coverage=payload["observationCoverage"],
|
|
277
|
+
unobserved_write_surfaces=tuple(payload["unobservedWriteSurfaces"]),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def write_policy_digest(payload: Mapping[str, Any]) -> str:
|
|
282
|
+
validate_write_policy_payload(payload)
|
|
283
|
+
encoded = json.dumps(
|
|
284
|
+
payload,
|
|
285
|
+
sort_keys=True,
|
|
286
|
+
separators=(",", ":"),
|
|
287
|
+
ensure_ascii=False,
|
|
288
|
+
).encode("utf-8")
|
|
289
|
+
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def validate_write_policy_payload(payload: Mapping[str, Any]) -> None:
|
|
293
|
+
if set(payload) != _POLICY_KEYS:
|
|
294
|
+
raise WritePolicyError("writePolicy must contain four canonical policy sections")
|
|
295
|
+
artifact = _mapping(payload.get("artifactPolicy"), "artifactPolicy")
|
|
296
|
+
source = _mapping(payload.get("sourcePolicy"), "sourcePolicy")
|
|
297
|
+
git = _mapping(payload.get("gitPolicy"), "gitPolicy")
|
|
298
|
+
auxiliary = _mapping(payload.get("auxiliaryPolicy"), "auxiliaryPolicy")
|
|
299
|
+
if set(artifact) != {"allowedRoot", "allowedPaths"}:
|
|
300
|
+
raise WritePolicyError("artifactPolicy is incomplete")
|
|
301
|
+
_absolute_text(artifact.get("allowedRoot"), "artifactPolicy.allowedRoot")
|
|
302
|
+
_relative_paths(artifact.get("allowedPaths", ()))
|
|
303
|
+
if source.get("mode") not in {"source-readonly", "project-mutation"}:
|
|
304
|
+
raise WritePolicyError("sourcePolicy mode is invalid")
|
|
305
|
+
if set(source) != {"mode", "allowedRoot", "plannedPaths", "protectedPaths"}:
|
|
306
|
+
raise WritePolicyError("sourcePolicy is incomplete")
|
|
307
|
+
_absolute_text(source.get("allowedRoot"), "sourcePolicy.allowedRoot")
|
|
308
|
+
_relative_paths(source.get("plannedPaths", ()))
|
|
309
|
+
_relative_paths(source.get("protectedPaths", ()))
|
|
310
|
+
if git.get("mode") not in {"disabled", "fast-forward-descendant-chain"}:
|
|
311
|
+
raise WritePolicyError("gitPolicy mode is invalid")
|
|
312
|
+
if git.get("mode") == "disabled" and set(git) != {"mode"}:
|
|
313
|
+
raise WritePolicyError("disabled gitPolicy must not grant Git paths")
|
|
314
|
+
required_git = {
|
|
315
|
+
"mode", "worktreeGitDir", "gitCommonDir", "expectedBaseCommit",
|
|
316
|
+
"allowedBranchRef", "commitMode",
|
|
317
|
+
}
|
|
318
|
+
if git.get("mode") != "disabled" and set(git) != required_git:
|
|
319
|
+
raise WritePolicyError("project mutation gitPolicy is incomplete")
|
|
320
|
+
if set(auxiliary) != {"scratchRoots", "generatedPaths", "externalRoots"}:
|
|
321
|
+
raise WritePolicyError("auxiliaryPolicy is incomplete")
|
|
322
|
+
_relative_paths(auxiliary.get("generatedPaths", ()))
|
|
323
|
+
for key in ("scratchRoots", "externalRoots"):
|
|
324
|
+
for value in _text_sequence(auxiliary.get(key), f"auxiliaryPolicy.{key}"):
|
|
325
|
+
_absolute_text(value, f"auxiliaryPolicy.{key}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def validate_write_enforcement_payload(payload: Mapping[str, Any]) -> None:
|
|
329
|
+
if set(payload) != _ENFORCEMENT_KEYS:
|
|
330
|
+
raise WritePolicyError("writeEnforcement is incomplete")
|
|
331
|
+
boundary = payload.get("boundaryPrecision")
|
|
332
|
+
mutation_audit = payload.get("mutationAudit")
|
|
333
|
+
attribution = payload.get("attribution")
|
|
334
|
+
coverage = payload.get("observationCoverage")
|
|
335
|
+
if boundary not in _PRECISION_RANK:
|
|
336
|
+
raise WritePolicyError("writeEnforcement boundaryPrecision is invalid")
|
|
337
|
+
if mutation_audit not in {"none", "batch"}:
|
|
338
|
+
raise WritePolicyError("writeEnforcement mutationAudit is invalid")
|
|
339
|
+
if attribution not in {"call", "batch-unattributed"}:
|
|
340
|
+
raise WritePolicyError("writeEnforcement attribution is invalid")
|
|
341
|
+
if coverage not in {"complete", "scoped", "partial"}:
|
|
342
|
+
raise WritePolicyError("writeEnforcement observationCoverage is invalid")
|
|
343
|
+
if (boundary, mutation_audit, attribution) == ("exact-path", "none", "call"):
|
|
344
|
+
if coverage != "complete":
|
|
345
|
+
raise WritePolicyError("exact enforcement must have complete coverage")
|
|
346
|
+
elif (boundary, mutation_audit, attribution) == (
|
|
347
|
+
"none",
|
|
348
|
+
"none",
|
|
349
|
+
"batch-unattributed",
|
|
350
|
+
):
|
|
351
|
+
if coverage != "partial":
|
|
352
|
+
raise WritePolicyError("host-session enforcement must be partial")
|
|
353
|
+
elif mutation_audit != "batch" or attribution != "batch-unattributed":
|
|
354
|
+
raise WritePolicyError("non-exact enforcement must use batch attribution")
|
|
355
|
+
elif boundary == "exact-path" or coverage == "complete":
|
|
356
|
+
raise WritePolicyError("batch enforcement cannot claim exact coverage")
|
|
357
|
+
for root in _text_sequence(payload.get("auditedRoots"), "auditedRoots"):
|
|
358
|
+
_absolute_text(root, "auditedRoots")
|
|
359
|
+
_text_sequence(payload.get("unobservedWriteSurfaces"), "unobservedWriteSurfaces")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _git_policy(
|
|
363
|
+
invocation: Mapping[str, Any], root: Path, source_mode: str
|
|
364
|
+
) -> dict[str, Any]:
|
|
365
|
+
if source_mode == "source-readonly":
|
|
366
|
+
return {"mode": "disabled"}
|
|
367
|
+
git_dir = _git_path(root, "--git-dir")
|
|
368
|
+
common_dir = _git_path(root, "--git-common-dir")
|
|
369
|
+
expected_base = _required_text(invocation, "expectedBaseCommit")
|
|
370
|
+
allowed_ref = _required_text(invocation, "allowedBranchRef")
|
|
371
|
+
_require_commit(root, expected_base)
|
|
372
|
+
current_ref = _git_output(root, "symbolic-ref", "-q", "HEAD")
|
|
373
|
+
if current_ref != allowed_ref or not allowed_ref.startswith("refs/heads/"):
|
|
374
|
+
raise WritePolicyError("allowedBranchRef does not match assigned worktree")
|
|
375
|
+
return {
|
|
376
|
+
"mode": "fast-forward-descendant-chain",
|
|
377
|
+
"worktreeGitDir": str(git_dir),
|
|
378
|
+
"gitCommonDir": str(common_dir),
|
|
379
|
+
"expectedBaseCommit": expected_base,
|
|
380
|
+
"allowedBranchRef": allowed_ref,
|
|
381
|
+
"commitMode": "fast-forward-descendant-chain",
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _audited_roots(policy: WritePolicy) -> tuple[Path, ...]:
|
|
386
|
+
values = [
|
|
387
|
+
Path(str(policy.source_policy["allowedRoot"])),
|
|
388
|
+
Path(str(policy.artifact_policy["allowedRoot"])),
|
|
389
|
+
]
|
|
390
|
+
values.extend(Path(value) for value in policy.auxiliary_policy.get("scratchRoots", ()))
|
|
391
|
+
git_dir = policy.git_policy.get("worktreeGitDir")
|
|
392
|
+
if isinstance(git_dir, str) and git_dir:
|
|
393
|
+
values.append(Path(git_dir))
|
|
394
|
+
seen: set[Path] = set()
|
|
395
|
+
roots: list[Path] = []
|
|
396
|
+
for value in values:
|
|
397
|
+
resolved = value.resolve()
|
|
398
|
+
if resolved not in seen:
|
|
399
|
+
roots.append(resolved)
|
|
400
|
+
seen.add(resolved)
|
|
401
|
+
return tuple(roots)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _trusted_root(invocation: Mapping[str, Any], worktree: Path | None) -> Path:
|
|
405
|
+
raw = worktree if worktree is not None else Path(_required_text(invocation, "projectRoot"))
|
|
406
|
+
if not raw.is_absolute() or not raw.is_dir():
|
|
407
|
+
raise WritePolicyError("assigned worktree must be an existing absolute path")
|
|
408
|
+
return raw.resolve()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _validated_auxiliary_roots(
|
|
412
|
+
invocation: Mapping[str, Any], assignment: Mapping[str, Any]
|
|
413
|
+
) -> tuple[Path, ...]:
|
|
414
|
+
requested = _absolute_roots(invocation.get("auxiliaryRoots", ()))
|
|
415
|
+
declared = set(_absolute_roots(assignment.get("validatedAuxiliaryRoots", ())))
|
|
416
|
+
if any(path not in declared for path in requested):
|
|
417
|
+
raise WritePolicyError("auxiliary root is not adapter-validated")
|
|
418
|
+
return requested
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _assignment_precision(assignment: Mapping[str, Any]) -> BoundaryPrecision:
|
|
422
|
+
capability = assignment.get("workerWriteCapability")
|
|
423
|
+
if not isinstance(capability, Mapping):
|
|
424
|
+
raise WritePolicyError("workerWriteCapability is required")
|
|
425
|
+
value = capability.get("maxBoundaryPrecision")
|
|
426
|
+
if value not in _PRECISION_RANK:
|
|
427
|
+
raise WritePolicyError("workerWriteCapability is invalid")
|
|
428
|
+
return value
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _absolute_roots(value: object) -> tuple[Path, ...]:
|
|
432
|
+
roots: list[Path] = []
|
|
433
|
+
for text in _text_sequence(value, "absolute roots"):
|
|
434
|
+
path = Path(text)
|
|
435
|
+
if not path.is_absolute() or not path.exists():
|
|
436
|
+
raise WritePolicyError("auxiliary root must be an existing absolute path")
|
|
437
|
+
if path.absolute() != path.resolve():
|
|
438
|
+
raise WritePolicyError("auxiliary root contains a symlink component")
|
|
439
|
+
roots.append(path.resolve())
|
|
440
|
+
if len(set(roots)) != len(roots):
|
|
441
|
+
raise WritePolicyError("auxiliary roots must be unique")
|
|
442
|
+
return tuple(roots)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _relative_to_root(path: Path, root: Path, label: str) -> str:
|
|
446
|
+
absolute = path if path.is_absolute() else root / path
|
|
447
|
+
try:
|
|
448
|
+
relative = absolute.absolute().relative_to(root).as_posix()
|
|
449
|
+
except ValueError as exc:
|
|
450
|
+
raise WritePolicyError(f"{label} path is outside project root") from exc
|
|
451
|
+
if not relative or relative == ".":
|
|
452
|
+
raise WritePolicyError(f"{label} path must name a file")
|
|
453
|
+
current = root
|
|
454
|
+
for part in PurePosixPath(relative).parts:
|
|
455
|
+
current /= part
|
|
456
|
+
if current.is_symlink():
|
|
457
|
+
raise WritePolicyError(f"{label} path contains a symlink component")
|
|
458
|
+
return relative
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _planned_paths_from_report(report_path: Path, stage: int) -> tuple[str, ...]:
|
|
462
|
+
payload = _read_json(report_path.with_suffix(".data.json"), "approved plan data")
|
|
463
|
+
planning = payload.get("implementationPlanning")
|
|
464
|
+
stages = planning.get("stages") if isinstance(planning, Mapping) else None
|
|
465
|
+
selected = next(
|
|
466
|
+
(
|
|
467
|
+
row for row in stages
|
|
468
|
+
if isinstance(row, Mapping) and row.get("stage") == stage
|
|
469
|
+
),
|
|
470
|
+
None,
|
|
471
|
+
) if isinstance(stages, list) else None
|
|
472
|
+
steps = selected.get("stepwiseExecution") if isinstance(selected, Mapping) else None
|
|
473
|
+
paths = {
|
|
474
|
+
part.strip().strip("`")
|
|
475
|
+
for step in steps if isinstance(step, Mapping)
|
|
476
|
+
for part in str(step.get("files") or "").replace("<br>", ",").split(",")
|
|
477
|
+
if part.strip()
|
|
478
|
+
} if isinstance(steps, list) else set()
|
|
479
|
+
if not paths:
|
|
480
|
+
raise WritePolicyError("implementer write policy stage has no planned paths")
|
|
481
|
+
return _relative_paths(tuple(sorted(paths)))
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _rooted(project_root: Path, value: str) -> Path:
|
|
485
|
+
path = Path(value)
|
|
486
|
+
return path if path.is_absolute() else project_root / path
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _read_json(path: Path, label: str) -> Mapping[str, Any]:
|
|
490
|
+
try:
|
|
491
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
492
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
493
|
+
raise WritePolicyError(f"{label} is invalid: {path}") from exc
|
|
494
|
+
if not isinstance(value, Mapping):
|
|
495
|
+
raise WritePolicyError(f"{label} must be an object: {path}")
|
|
496
|
+
return value
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _relative_paths(value: object) -> tuple[str, ...]:
|
|
500
|
+
paths: list[str] = []
|
|
501
|
+
for raw in _text_sequence(value, "relative paths"):
|
|
502
|
+
candidate = PurePosixPath(raw)
|
|
503
|
+
if candidate.is_absolute() or not candidate.parts or ".." in candidate.parts:
|
|
504
|
+
raise WritePolicyError("policy paths must be normalized relative paths")
|
|
505
|
+
normalized = candidate.as_posix()
|
|
506
|
+
if normalized in {"", "."} or normalized != raw:
|
|
507
|
+
raise WritePolicyError("policy paths must be normalized relative paths")
|
|
508
|
+
paths.append(normalized)
|
|
509
|
+
if len(set(paths)) != len(paths):
|
|
510
|
+
raise WritePolicyError("policy paths must be unique")
|
|
511
|
+
return tuple(paths)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _mapping(value: object, label: str) -> Mapping[str, Any]:
|
|
515
|
+
if not isinstance(value, Mapping):
|
|
516
|
+
raise WritePolicyError(f"{label} must be an object")
|
|
517
|
+
return value
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _text_sequence(value: object, label: str) -> tuple[str, ...]:
|
|
521
|
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
|
|
522
|
+
raise WritePolicyError(f"{label} must be an array")
|
|
523
|
+
rows = tuple(value)
|
|
524
|
+
if any(not isinstance(row, str) or not row for row in rows):
|
|
525
|
+
raise WritePolicyError(f"{label} must contain non-empty strings")
|
|
526
|
+
return rows
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _required_text(payload: Mapping[str, Any], key: str) -> str:
|
|
530
|
+
value = payload.get(key)
|
|
531
|
+
if not isinstance(value, str) or not value.strip():
|
|
532
|
+
raise WritePolicyError(f"{key} must be a non-empty string")
|
|
533
|
+
return value
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _absolute_text(value: object, label: str) -> str:
|
|
537
|
+
if not isinstance(value, str) or not value or not Path(value).is_absolute():
|
|
538
|
+
raise WritePolicyError(f"{label} must be an absolute path")
|
|
539
|
+
return value
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _git_path(root: Path, flag: str) -> Path:
|
|
543
|
+
raw = _git_output(root, "rev-parse", "--path-format=absolute", flag)
|
|
544
|
+
path = Path(raw).resolve()
|
|
545
|
+
if not path.exists():
|
|
546
|
+
raise WritePolicyError(f"Git projection path does not exist: {path}")
|
|
547
|
+
return path
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _require_commit(root: Path, commit: str) -> None:
|
|
551
|
+
result = subprocess.run(
|
|
552
|
+
["git", "-C", str(root), "cat-file", "-e", f"{commit}^{{commit}}"],
|
|
553
|
+
capture_output=True,
|
|
554
|
+
text=True,
|
|
555
|
+
check=False,
|
|
556
|
+
)
|
|
557
|
+
if result.returncode != 0:
|
|
558
|
+
raise WritePolicyError(result.stderr.strip() or "expected base is not a commit")
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _git_output(root: Path, *args: str) -> str:
|
|
562
|
+
result = subprocess.run(
|
|
563
|
+
["git", "-C", str(root), *args],
|
|
564
|
+
capture_output=True,
|
|
565
|
+
text=True,
|
|
566
|
+
check=False,
|
|
567
|
+
)
|
|
568
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
569
|
+
detail = result.stderr.strip() or "Git command returned no value"
|
|
570
|
+
raise WritePolicyError(detail)
|
|
571
|
+
return result.stdout.strip()
|