okstra 0.173.0 → 0.175.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -9
- package/bin/okstra +8 -4
- package/dist/cli-registry.d.mts +16 -0
- package/dist/cli-registry.mjs +560 -0
- package/dist/cli-registry.mjs.map +1 -0
- package/dist/commands/execute/agent-prompt.d.mts +1 -0
- package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
- package/dist/commands/execute/agent-prompt.mjs.map +1 -0
- package/dist/commands/execute/codex-dispatch.d.mts +3 -0
- package/dist/commands/execute/codex-dispatch.mjs +6 -0
- package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
- package/dist/commands/execute/codex-run.d.mts +3 -0
- package/{src → dist}/commands/execute/codex-run.mjs +30 -37
- package/dist/commands/execute/codex-run.mjs.map +1 -0
- package/dist/commands/execute/convergence.d.mts +1 -0
- package/{src → dist}/commands/execute/convergence.mjs +15 -16
- package/dist/commands/execute/convergence.mjs.map +1 -0
- package/dist/commands/execute/error-log.d.mts +1 -0
- package/{src → dist}/commands/execute/error-log.mjs +2 -3
- package/dist/commands/execute/error-log.mjs.map +1 -0
- package/dist/commands/execute/git-reconcile.d.mts +1 -0
- package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
- package/dist/commands/execute/git-reconcile.mjs.map +1 -0
- package/dist/commands/execute/handoff.d.mts +1 -0
- package/{src → dist}/commands/execute/handoff.mjs +10 -11
- package/dist/commands/execute/handoff.mjs.map +1 -0
- package/dist/commands/execute/incremental-carry.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
- package/dist/commands/execute/incremental-carry.mjs.map +1 -0
- package/dist/commands/execute/incremental-scope.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
- package/dist/commands/execute/incremental-scope.mjs.map +1 -0
- package/dist/commands/execute/integrate-stages.d.mts +1 -0
- package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
- package/dist/commands/execute/integrate-stages.mjs.map +1 -0
- package/dist/commands/execute/pane-title.d.mts +1 -0
- package/dist/commands/execute/pane-title.mjs +20 -0
- package/dist/commands/execute/pane-title.mjs.map +1 -0
- package/dist/commands/execute/plan-items.d.mts +1 -0
- package/dist/commands/execute/plan-items.mjs +9 -0
- package/dist/commands/execute/plan-items.mjs.map +1 -0
- package/dist/commands/execute/plan-validate.d.mts +1 -0
- package/dist/commands/execute/plan-validate.mjs +68 -0
- package/dist/commands/execute/plan-validate.mjs.map +1 -0
- package/dist/commands/execute/plan-verify.d.mts +1 -0
- package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
- package/dist/commands/execute/plan-verify.mjs.map +1 -0
- package/dist/commands/execute/render-bundle.d.mts +3 -0
- package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
- package/dist/commands/execute/render-bundle.mjs.map +1 -0
- package/dist/commands/execute/run.d.mts +24 -0
- package/dist/commands/execute/run.mjs +196 -0
- package/dist/commands/execute/run.mjs.map +1 -0
- package/dist/commands/execute/spawn-followups.d.mts +1 -0
- package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
- package/dist/commands/execute/spawn-followups.mjs.map +1 -0
- package/dist/commands/execute/team.d.mts +3 -0
- package/dist/commands/execute/team.mjs +56 -0
- package/dist/commands/execute/team.mjs.map +1 -0
- package/dist/commands/execute/token-usage.d.mts +1 -0
- package/{src → dist}/commands/execute/token-usage.mjs +2 -3
- package/dist/commands/execute/token-usage.mjs.map +1 -0
- package/dist/commands/execute/wizard.d.mts +8 -0
- package/dist/commands/execute/wizard.mjs +191 -0
- package/dist/commands/execute/wizard.mjs.map +1 -0
- package/dist/commands/execute/worker-audit-check.d.mts +1 -0
- package/{src → dist}/commands/execute/worker-audit-check.mjs +11 -12
- package/dist/commands/execute/worker-audit-check.mjs.map +1 -0
- package/dist/commands/execute/worker-dispatch.d.mts +7 -0
- package/{src → dist}/commands/execute/worker-dispatch.mjs +34 -46
- package/dist/commands/execute/worker-dispatch.mjs.map +1 -0
- package/dist/commands/execute/worker-state.d.mts +1 -0
- package/{src → dist}/commands/execute/worker-state.mjs +15 -16
- package/dist/commands/execute/worker-state.mjs.map +1 -0
- package/dist/commands/execute/worktree-lookup.d.mts +1 -0
- package/{src → dist}/commands/execute/worktree-lookup.mjs +38 -37
- package/dist/commands/execute/worktree-lookup.mjs.map +1 -0
- package/dist/commands/execute/worktree-status.d.mts +1 -0
- package/dist/commands/execute/worktree-status.mjs +115 -0
- package/dist/commands/execute/worktree-status.mjs.map +1 -0
- package/dist/commands/inspect/code-review.d.mts +1 -0
- package/{src → dist}/commands/inspect/code-review.mjs +16 -19
- package/dist/commands/inspect/code-review.mjs.map +1 -0
- package/dist/commands/inspect/container.d.mts +1 -0
- package/{src → dist}/commands/inspect/container.mjs +11 -13
- package/dist/commands/inspect/container.mjs.map +1 -0
- package/dist/commands/inspect/context-cost.d.mts +1 -0
- package/{src → dist}/commands/inspect/context-cost.mjs +11 -13
- package/dist/commands/inspect/context-cost.mjs.map +1 -0
- package/dist/commands/inspect/design-prep.d.mts +1 -0
- package/{src → dist}/commands/inspect/design-prep.mjs +10 -11
- package/dist/commands/inspect/design-prep.mjs.map +1 -0
- package/dist/commands/inspect/error-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/error-report.mjs +11 -13
- package/dist/commands/inspect/error-report.mjs.map +1 -0
- package/dist/commands/inspect/error-zip.d.mts +1 -0
- package/{src → dist}/commands/inspect/error-zip.mjs +11 -12
- package/dist/commands/inspect/error-zip.mjs.map +1 -0
- package/dist/commands/inspect/log-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/log-report.mjs +11 -13
- package/dist/commands/inspect/log-report.mjs.map +1 -0
- package/dist/commands/inspect/profile-show.d.mts +1 -0
- package/{src → dist}/commands/inspect/profile-show.mjs +10 -11
- package/dist/commands/inspect/profile-show.mjs.map +1 -0
- package/dist/commands/inspect/recap.d.mts +1 -0
- package/{src → dist}/commands/inspect/recap.mjs +11 -13
- package/dist/commands/inspect/recap.mjs.map +1 -0
- package/dist/commands/inspect/resolve-task-key.d.mts +1 -0
- package/{src → dist}/commands/inspect/resolve-task-key.mjs +11 -13
- package/dist/commands/inspect/resolve-task-key.mjs.map +1 -0
- package/dist/commands/inspect/rollup.d.mts +1 -0
- package/{src → dist}/commands/inspect/rollup.mjs +11 -13
- package/dist/commands/inspect/rollup.mjs.map +1 -0
- package/dist/commands/inspect/run-audit.d.mts +1 -0
- package/{src → dist}/commands/inspect/run-audit.mjs +11 -12
- package/dist/commands/inspect/run-audit.mjs.map +1 -0
- package/dist/commands/inspect/set-work-status.d.mts +1 -0
- package/{src → dist}/commands/inspect/set-work-status.mjs +11 -13
- package/dist/commands/inspect/set-work-status.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.d.mts +1 -0
- package/dist/commands/inspect/stage-map.mjs +110 -0
- package/dist/commands/inspect/stage-map.mjs.map +1 -0
- package/dist/commands/inspect/task-list.d.mts +1 -0
- package/dist/commands/inspect/task-list.mjs +103 -0
- package/dist/commands/inspect/task-list.mjs.map +1 -0
- package/dist/commands/inspect/task-show.d.mts +1 -0
- package/dist/commands/inspect/task-show.mjs +108 -0
- package/dist/commands/inspect/task-show.mjs.map +1 -0
- package/dist/commands/inspect/time-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/time-report.mjs +11 -13
- package/dist/commands/inspect/time-report.mjs.map +1 -0
- package/dist/commands/inspect/usage-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/usage-report.mjs +11 -13
- package/dist/commands/inspect/usage-report.mjs.map +1 -0
- package/dist/commands/inspect/user-response.d.mts +1 -0
- package/{src → dist}/commands/inspect/user-response.mjs +10 -11
- package/dist/commands/inspect/user-response.mjs.map +1 -0
- package/dist/commands/inspect/worker-liveness.d.mts +1 -0
- package/{src → dist}/commands/inspect/worker-liveness.mjs +11 -13
- package/dist/commands/inspect/worker-liveness.mjs.map +1 -0
- package/dist/commands/lifecycle/check-project.d.mts +21 -0
- package/dist/commands/lifecycle/check-project.mjs +175 -0
- package/dist/commands/lifecycle/check-project.mjs.map +1 -0
- package/dist/commands/lifecycle/config.d.mts +1 -0
- package/dist/commands/lifecycle/config.mjs +369 -0
- package/dist/commands/lifecycle/config.mjs.map +1 -0
- package/dist/commands/lifecycle/doctor.d.mts +10 -0
- package/dist/commands/lifecycle/doctor.mjs +317 -0
- package/dist/commands/lifecycle/doctor.mjs.map +1 -0
- package/dist/commands/lifecycle/install.d.mts +17 -0
- package/dist/commands/lifecycle/install.mjs +1077 -0
- package/dist/commands/lifecycle/install.mjs.map +1 -0
- package/dist/commands/lifecycle/migrate.d.mts +1 -0
- package/{src → dist}/commands/lifecycle/migrate.mjs +10 -11
- package/dist/commands/lifecycle/migrate.mjs.map +1 -0
- package/dist/commands/lifecycle/model.d.mts +1 -0
- package/dist/commands/lifecycle/model.mjs +22 -0
- package/dist/commands/lifecycle/model.mjs.map +1 -0
- package/dist/commands/lifecycle/paths.d.mts +1 -0
- package/dist/commands/lifecycle/paths.mjs +102 -0
- package/dist/commands/lifecycle/paths.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.d.mts +1 -0
- package/dist/commands/lifecycle/preflight.mjs +124 -0
- package/dist/commands/lifecycle/preflight.mjs.map +1 -0
- package/dist/commands/lifecycle/setup.d.mts +1 -0
- package/dist/commands/lifecycle/setup.mjs +297 -0
- package/dist/commands/lifecycle/setup.mjs.map +1 -0
- package/dist/commands/lifecycle/uninstall.d.mts +4 -0
- package/dist/commands/lifecycle/uninstall.mjs +262 -0
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -0
- package/dist/commands/manager.d.mts +3 -0
- package/{src → dist}/commands/manager.mjs +25 -32
- package/dist/commands/manager.mjs.map +1 -0
- package/dist/commands/memory/memory.d.mts +1 -0
- package/dist/commands/memory/memory.mjs +569 -0
- package/dist/commands/memory/memory.mjs.map +1 -0
- package/dist/commands/pr/pr.d.mts +33 -0
- package/dist/commands/pr/pr.mjs +296 -0
- package/dist/commands/pr/pr.mjs.map +1 -0
- package/dist/commands/report/agent-activity.d.mts +1 -0
- package/{src → dist}/commands/report/agent-activity.mjs +11 -12
- package/dist/commands/report/agent-activity.mjs.map +1 -0
- package/dist/commands/report/finalize.d.mts +4 -0
- package/dist/commands/report/finalize.mjs +64 -0
- package/dist/commands/report/finalize.mjs.map +1 -0
- package/dist/commands/report/inject-report-index.d.mts +1 -0
- package/{src → dist}/commands/report/inject-report-index.mjs +6 -7
- package/dist/commands/report/inject-report-index.mjs.map +1 -0
- package/dist/commands/report/render-final-report.d.mts +1 -0
- package/{src → dist}/commands/report/render-final-report.mjs +6 -7
- package/dist/commands/report/render-final-report.mjs.map +1 -0
- package/dist/commands/report/render-views.d.mts +1 -0
- package/{src → dist}/commands/report/render-views.mjs +6 -7
- package/dist/commands/report/render-views.mjs.map +1 -0
- package/dist/commands/report/translate.d.mts +1 -0
- package/{src → dist}/commands/report/translate.mjs +6 -7
- package/dist/commands/report/translate.mjs.map +1 -0
- package/dist/lib/helper-scripts.d.mts +3 -0
- package/{src → dist}/lib/helper-scripts.mjs +11 -14
- package/dist/lib/helper-scripts.mjs.map +1 -0
- package/dist/lib/host-registry-client.d.mts +25 -0
- package/dist/lib/host-registry-client.mjs +152 -0
- package/dist/lib/host-registry-client.mjs.map +1 -0
- package/dist/lib/install-assets.d.mts +8 -0
- package/{src → dist}/lib/install-assets.mjs +17 -20
- package/dist/lib/install-assets.mjs.map +1 -0
- package/dist/lib/okstra-dirs.d.mts +4 -0
- package/{src → dist}/lib/okstra-dirs.mjs +3 -6
- package/dist/lib/okstra-dirs.mjs.map +1 -0
- package/dist/lib/paths.d.mts +4 -0
- package/dist/lib/paths.mjs +78 -0
- package/dist/lib/paths.mjs.map +1 -0
- package/dist/lib/proc.d.mts +3 -0
- package/dist/lib/proc.mjs +31 -0
- package/dist/lib/proc.mjs.map +1 -0
- package/dist/lib/python-helper.d.mts +8 -0
- package/dist/lib/python-helper.mjs +123 -0
- package/dist/lib/python-helper.mjs.map +1 -0
- package/dist/lib/runtime-manifest.d.mts +7 -0
- package/dist/lib/runtime-manifest.mjs +73 -0
- package/dist/lib/runtime-manifest.mjs.map +1 -0
- package/dist/lib/skill-catalog.d.mts +4 -0
- package/dist/lib/skill-catalog.mjs +50 -0
- package/dist/lib/skill-catalog.mjs.map +1 -0
- package/dist/lib/types.d.mts +129 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/lib/types.mjs.map +1 -0
- package/dist/lib/version.d.mts +2 -0
- package/dist/lib/version.mjs +27 -0
- package/dist/lib/version.mjs.map +1 -0
- package/docs/architecture/storage-model.md +17 -3
- package/docs/architecture.md +12 -22
- package/docs/cli.md +29 -8
- package/docs/container.md +1 -1
- package/docs/contributor-change-matrix.md +3 -3
- package/docs/project-structure-overview.md +29 -20
- package/docs/task-process/README.md +3 -1
- package/docs/task-process/implementation-planning.md +2 -2
- package/docs/task-process/implementation.md +1 -1
- package/package.json +11 -5
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +3 -2
- package/runtime/agents/workers/report-writer-worker.md +3 -1
- package/runtime/agents/workers/translator-worker.md +3 -1
- package/runtime/bin/lib/okstra/cli.sh +12 -0
- package/runtime/bin/lib/okstra/globals.sh +4 -1
- package/runtime/bin/lib/okstra/usage.sh +4 -4
- package/runtime/bin/okstra-error-log.py +27 -0
- package/runtime/bin/okstra-provider-exec.py +245 -16
- package/runtime/bin/okstra-trace-cleanup.sh +58 -129
- package/runtime/bin/okstra.sh +10 -0
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/convergence.md +27 -18
- package/runtime/prompts/lead/okstra-lead-contract.md +4 -2
- package/runtime/prompts/lead/plan-body-verification.md +13 -6
- package/runtime/prompts/lead/report-writer.md +6 -6
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/change-impact-analysis.md +16 -0
- package/runtime/prompts/profiles/error-analysis.md +21 -1
- package/runtime/prompts/profiles/feature-analysis.md +16 -0
- package/runtime/prompts/profiles/final-verification.md +16 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +16 -0
- package/runtime/prompts/profiles/implementation-planning.md +32 -9
- package/runtime/prompts/profiles/implementation.md +17 -1
- package/runtime/prompts/profiles/improvement-discovery.md +16 -0
- package/runtime/prompts/profiles/project-analysis.md +16 -0
- package/runtime/prompts/profiles/release-handoff.md +5 -1
- package/runtime/prompts/profiles/requirements-discovery.md +20 -0
- package/runtime/prompts/wizard/prompts.ko.json +18 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +100 -25
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
- package/runtime/python/okstra_ctl/agent_activity.py +5 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
- package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
- package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
- package/runtime/python/okstra_ctl/conformance.py +68 -0
- package/runtime/python/okstra_ctl/convergence.py +409 -3
- package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
- package/runtime/python/okstra_ctl/convergence_store.py +170 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +1181 -102
- package/runtime/python/okstra_ctl/dispatch_state.py +835 -22
- package/runtime/python/okstra_ctl/doctor.py +16 -2
- package/runtime/python/okstra_ctl/domain/host.py +32 -1
- package/runtime/python/okstra_ctl/domain/provider.py +212 -6
- package/runtime/python/okstra_ctl/domain/role.py +125 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +13 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +39 -1
- package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
- package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
- package/runtime/python/okstra_ctl/error_log_write.py +9 -0
- package/runtime/python/okstra_ctl/error_report.py +21 -1
- package/runtime/python/okstra_ctl/execution_identity.py +529 -0
- package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
- package/runtime/python/okstra_ctl/index.py +6 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
- package/runtime/python/okstra_ctl/legacy_model_selection.py +490 -0
- package/runtime/python/okstra_ctl/model_cli.py +278 -0
- package/runtime/python/okstra_ctl/model_defaults.py +46 -0
- package/runtime/python/okstra_ctl/model_discovery.py +26 -7
- package/runtime/python/okstra_ctl/model_pool.py +501 -0
- package/runtime/python/okstra_ctl/models.py +47 -27
- package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
- package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
- package/runtime/python/okstra_ctl/pane_title.py +151 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
- package/runtime/python/okstra_ctl/render.py +78 -10
- package/runtime/python/okstra_ctl/render_final_report.py +34 -19
- package/runtime/python/okstra_ctl/report_contract.py +39 -1
- package/runtime/python/okstra_ctl/report_finalize.py +84 -9
- package/runtime/python/okstra_ctl/report_html/render.py +1 -0
- package/runtime/python/okstra_ctl/role_requirements.py +217 -0
- package/runtime/python/okstra_ctl/run.py +1250 -260
- package/runtime/python/okstra_ctl/run_index_row.py +7 -1
- package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
- package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
- package/runtime/python/okstra_ctl/session.py +69 -12
- package/runtime/python/okstra_ctl/team.py +51 -25
- package/runtime/python/okstra_ctl/time_report.py +38 -4
- package/runtime/python/okstra_ctl/tmux.py +19 -149
- package/runtime/python/okstra_ctl/usage_identity.py +86 -0
- package/runtime/python/okstra_ctl/usage_report.py +15 -3
- package/runtime/python/okstra_ctl/wizard.py +1253 -62
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
- package/runtime/python/okstra_ctl/worker_request.py +9 -1
- package/runtime/python/okstra_ctl/worker_runner.py +130 -5
- package/runtime/python/okstra_ctl/workers.py +7 -5
- package/runtime/python/okstra_ctl/worktree.py +69 -3
- package/runtime/python/okstra_ctl/write_policy.py +571 -0
- package/runtime/python/okstra_token_usage/cli.py +1 -1
- package/runtime/python/okstra_token_usage/collect.py +66 -6
- package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
- package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
- package/runtime/schemas/final-report-v2.0.schema.json +25 -0
- package/runtime/skills/okstra-run/SKILL.md +9 -9
- package/runtime/skills/okstra-setup/references/project-config.md +11 -0
- package/runtime/templates/report-writer-prompt-preamble.md +2 -0
- package/runtime/templates/reports/final-report-v2.template.md +7 -1
- package/runtime/templates/reports/html/base.template.html +21 -0
- package/runtime/templates/reports/html/i18n/en.json +4 -0
- package/runtime/templates/reports/html/i18n/ko.json +4 -0
- package/runtime/templates/reports/settings.template.json +0 -24
- package/runtime/templates/worker-prompt-preamble.md +6 -2
- package/runtime/validators/lib/fixtures.sh +98 -33
- package/runtime/validators/validate-implementation-plan-stages.py +63 -3
- package/runtime/validators/validate-run.py +443 -508
- package/runtime/validators/validate_session_conformance.py +26 -8
- package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
- package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
- package/runtime/templates/reports/final-report.template.md +0 -1258
- package/src/cli-registry.mjs +0 -532
- package/src/commands/execute/codex-dispatch.mjs +0 -10
- package/src/commands/execute/plan-items.mjs +0 -9
- package/src/commands/execute/plan-validate.mjs +0 -68
- package/src/commands/execute/run.mjs +0 -208
- package/src/commands/execute/team.mjs +0 -63
- package/src/commands/execute/wizard.mjs +0 -167
- package/src/commands/inspect/stage-map.mjs +0 -98
- package/src/commands/inspect/task-list.mjs +0 -97
- package/src/commands/inspect/task-show.mjs +0 -100
- package/src/commands/lifecycle/check-project.mjs +0 -187
- package/src/commands/lifecycle/config.mjs +0 -385
- package/src/commands/lifecycle/doctor.mjs +0 -308
- package/src/commands/lifecycle/install.mjs +0 -1172
- package/src/commands/lifecycle/paths.mjs +0 -104
- package/src/commands/lifecycle/preflight.mjs +0 -125
- package/src/commands/lifecycle/setup.mjs +0 -333
- package/src/commands/lifecycle/uninstall.mjs +0 -279
- package/src/commands/memory/memory.mjs +0 -524
- package/src/commands/pr/pr.mjs +0 -261
- package/src/commands/report/finalize.mjs +0 -64
- package/src/lib/host-registry-client.mjs +0 -176
- package/src/lib/paths.mjs +0 -86
- package/src/lib/proc.mjs +0 -31
- package/src/lib/python-helper.mjs +0 -142
- package/src/lib/runtime-manifest.mjs +0 -70
- package/src/lib/skill-catalog.mjs +0 -53
- package/src/lib/version.mjs +0 -20
- /package/{src → dist}/commands/pr/default.md +0 -0
|
@@ -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()
|
|
@@ -57,7 +57,7 @@ def main() -> int:
|
|
|
57
57
|
"from the freshly computed usageSummary, then re-render the "
|
|
58
58
|
"sibling final-report markdown via the renderer. The data.json "
|
|
59
59
|
"is the SSOT; the markdown is regenerated from it. "
|
|
60
|
-
"See schemas/final-report-
|
|
60
|
+
"See schemas/final-report-v2.0.schema.json for the data shape."
|
|
61
61
|
),
|
|
62
62
|
)
|
|
63
63
|
parser.add_argument(
|
|
@@ -21,6 +21,7 @@ from .antigravity import (
|
|
|
21
21
|
)
|
|
22
22
|
from .paths import claude_project_dir, utc_now
|
|
23
23
|
from .pricing import antigravity_cost_usd, provider_cost_usd
|
|
24
|
+
from okstra_ctl.dispatch_state import worker_session_ids
|
|
24
25
|
from okstra_ctl.models import provider_wrappers
|
|
25
26
|
from okstra_ctl.wrapper_status import read_wrapper_status, status_path_for_prompt
|
|
26
27
|
|
|
@@ -864,22 +865,66 @@ def collect_claude_runtime_usage(
|
|
|
864
865
|
f"lead session jsonl not found under {claude_project_dir(cwd)} (sessionId={lead_sid})"
|
|
865
866
|
)
|
|
866
867
|
|
|
867
|
-
# Workers —
|
|
868
|
-
#
|
|
869
|
-
# implementation `-executor`, report-writer `-impl` / `-2
|
|
868
|
+
# Workers — dispatch 가 기록한 세션 id 로 짚은 세션과 agentName prefix 로
|
|
869
|
+
# 찾은 세션의 합집합을 합산한다(재배치 `-002`, convergence `-reverify-r1`,
|
|
870
|
+
# implementation `-executor`, report-writer `-impl` / `-2` 등).
|
|
871
|
+
sessions_dir = claude_project_dir(cwd)
|
|
872
|
+
# sid 로 귀속한 세션 id 전체 — 아래 unattributed 폴드에서 빼는 데 쓴다.
|
|
873
|
+
attributed_sids: set[str] = set()
|
|
870
874
|
for worker in state.get("workers", []):
|
|
871
875
|
worker_id = worker.get("workerId")
|
|
872
876
|
agent = worker.get("agent")
|
|
873
877
|
prefixes = match_prefixes(worker_id) if worker_id else []
|
|
874
878
|
|
|
879
|
+
# pane 워커는 별도 `claude -p` 프로세스라 jsonl 에 agentName 도 teamName
|
|
880
|
+
# 도 안 남긴다 — 아래 prefix 경로로는 영원히 매칭되지 않는다. dispatch 가
|
|
881
|
+
# 발급해 적어 둔 이 id 가 그 세션을 짚는 유일한 결정적 단서다. 재시도는
|
|
882
|
+
# attempt 마다 새 세션이므로 기록된 id 를 전부 합산한다.
|
|
883
|
+
# 헬퍼에서 worker_id=None 은 "run 전체"라, 이름 없는 워커에 그대로 넘기면
|
|
884
|
+
# 그 워커가 run 의 모든 세션을 흡수한다.
|
|
885
|
+
dispatched_sids = worker_session_ids(state, worker_id) if worker_id else []
|
|
875
886
|
matched: list[tuple[str, Path, dict]] = []
|
|
887
|
+
matched_sids: set[str] = set()
|
|
888
|
+
for sid in dispatched_sids:
|
|
889
|
+
path = sessions_dir / f"{sid}.jsonl"
|
|
890
|
+
if not path.is_file():
|
|
891
|
+
continue
|
|
892
|
+
totals = claude_session_totals(path, since=run_since, until=run_until,
|
|
893
|
+
incremental=incremental)
|
|
894
|
+
# 창 밖 세션은 여기서 버린다 — agentName 발견 경로가 위에서 같은
|
|
895
|
+
# `startedAt` 검사로 거르는 것과 같은 이유다. 넣으면 0 토큰 totals 가
|
|
896
|
+
# usage_block 을 타고 "이 워커는 0 을 썼다"로 보고되어, unavailable 로
|
|
897
|
+
# 남아야 할 상태를 허위 0 이 덮는다.
|
|
898
|
+
if not totals.get("startedAt"):
|
|
899
|
+
continue
|
|
900
|
+
matched.append((sid, path, totals))
|
|
901
|
+
matched_sids.add(sid)
|
|
902
|
+
attributed_sids.add(sid)
|
|
903
|
+
|
|
904
|
+
# 조건부 폴백이 아니라 합집합이다. 한 워커의 attempt 1 이 pane(sid 로만
|
|
905
|
+
# 찾힌다), attempt 2 가 in-process 서브에이전트(agentName 으로만 찾힌다)일
|
|
906
|
+
# 수 있고, `if not matched:` 로 두면 sid 가 하나라도 맞는 순간 attempt 2 의
|
|
907
|
+
# 토큰이 통째로 누락된다. 양쪽에서 온 같은 세션은 sid 로 한 번만 센다.
|
|
876
908
|
for agent_name, entries in by_agent.items():
|
|
877
|
-
if agent_matches(agent_name, prefixes):
|
|
878
|
-
|
|
909
|
+
if not agent_matches(agent_name, prefixes):
|
|
910
|
+
continue
|
|
911
|
+
for entry in entries:
|
|
912
|
+
if entry[0] in matched_sids:
|
|
913
|
+
continue
|
|
914
|
+
# team-needle 경로는 창 검사 없이 by_agent 를 채운다. 같은 리드
|
|
915
|
+
# 세션의 다음 run 이 띄운 워커도 같은 needle 에 걸리는데, 창 밖이라
|
|
916
|
+
# startedAt 이 없어 아래 정렬에서 맨 앞에 서고 `sessionId` 를
|
|
917
|
+
# 차지한다 — 토큰은 0 이라 합계는 안 틀리고 리포트가 가리키는
|
|
918
|
+
# 세션만 남의 것이 된다.
|
|
919
|
+
if not entry[2].get("startedAt"):
|
|
920
|
+
continue
|
|
921
|
+
matched.append(entry)
|
|
922
|
+
matched_sids.add(entry[0])
|
|
879
923
|
|
|
880
924
|
if not matched:
|
|
881
925
|
worker["usage"] = na_block(
|
|
882
|
-
|
|
926
|
+
"no Claude session jsonl in the run window for dispatched "
|
|
927
|
+
f"sessionIds {dispatched_sids} or agentName prefixes {prefixes}"
|
|
883
928
|
)
|
|
884
929
|
continue
|
|
885
930
|
|
|
@@ -919,6 +964,21 @@ def collect_claude_runtime_usage(
|
|
|
919
964
|
# session the harness never tagged with `name`. Without this, the run-level
|
|
920
965
|
# Worker total reads 0 and the report validator hard-fails a legitimate run.
|
|
921
966
|
# Attribution is aggregate, not per-worker; usageSummary records it openly.
|
|
967
|
+
#
|
|
968
|
+
# 먼저 sid 로 귀속된 세션을 뺀다. 이 두 집합은 `agentName` 유무로 배타적이었지만
|
|
969
|
+
# (by_agent 는 있어야 들어가고 unattributed 는 없어야 들어간다) sid 경로는
|
|
970
|
+
# agentName 을 안 보므로 그 배타성이 더는 성립하지 않는다. team needle 에
|
|
971
|
+
# 걸리면서 agentName 이 없는 세션이 동시에 기록된 dispatch sid 이기도 하면,
|
|
972
|
+
# 빼지 않을 경우 그 토큰이 워커 usage 와 이 폴드 양쪽에 들어가 `_populate_usage_summary`
|
|
973
|
+
# 의 workerTotalTokens 를 부풀린다. 두 리스트는 인덱스가 대응하므로 함께 거른다.
|
|
974
|
+
kept = [
|
|
975
|
+
(sid, totals)
|
|
976
|
+
for sid, totals in zip(unattributed_sessions, unattributed_totals)
|
|
977
|
+
if sid not in attributed_sids
|
|
978
|
+
]
|
|
979
|
+
unattributed_sessions = [sid for sid, _ in kept]
|
|
980
|
+
unattributed_totals = [totals for _, totals in kept]
|
|
981
|
+
|
|
922
982
|
unattributed_usage = None
|
|
923
983
|
if unattributed_totals:
|
|
924
984
|
unattributed_usage = usage_block(
|