okstra 0.174.0 → 0.176.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -9
- package/bin/okstra +8 -4
- package/dist/cli-registry.d.mts +16 -0
- package/dist/cli-registry.mjs +560 -0
- package/dist/cli-registry.mjs.map +1 -0
- package/dist/commands/execute/agent-prompt.d.mts +1 -0
- package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
- package/dist/commands/execute/agent-prompt.mjs.map +1 -0
- package/dist/commands/execute/codex-dispatch.d.mts +3 -0
- package/dist/commands/execute/codex-dispatch.mjs +6 -0
- package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
- package/dist/commands/execute/codex-run.d.mts +3 -0
- package/{src → dist}/commands/execute/codex-run.mjs +30 -37
- package/dist/commands/execute/codex-run.mjs.map +1 -0
- package/dist/commands/execute/convergence.d.mts +1 -0
- package/{src → dist}/commands/execute/convergence.mjs +15 -16
- package/dist/commands/execute/convergence.mjs.map +1 -0
- package/dist/commands/execute/error-log.d.mts +1 -0
- package/{src → dist}/commands/execute/error-log.mjs +2 -3
- package/dist/commands/execute/error-log.mjs.map +1 -0
- package/dist/commands/execute/git-reconcile.d.mts +1 -0
- package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
- package/dist/commands/execute/git-reconcile.mjs.map +1 -0
- package/dist/commands/execute/handoff.d.mts +1 -0
- package/{src → dist}/commands/execute/handoff.mjs +10 -11
- package/dist/commands/execute/handoff.mjs.map +1 -0
- package/dist/commands/execute/incremental-carry.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
- package/dist/commands/execute/incremental-carry.mjs.map +1 -0
- package/dist/commands/execute/incremental-scope.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
- package/dist/commands/execute/incremental-scope.mjs.map +1 -0
- package/dist/commands/execute/integrate-stages.d.mts +1 -0
- package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
- package/dist/commands/execute/integrate-stages.mjs.map +1 -0
- package/dist/commands/execute/pane-title.d.mts +1 -0
- package/dist/commands/execute/pane-title.mjs +20 -0
- package/dist/commands/execute/pane-title.mjs.map +1 -0
- package/dist/commands/execute/plan-items.d.mts +1 -0
- package/dist/commands/execute/plan-items.mjs +9 -0
- package/dist/commands/execute/plan-items.mjs.map +1 -0
- package/dist/commands/execute/plan-validate.d.mts +1 -0
- package/dist/commands/execute/plan-validate.mjs +68 -0
- package/dist/commands/execute/plan-validate.mjs.map +1 -0
- package/dist/commands/execute/plan-verify.d.mts +1 -0
- package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
- package/dist/commands/execute/plan-verify.mjs.map +1 -0
- package/dist/commands/execute/render-bundle.d.mts +3 -0
- package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
- package/dist/commands/execute/render-bundle.mjs.map +1 -0
- package/dist/commands/execute/run.d.mts +24 -0
- package/dist/commands/execute/run.mjs +196 -0
- package/dist/commands/execute/run.mjs.map +1 -0
- package/dist/commands/execute/spawn-followups.d.mts +1 -0
- package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
- package/dist/commands/execute/spawn-followups.mjs.map +1 -0
- package/dist/commands/execute/team.d.mts +3 -0
- package/{src → dist}/commands/execute/team.mjs +29 -36
- package/dist/commands/execute/team.mjs.map +1 -0
- package/dist/commands/execute/token-usage.d.mts +1 -0
- package/{src → dist}/commands/execute/token-usage.mjs +2 -3
- package/dist/commands/execute/token-usage.mjs.map +1 -0
- package/dist/commands/execute/wizard.d.mts +8 -0
- package/dist/commands/execute/wizard.mjs +191 -0
- package/dist/commands/execute/wizard.mjs.map +1 -0
- package/dist/commands/execute/worker-audit-check.d.mts +1 -0
- package/{src → dist}/commands/execute/worker-audit-check.mjs +11 -12
- package/dist/commands/execute/worker-audit-check.mjs.map +1 -0
- package/dist/commands/execute/worker-dispatch.d.mts +7 -0
- package/{src → dist}/commands/execute/worker-dispatch.mjs +34 -46
- package/dist/commands/execute/worker-dispatch.mjs.map +1 -0
- package/dist/commands/execute/worker-state.d.mts +1 -0
- package/{src → dist}/commands/execute/worker-state.mjs +15 -16
- package/dist/commands/execute/worker-state.mjs.map +1 -0
- package/dist/commands/execute/worktree-lookup.d.mts +1 -0
- package/{src → dist}/commands/execute/worktree-lookup.mjs +38 -37
- package/dist/commands/execute/worktree-lookup.mjs.map +1 -0
- package/dist/commands/execute/worktree-status.d.mts +1 -0
- package/{src → dist}/commands/execute/worktree-status.mjs +52 -46
- package/dist/commands/execute/worktree-status.mjs.map +1 -0
- package/dist/commands/inspect/code-review.d.mts +1 -0
- package/{src → dist}/commands/inspect/code-review.mjs +16 -19
- package/dist/commands/inspect/code-review.mjs.map +1 -0
- package/dist/commands/inspect/container.d.mts +1 -0
- package/{src → dist}/commands/inspect/container.mjs +11 -13
- package/dist/commands/inspect/container.mjs.map +1 -0
- package/dist/commands/inspect/context-cost.d.mts +1 -0
- package/{src → dist}/commands/inspect/context-cost.mjs +11 -13
- package/dist/commands/inspect/context-cost.mjs.map +1 -0
- package/dist/commands/inspect/design-prep.d.mts +1 -0
- package/{src → dist}/commands/inspect/design-prep.mjs +10 -11
- package/dist/commands/inspect/design-prep.mjs.map +1 -0
- package/dist/commands/inspect/error-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/error-report.mjs +11 -13
- package/dist/commands/inspect/error-report.mjs.map +1 -0
- package/dist/commands/inspect/error-zip.d.mts +1 -0
- package/{src → dist}/commands/inspect/error-zip.mjs +11 -12
- package/dist/commands/inspect/error-zip.mjs.map +1 -0
- package/dist/commands/inspect/log-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/log-report.mjs +11 -13
- package/dist/commands/inspect/log-report.mjs.map +1 -0
- package/dist/commands/inspect/profile-show.d.mts +1 -0
- package/{src → dist}/commands/inspect/profile-show.mjs +10 -11
- package/dist/commands/inspect/profile-show.mjs.map +1 -0
- package/dist/commands/inspect/recap.d.mts +1 -0
- package/{src → dist}/commands/inspect/recap.mjs +11 -13
- package/dist/commands/inspect/recap.mjs.map +1 -0
- package/dist/commands/inspect/resolve-task-key.d.mts +1 -0
- package/{src → dist}/commands/inspect/resolve-task-key.mjs +11 -13
- package/dist/commands/inspect/resolve-task-key.mjs.map +1 -0
- package/dist/commands/inspect/rollup.d.mts +1 -0
- package/{src → dist}/commands/inspect/rollup.mjs +11 -13
- package/dist/commands/inspect/rollup.mjs.map +1 -0
- package/dist/commands/inspect/run-audit.d.mts +1 -0
- package/{src → dist}/commands/inspect/run-audit.mjs +11 -12
- package/dist/commands/inspect/run-audit.mjs.map +1 -0
- package/dist/commands/inspect/set-work-status.d.mts +1 -0
- package/{src → dist}/commands/inspect/set-work-status.mjs +11 -13
- package/dist/commands/inspect/set-work-status.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.d.mts +1 -0
- package/dist/commands/inspect/stage-map.mjs +110 -0
- package/dist/commands/inspect/stage-map.mjs.map +1 -0
- package/dist/commands/inspect/task-list.d.mts +1 -0
- package/dist/commands/inspect/task-list.mjs +103 -0
- package/dist/commands/inspect/task-list.mjs.map +1 -0
- package/dist/commands/inspect/task-show.d.mts +1 -0
- package/dist/commands/inspect/task-show.mjs +108 -0
- package/dist/commands/inspect/task-show.mjs.map +1 -0
- package/dist/commands/inspect/time-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/time-report.mjs +11 -13
- package/dist/commands/inspect/time-report.mjs.map +1 -0
- package/dist/commands/inspect/usage-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/usage-report.mjs +11 -13
- package/dist/commands/inspect/usage-report.mjs.map +1 -0
- package/dist/commands/inspect/user-response.d.mts +1 -0
- package/{src → dist}/commands/inspect/user-response.mjs +10 -11
- package/dist/commands/inspect/user-response.mjs.map +1 -0
- package/dist/commands/inspect/worker-liveness.d.mts +1 -0
- package/{src → dist}/commands/inspect/worker-liveness.mjs +11 -13
- package/dist/commands/inspect/worker-liveness.mjs.map +1 -0
- package/dist/commands/lifecycle/check-project.d.mts +21 -0
- package/dist/commands/lifecycle/check-project.mjs +175 -0
- package/dist/commands/lifecycle/check-project.mjs.map +1 -0
- package/dist/commands/lifecycle/config.d.mts +1 -0
- package/dist/commands/lifecycle/config.mjs +369 -0
- package/dist/commands/lifecycle/config.mjs.map +1 -0
- package/dist/commands/lifecycle/doctor.d.mts +10 -0
- package/dist/commands/lifecycle/doctor.mjs +317 -0
- package/dist/commands/lifecycle/doctor.mjs.map +1 -0
- package/dist/commands/lifecycle/install.d.mts +17 -0
- package/dist/commands/lifecycle/install.mjs +1077 -0
- package/dist/commands/lifecycle/install.mjs.map +1 -0
- package/dist/commands/lifecycle/migrate.d.mts +1 -0
- package/{src → dist}/commands/lifecycle/migrate.mjs +10 -11
- package/dist/commands/lifecycle/migrate.mjs.map +1 -0
- package/dist/commands/lifecycle/model.d.mts +1 -0
- package/dist/commands/lifecycle/model.mjs +22 -0
- package/dist/commands/lifecycle/model.mjs.map +1 -0
- package/dist/commands/lifecycle/paths.d.mts +1 -0
- package/dist/commands/lifecycle/paths.mjs +102 -0
- package/dist/commands/lifecycle/paths.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.d.mts +1 -0
- package/dist/commands/lifecycle/preflight.mjs +124 -0
- package/dist/commands/lifecycle/preflight.mjs.map +1 -0
- package/dist/commands/lifecycle/setup.d.mts +1 -0
- package/dist/commands/lifecycle/setup.mjs +297 -0
- package/dist/commands/lifecycle/setup.mjs.map +1 -0
- package/dist/commands/lifecycle/uninstall.d.mts +4 -0
- package/dist/commands/lifecycle/uninstall.mjs +262 -0
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -0
- package/dist/commands/manager.d.mts +3 -0
- package/{src → dist}/commands/manager.mjs +25 -32
- package/dist/commands/manager.mjs.map +1 -0
- package/dist/commands/memory/memory.d.mts +1 -0
- package/dist/commands/memory/memory.mjs +569 -0
- package/dist/commands/memory/memory.mjs.map +1 -0
- package/dist/commands/pr/pr.d.mts +33 -0
- package/dist/commands/pr/pr.mjs +296 -0
- package/dist/commands/pr/pr.mjs.map +1 -0
- package/dist/commands/report/agent-activity.d.mts +1 -0
- package/{src → dist}/commands/report/agent-activity.mjs +11 -12
- package/dist/commands/report/agent-activity.mjs.map +1 -0
- package/dist/commands/report/finalize.d.mts +4 -0
- package/{src → dist}/commands/report/finalize.mjs +25 -32
- package/dist/commands/report/finalize.mjs.map +1 -0
- package/dist/commands/report/inject-report-index.d.mts +1 -0
- package/{src → dist}/commands/report/inject-report-index.mjs +6 -7
- package/dist/commands/report/inject-report-index.mjs.map +1 -0
- package/dist/commands/report/render-final-report.d.mts +1 -0
- package/{src → dist}/commands/report/render-final-report.mjs +6 -7
- package/dist/commands/report/render-final-report.mjs.map +1 -0
- package/dist/commands/report/render-views.d.mts +1 -0
- package/{src → dist}/commands/report/render-views.mjs +6 -7
- package/dist/commands/report/render-views.mjs.map +1 -0
- package/dist/commands/report/translate.d.mts +1 -0
- package/{src → dist}/commands/report/translate.mjs +6 -7
- package/dist/commands/report/translate.mjs.map +1 -0
- package/dist/lib/helper-scripts.d.mts +3 -0
- package/{src → dist}/lib/helper-scripts.mjs +11 -14
- package/dist/lib/helper-scripts.mjs.map +1 -0
- package/dist/lib/host-registry-client.d.mts +25 -0
- package/dist/lib/host-registry-client.mjs +152 -0
- package/dist/lib/host-registry-client.mjs.map +1 -0
- package/dist/lib/install-assets.d.mts +8 -0
- package/{src → dist}/lib/install-assets.mjs +17 -20
- package/dist/lib/install-assets.mjs.map +1 -0
- package/dist/lib/okstra-dirs.d.mts +4 -0
- package/{src → dist}/lib/okstra-dirs.mjs +3 -6
- package/dist/lib/okstra-dirs.mjs.map +1 -0
- package/dist/lib/paths.d.mts +4 -0
- package/dist/lib/paths.mjs +78 -0
- package/dist/lib/paths.mjs.map +1 -0
- package/dist/lib/proc.d.mts +3 -0
- package/dist/lib/proc.mjs +31 -0
- package/dist/lib/proc.mjs.map +1 -0
- package/dist/lib/python-helper.d.mts +8 -0
- package/dist/lib/python-helper.mjs +123 -0
- package/dist/lib/python-helper.mjs.map +1 -0
- package/dist/lib/runtime-manifest.d.mts +7 -0
- package/dist/lib/runtime-manifest.mjs +73 -0
- package/dist/lib/runtime-manifest.mjs.map +1 -0
- package/dist/lib/skill-catalog.d.mts +4 -0
- package/dist/lib/skill-catalog.mjs +50 -0
- package/dist/lib/skill-catalog.mjs.map +1 -0
- package/dist/lib/types.d.mts +129 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/lib/types.mjs.map +1 -0
- package/dist/lib/version.d.mts +2 -0
- package/dist/lib/version.mjs +27 -0
- package/dist/lib/version.mjs.map +1 -0
- package/docs/architecture/storage-model.md +6 -2
- package/docs/architecture.md +29 -20
- package/docs/cli.md +40 -16
- package/docs/contributor-change-matrix.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -3
- package/docs/for-ai/skills/okstra-rollup.md +1 -0
- package/docs/project-structure-overview.md +72 -62
- package/docs/task-process/README.md +12 -8
- package/docs/task-process/common-flow.md +13 -16
- package/docs/task-process/error-analysis.md +9 -10
- package/docs/task-process/final-verification.md +7 -7
- package/docs/task-process/implementation-planning.md +9 -9
- package/docs/task-process/implementation.md +6 -6
- package/docs/task-process/release-handoff.md +8 -7
- package/docs/task-process/requirements-discovery.md +8 -8
- package/package.json +11 -5
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +2 -1
- package/runtime/agents/workers/report-writer-worker.md +3 -1
- package/runtime/agents/workers/translator-worker.md +3 -1
- package/runtime/bin/lib/okstra/cli.sh +12 -0
- package/runtime/bin/lib/okstra/globals.sh +3 -0
- package/runtime/bin/lib/okstra/interactive.sh +12 -6
- package/runtime/bin/lib/okstra/usage.sh +7 -6
- package/runtime/bin/okstra-error-log.py +27 -0
- package/runtime/bin/okstra-provider-exec.py +217 -5
- package/runtime/bin/okstra-spawn-followups.py +4 -2
- package/runtime/bin/okstra.sh +10 -0
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/lead/context-loader.md +1 -2
- package/runtime/prompts/lead/convergence.md +27 -18
- package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
- package/runtime/prompts/lead/plan-body-verification.md +10 -3
- package/runtime/prompts/lead/report-writer.md +15 -1
- package/runtime/prompts/lead/team-contract.md +17 -13
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +0 -3
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -7
- package/runtime/prompts/profiles/change-impact-analysis.md +20 -0
- package/runtime/prompts/profiles/error-analysis.md +27 -1
- package/runtime/prompts/profiles/feature-analysis.md +20 -0
- package/runtime/prompts/profiles/final-verification.md +20 -1
- package/runtime/prompts/profiles/implementation-option-selection.md +20 -0
- package/runtime/prompts/profiles/implementation-planning.md +26 -0
- package/runtime/prompts/profiles/implementation.md +19 -0
- package/runtime/prompts/profiles/improvement-discovery.md +21 -1
- package/runtime/prompts/profiles/project-analysis.md +20 -0
- package/runtime/prompts/profiles/release-handoff.md +4 -0
- package/runtime/prompts/profiles/requirements-discovery.md +27 -1
- package/runtime/prompts/wizard/prompts.ko.json +35 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +95 -25
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
- package/runtime/python/okstra_ctl/adapters/runtime/__init__.py +1 -0
- package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +27 -0
- package/runtime/python/okstra_ctl/adapters/runtime/cli_wrapper.py +49 -0
- package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +74 -0
- package/runtime/python/okstra_ctl/agent_activity.py +5 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
- package/runtime/python/okstra_ctl/application/open_worker.py +29 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
- package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
- package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
- package/runtime/python/okstra_ctl/convergence.py +409 -3
- package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
- package/runtime/python/okstra_ctl/convergence_store.py +170 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +1204 -155
- package/runtime/python/okstra_ctl/dispatch_state.py +696 -11
- package/runtime/python/okstra_ctl/doctor.py +14 -0
- package/runtime/python/okstra_ctl/domain/host.py +34 -1
- package/runtime/python/okstra_ctl/domain/provider.py +212 -6
- package/runtime/python/okstra_ctl/domain/role.py +125 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +18 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +34 -1
- package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +44 -0
- package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
- package/runtime/python/okstra_ctl/error_log_write.py +9 -0
- package/runtime/python/okstra_ctl/error_report.py +21 -1
- package/runtime/python/okstra_ctl/execution_identity.py +529 -0
- package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
- package/runtime/python/okstra_ctl/implementation_outcome.py +21 -5
- package/runtime/python/okstra_ctl/index.py +6 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
- package/runtime/python/okstra_ctl/legacy_model_selection.py +578 -0
- package/runtime/python/okstra_ctl/manager_sync.py +4 -1
- package/runtime/python/okstra_ctl/model_cli.py +278 -0
- package/runtime/python/okstra_ctl/model_defaults.py +46 -0
- package/runtime/python/okstra_ctl/model_discovery.py +26 -7
- package/runtime/python/okstra_ctl/model_pool.py +501 -0
- package/runtime/python/okstra_ctl/models.py +47 -27
- package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
- package/runtime/python/okstra_ctl/next_phase.py +236 -0
- package/runtime/python/okstra_ctl/pane_title.py +151 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
- package/runtime/python/okstra_ctl/ports/worker_runtime.py +26 -0
- package/runtime/python/okstra_ctl/recap.py +4 -1
- package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
- package/runtime/python/okstra_ctl/render.py +124 -57
- package/runtime/python/okstra_ctl/render_final_report.py +19 -0
- package/runtime/python/okstra_ctl/report_contract.py +39 -0
- package/runtime/python/okstra_ctl/report_finalize.py +16 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -0
- package/runtime/python/okstra_ctl/role_requirements.py +210 -0
- package/runtime/python/okstra_ctl/rollup.py +4 -1
- package/runtime/python/okstra_ctl/run.py +1210 -258
- package/runtime/python/okstra_ctl/run_index_row.py +7 -1
- package/runtime/python/okstra_ctl/stage_fix_carry.py +17 -1
- package/runtime/python/okstra_ctl/team.py +14 -18
- package/runtime/python/okstra_ctl/time_report.py +38 -4
- package/runtime/python/okstra_ctl/usage_identity.py +86 -0
- package/runtime/python/okstra_ctl/usage_report.py +15 -3
- package/runtime/python/okstra_ctl/wizard.py +1449 -66
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
- package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -2
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +40 -9
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +16 -1
- package/runtime/python/okstra_ctl/worker_request.py +7 -1
- package/runtime/python/okstra_ctl/worker_runner.py +130 -5
- package/runtime/python/okstra_ctl/workers.py +7 -5
- package/runtime/python/okstra_ctl/workflow.py +18 -32
- package/runtime/python/okstra_ctl/worktree.py +3 -3
- package/runtime/python/okstra_ctl/worktree_registry.py +5 -4
- package/runtime/python/okstra_ctl/write_policy.py +571 -0
- package/runtime/python/okstra_project/state.py +54 -6
- package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
- package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
- package/runtime/schemas/final-report-v2.0.schema.json +68 -5
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -0
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +15 -13
- package/runtime/skills/okstra-rollup/SKILL.md +1 -0
- package/runtime/skills/okstra-run/SKILL.md +14 -14
- package/runtime/templates/implementation-worker-preamble.md +3 -18
- package/runtime/templates/project-docs/task-index.template.md +0 -1
- package/runtime/templates/report-writer-prompt-preamble.md +2 -0
- package/runtime/templates/reports/final-report-v2.template.md +7 -1
- package/runtime/templates/reports/html/base.template.html +21 -0
- package/runtime/templates/reports/html/i18n/en.json +4 -0
- package/runtime/templates/reports/html/i18n/ko.json +4 -0
- package/runtime/templates/reports/html/macros/forms.html +5 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +1 -1
- package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
- package/runtime/templates/worker-prompt-preamble.md +6 -35
- package/runtime/validators/lib/fixtures.sh +49 -16
- package/runtime/validators/validate-run.py +486 -134
- package/runtime/validators/validate_session_conformance.py +25 -7
- package/src/cli-registry.mjs +0 -539
- package/src/commands/execute/codex-dispatch.mjs +0 -10
- package/src/commands/execute/plan-items.mjs +0 -9
- package/src/commands/execute/plan-validate.mjs +0 -68
- package/src/commands/execute/run.mjs +0 -208
- package/src/commands/execute/wizard.mjs +0 -167
- package/src/commands/inspect/stage-map.mjs +0 -98
- package/src/commands/inspect/task-list.mjs +0 -97
- package/src/commands/inspect/task-show.mjs +0 -100
- package/src/commands/lifecycle/check-project.mjs +0 -187
- package/src/commands/lifecycle/config.mjs +0 -385
- package/src/commands/lifecycle/doctor.mjs +0 -308
- package/src/commands/lifecycle/install.mjs +0 -1170
- package/src/commands/lifecycle/paths.mjs +0 -104
- package/src/commands/lifecycle/preflight.mjs +0 -125
- package/src/commands/lifecycle/setup.mjs +0 -333
- package/src/commands/lifecycle/uninstall.mjs +0 -279
- package/src/commands/memory/memory.mjs +0 -524
- package/src/commands/pr/pr.mjs +0 -261
- package/src/lib/host-registry-client.mjs +0 -176
- package/src/lib/paths.mjs +0 -86
- package/src/lib/proc.mjs +0 -31
- package/src/lib/python-helper.mjs +0 -142
- package/src/lib/runtime-manifest.mjs +0 -70
- package/src/lib/skill-catalog.mjs +0 -53
- package/src/lib/version.mjs +0 -20
- /package/{src → dist}/commands/pr/default.md +0 -0
|
@@ -18,6 +18,7 @@ from .agent_invocation import (
|
|
|
18
18
|
AgentInvocationRequest,
|
|
19
19
|
agent_model_assignment_from_payload,
|
|
20
20
|
compose_agent_prompt,
|
|
21
|
+
invocation_execution_identity_from_manifest,
|
|
21
22
|
prepare_agent_invocation,
|
|
22
23
|
verify_agent_invocation,
|
|
23
24
|
)
|
|
@@ -127,6 +128,11 @@ _MODEL_ASSIGNMENT_KEYS = {
|
|
|
127
128
|
"hostRuntime",
|
|
128
129
|
"hostModelValue",
|
|
129
130
|
}
|
|
131
|
+
_V2_EXECUTION_IDENTITY_KEYS = {
|
|
132
|
+
"participantRef",
|
|
133
|
+
"roleExecutionRef",
|
|
134
|
+
"dutyId",
|
|
135
|
+
}
|
|
130
136
|
_RUN_INVOCATION_PATH_KEYS = {
|
|
131
137
|
"runManifestPath",
|
|
132
138
|
"workerPromptsDirectoryPath",
|
|
@@ -259,6 +265,26 @@ def _validate_invocation_manifest_authority(
|
|
|
259
265
|
return
|
|
260
266
|
contract = manifest.get("agentContract")
|
|
261
267
|
assignments = manifest.get("invocationAssignments")
|
|
268
|
+
schema = manifest.get("schemaVersion")
|
|
269
|
+
identity_version = manifest.get("executionIdentityVersion")
|
|
270
|
+
is_v1 = schema in (None, 1, "1", "1.0") and identity_version is None
|
|
271
|
+
is_v2 = schema == "2.0" and identity_version == 2
|
|
272
|
+
if not is_v1 and not is_v2:
|
|
273
|
+
raise InitialPromptMaterializationError(
|
|
274
|
+
"required_input_missing",
|
|
275
|
+
"invocation manifest mixes v1 and v2 execution identity",
|
|
276
|
+
)
|
|
277
|
+
allowed_assignment_shapes = {frozenset(_MODEL_ASSIGNMENT_KEYS)}
|
|
278
|
+
if is_v2 and isinstance(assignments, Mapping):
|
|
279
|
+
if any(
|
|
280
|
+
isinstance(item, Mapping)
|
|
281
|
+
and bool(set(item) & _V2_EXECUTION_IDENTITY_KEYS)
|
|
282
|
+
for item in assignments.values()
|
|
283
|
+
):
|
|
284
|
+
raise InitialPromptMaterializationError(
|
|
285
|
+
"required_input_missing",
|
|
286
|
+
"invocation assignment duplicates v2 execution identity",
|
|
287
|
+
)
|
|
262
288
|
complete = (
|
|
263
289
|
isinstance(contract, Mapping)
|
|
264
290
|
and set(contract) == _AGENT_CONTRACT_KEYS
|
|
@@ -267,7 +293,7 @@ def _validate_invocation_manifest_authority(
|
|
|
267
293
|
and bool(assignments)
|
|
268
294
|
and all(
|
|
269
295
|
isinstance(item, Mapping)
|
|
270
|
-
and
|
|
296
|
+
and frozenset(item) in allowed_assignment_shapes
|
|
271
297
|
for item in assignments.values()
|
|
272
298
|
)
|
|
273
299
|
and all(
|
|
@@ -453,6 +479,12 @@ def _agent_invocation_request(
|
|
|
453
479
|
payload = assignments.get(assignment_ref) if isinstance(assignments, Mapping) else None
|
|
454
480
|
try:
|
|
455
481
|
assignment = agent_model_assignment_from_payload(payload)
|
|
482
|
+
identity = invocation_execution_identity_from_manifest(
|
|
483
|
+
context.manifest,
|
|
484
|
+
assignment=assignment,
|
|
485
|
+
assignment_ref=assignment_ref,
|
|
486
|
+
duty_id=item.plan.duty_audience,
|
|
487
|
+
)
|
|
456
488
|
except AgentInvocationError as exc:
|
|
457
489
|
raise InitialPromptMaterializationError(
|
|
458
490
|
"required_input_missing", str(exc)
|
|
@@ -475,9 +507,10 @@ def _agent_invocation_request(
|
|
|
475
507
|
"project", _required_string(context.manifest, "teamStatePath")
|
|
476
508
|
),
|
|
477
509
|
)
|
|
510
|
+
invocation_id = _worker_invocation_id(context.manifest, item.worker.worker_id)
|
|
478
511
|
return AgentInvocationRequest(
|
|
479
|
-
invocation_id=
|
|
480
|
-
worker_id=item.worker.worker_id,
|
|
512
|
+
invocation_id=invocation_id,
|
|
513
|
+
worker_id=item.worker.worker_id if identity is None else None,
|
|
481
514
|
audience=item.plan.duty_audience,
|
|
482
515
|
assignment_ref=assignment_ref,
|
|
483
516
|
purpose=None,
|
|
@@ -493,6 +526,13 @@ def _agent_invocation_request(
|
|
|
493
526
|
prompt_path=item.final_path,
|
|
494
527
|
metadata_path=item.final_path.with_name(item.final_path.name + ".meta.json"),
|
|
495
528
|
dispatch_kind="initial",
|
|
529
|
+
participant_ref=identity.participant_ref if identity is not None else None,
|
|
530
|
+
role_execution_ref=(
|
|
531
|
+
identity.role_execution_ref if identity is not None else None
|
|
532
|
+
),
|
|
533
|
+
duty_id=identity.duty_id if identity is not None else None,
|
|
534
|
+
invocation_ref=invocation_id if identity is not None else None,
|
|
535
|
+
attempt=1,
|
|
496
536
|
)
|
|
497
537
|
|
|
498
538
|
|
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""Normalize canonical role selections and legacy provider CLI inputs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Mapping, Sequence
|
|
8
|
+
|
|
9
|
+
from .domain.host import (
|
|
10
|
+
CurrentSessionModelAttestation,
|
|
11
|
+
HostSessionContext,
|
|
12
|
+
)
|
|
13
|
+
from .domain.role import RoleCatalogError, normalize_role
|
|
14
|
+
from .role_requirements import RoleProfile, RoleRequirement
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ModelSelectionInputError(ValueError):
|
|
18
|
+
"""Raised before prepare side effects when role selection is invalid."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class CanonicalModelSelection:
|
|
23
|
+
role_counts: Mapping[str, int]
|
|
24
|
+
role_models: Mapping[str, tuple[str, ...]]
|
|
25
|
+
provider_constraints: Mapping[str, tuple[str, ...]]
|
|
26
|
+
legacy_model_values: Mapping[str, Mapping[str, str]]
|
|
27
|
+
legacy_sources: tuple[str, ...]
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
model_values = {
|
|
31
|
+
role: MappingProxyType(dict(values))
|
|
32
|
+
for role, values in self.legacy_model_values.items()
|
|
33
|
+
}
|
|
34
|
+
object.__setattr__(
|
|
35
|
+
self,
|
|
36
|
+
"role_counts",
|
|
37
|
+
MappingProxyType(dict(self.role_counts)),
|
|
38
|
+
)
|
|
39
|
+
object.__setattr__(
|
|
40
|
+
self,
|
|
41
|
+
"role_models",
|
|
42
|
+
MappingProxyType(dict(self.role_models)),
|
|
43
|
+
)
|
|
44
|
+
object.__setattr__(
|
|
45
|
+
self,
|
|
46
|
+
"provider_constraints",
|
|
47
|
+
MappingProxyType(dict(self.provider_constraints)),
|
|
48
|
+
)
|
|
49
|
+
object.__setattr__(
|
|
50
|
+
self,
|
|
51
|
+
"legacy_model_values",
|
|
52
|
+
MappingProxyType(model_values),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def normalize_model_selection_inputs(
|
|
57
|
+
*,
|
|
58
|
+
profile: RoleProfile,
|
|
59
|
+
role_counts_raw: Sequence[str],
|
|
60
|
+
role_models_raw: Sequence[str],
|
|
61
|
+
workers: str,
|
|
62
|
+
worker_model: str,
|
|
63
|
+
critic: str,
|
|
64
|
+
executor: str,
|
|
65
|
+
lead_provider: str,
|
|
66
|
+
lead_model: str,
|
|
67
|
+
report_writer_provider: str,
|
|
68
|
+
report_writer_model: str,
|
|
69
|
+
provider_models: Mapping[str, str],
|
|
70
|
+
) -> CanonicalModelSelection:
|
|
71
|
+
"""Return one ordered role-selection view for canonical and legacy inputs."""
|
|
72
|
+
counts = _parse_role_counts(role_counts_raw)
|
|
73
|
+
models = _parse_role_models(role_models_raw)
|
|
74
|
+
legacy = _legacy_selection(
|
|
75
|
+
profile=profile,
|
|
76
|
+
workers=workers,
|
|
77
|
+
worker_model=worker_model,
|
|
78
|
+
critic=critic,
|
|
79
|
+
executor=executor,
|
|
80
|
+
lead_provider=lead_provider,
|
|
81
|
+
lead_model=lead_model,
|
|
82
|
+
report_writer_provider=report_writer_provider,
|
|
83
|
+
report_writer_model=report_writer_model,
|
|
84
|
+
provider_models=provider_models,
|
|
85
|
+
)
|
|
86
|
+
# 같은 값이면 정본을 남기고 레거시를 접는다. 다르면 prepare 전에 거부.
|
|
87
|
+
legacy = _reconcile_legacy_with_canonical(counts, models, legacy)
|
|
88
|
+
merged_counts = {**counts, **legacy.role_counts}
|
|
89
|
+
merged_models = {**models, **legacy.role_models}
|
|
90
|
+
_validate_selection(profile, merged_counts, merged_models, counts)
|
|
91
|
+
return CanonicalModelSelection(
|
|
92
|
+
merged_counts,
|
|
93
|
+
merged_models,
|
|
94
|
+
legacy.provider_constraints,
|
|
95
|
+
legacy.legacy_model_values,
|
|
96
|
+
legacy.legacy_sources,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _parse_role_counts(raw_values: Sequence[str]) -> dict[str, int]:
|
|
101
|
+
counts: dict[str, int] = {}
|
|
102
|
+
for raw in raw_values:
|
|
103
|
+
role, value = _split_role_value(raw, "--role-count")
|
|
104
|
+
if role in counts:
|
|
105
|
+
raise ModelSelectionInputError(f"duplicate --role-count role: {role}")
|
|
106
|
+
try:
|
|
107
|
+
count = int(value)
|
|
108
|
+
except ValueError as exc:
|
|
109
|
+
raise ModelSelectionInputError(
|
|
110
|
+
f"--role-count requires role=<non-negative integer>: {raw!r}"
|
|
111
|
+
) from exc
|
|
112
|
+
if count < 0 or str(count) != value:
|
|
113
|
+
raise ModelSelectionInputError(
|
|
114
|
+
f"--role-count requires role=<non-negative integer>: {raw!r}"
|
|
115
|
+
)
|
|
116
|
+
counts[role] = count
|
|
117
|
+
return counts
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _parse_role_models(raw_values: Sequence[str]) -> dict[str, tuple[str, ...]]:
|
|
121
|
+
grouped: dict[str, list[str]] = {}
|
|
122
|
+
for raw in raw_values:
|
|
123
|
+
role, model_ref = _split_role_value(raw, "--role-model")
|
|
124
|
+
grouped.setdefault(role, []).append(model_ref)
|
|
125
|
+
return {role: tuple(values) for role, values in grouped.items()}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _split_role_value(raw: str, option: str) -> tuple[str, str]:
|
|
129
|
+
role_raw, separator, value = raw.partition("=")
|
|
130
|
+
role_raw = role_raw.strip()
|
|
131
|
+
value = value.strip()
|
|
132
|
+
if not separator or not role_raw or not value:
|
|
133
|
+
raise ModelSelectionInputError(f"{option} requires role=value: {raw!r}")
|
|
134
|
+
try:
|
|
135
|
+
role = normalize_role(role_raw)
|
|
136
|
+
except RoleCatalogError as exc:
|
|
137
|
+
raise ModelSelectionInputError(str(exc)) from exc
|
|
138
|
+
return role, value
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _legacy_selection(
|
|
142
|
+
*,
|
|
143
|
+
profile: RoleProfile,
|
|
144
|
+
workers: str,
|
|
145
|
+
worker_model: str,
|
|
146
|
+
critic: str,
|
|
147
|
+
executor: str,
|
|
148
|
+
lead_provider: str,
|
|
149
|
+
lead_model: str,
|
|
150
|
+
report_writer_provider: str,
|
|
151
|
+
report_writer_model: str,
|
|
152
|
+
provider_models: Mapping[str, str],
|
|
153
|
+
) -> CanonicalModelSelection:
|
|
154
|
+
counts: dict[str, int] = {}
|
|
155
|
+
constraints: dict[str, tuple[str, ...]] = {}
|
|
156
|
+
values: dict[str, dict[str, str]] = {}
|
|
157
|
+
sources: list[str] = []
|
|
158
|
+
needs_initial = bool(
|
|
159
|
+
workers.strip()
|
|
160
|
+
or worker_model.strip()
|
|
161
|
+
or any(model.strip() for model in provider_models.values())
|
|
162
|
+
)
|
|
163
|
+
initial = (
|
|
164
|
+
_initial_cross_verification_requirement(profile)
|
|
165
|
+
if needs_initial
|
|
166
|
+
else None
|
|
167
|
+
)
|
|
168
|
+
if workers.strip() and initial is not None:
|
|
169
|
+
providers = _legacy_worker_providers(workers)
|
|
170
|
+
_validate_legacy_workers(initial, providers)
|
|
171
|
+
counts[initial.role] = len(providers)
|
|
172
|
+
constraints[initial.role] = providers
|
|
173
|
+
report_writer = _requirement(profile, "report-writer")
|
|
174
|
+
if report_writer is not None:
|
|
175
|
+
counts["report-writer"] = report_writer.recommended_count
|
|
176
|
+
sources.append("--workers")
|
|
177
|
+
worker_values = _parse_worker_models(worker_model, provider_models)
|
|
178
|
+
if worker_values and initial is not None:
|
|
179
|
+
values[initial.role] = worker_values
|
|
180
|
+
sources.append("worker-model")
|
|
181
|
+
_add_provider_selection(
|
|
182
|
+
constraints, values, sources, "leader", lead_provider, lead_model, "lead"
|
|
183
|
+
)
|
|
184
|
+
_add_provider_selection(
|
|
185
|
+
constraints,
|
|
186
|
+
values,
|
|
187
|
+
sources,
|
|
188
|
+
"report-writer",
|
|
189
|
+
report_writer_provider,
|
|
190
|
+
report_writer_model,
|
|
191
|
+
"report-writer",
|
|
192
|
+
)
|
|
193
|
+
_add_simple_legacy_roles(counts, constraints, sources, critic, executor)
|
|
194
|
+
return CanonicalModelSelection(counts, {}, constraints, values, tuple(sources))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _initial_cross_verification_requirement(profile: RoleProfile) -> RoleRequirement:
|
|
198
|
+
for requirement in profile.roles:
|
|
199
|
+
if not requirement.dynamic and requirement.role in {
|
|
200
|
+
"analyser", "designer", "planner", "verifier"
|
|
201
|
+
}:
|
|
202
|
+
return requirement
|
|
203
|
+
raise ModelSelectionInputError(
|
|
204
|
+
"profile has no initial cross-verification role for legacy --workers"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _legacy_worker_providers(workers: str) -> tuple[str, ...]:
|
|
209
|
+
tokens = tuple(
|
|
210
|
+
token.strip().lower() for token in workers.split(",") if token.strip()
|
|
211
|
+
)
|
|
212
|
+
return tuple(token for token in tokens if token != "report-writer")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _validate_legacy_workers(
|
|
216
|
+
requirement: RoleRequirement,
|
|
217
|
+
providers: tuple[str, ...],
|
|
218
|
+
) -> None:
|
|
219
|
+
# minDistinctProviders 제거. 모델 참조 중복 거부는 Task 2.
|
|
220
|
+
if (
|
|
221
|
+
len(providers) < requirement.min_count
|
|
222
|
+
or len(providers) > requirement.max_count
|
|
223
|
+
):
|
|
224
|
+
raise ModelSelectionInputError(
|
|
225
|
+
f"legacy --workers selects {len(providers)} {requirement.role} instances; "
|
|
226
|
+
f"profile requires {requirement.min_count}..{requirement.max_count}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _parse_worker_models(
|
|
231
|
+
worker_model: str,
|
|
232
|
+
provider_models: Mapping[str, str],
|
|
233
|
+
) -> dict[str, str]:
|
|
234
|
+
values = {
|
|
235
|
+
provider.strip().lower(): model.strip()
|
|
236
|
+
for provider, model in provider_models.items()
|
|
237
|
+
if model.strip()
|
|
238
|
+
}
|
|
239
|
+
for item in worker_model.split(","):
|
|
240
|
+
if not item.strip():
|
|
241
|
+
continue
|
|
242
|
+
provider, separator, model = item.partition("=")
|
|
243
|
+
provider = provider.strip().lower()
|
|
244
|
+
model = model.strip()
|
|
245
|
+
if not separator or not provider or not model:
|
|
246
|
+
raise ModelSelectionInputError(
|
|
247
|
+
"--worker-model must use provider=model entries separated by commas"
|
|
248
|
+
)
|
|
249
|
+
if provider in values:
|
|
250
|
+
raise ModelSelectionInputError(
|
|
251
|
+
f"duplicate legacy worker model provider: {provider}"
|
|
252
|
+
)
|
|
253
|
+
values[provider] = model
|
|
254
|
+
return values
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _add_provider_selection(
|
|
258
|
+
constraints: dict[str, tuple[str, ...]],
|
|
259
|
+
values: dict[str, dict[str, str]],
|
|
260
|
+
sources: list[str],
|
|
261
|
+
role: str,
|
|
262
|
+
provider_raw: str,
|
|
263
|
+
model_raw: str,
|
|
264
|
+
source: str,
|
|
265
|
+
) -> None:
|
|
266
|
+
provider = provider_raw.strip().lower()
|
|
267
|
+
model = model_raw.strip()
|
|
268
|
+
if not provider and not model:
|
|
269
|
+
return
|
|
270
|
+
constraints[role] = (provider,)
|
|
271
|
+
if model:
|
|
272
|
+
values[role] = {provider: model}
|
|
273
|
+
sources.append(source)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _add_simple_legacy_roles(
|
|
277
|
+
counts: dict[str, int],
|
|
278
|
+
constraints: dict[str, tuple[str, ...]],
|
|
279
|
+
sources: list[str],
|
|
280
|
+
critic_raw: str,
|
|
281
|
+
executor_raw: str,
|
|
282
|
+
) -> None:
|
|
283
|
+
critic = critic_raw.strip().lower()
|
|
284
|
+
if critic:
|
|
285
|
+
counts["critic"] = 0 if critic == "off" else 1
|
|
286
|
+
if critic != "off":
|
|
287
|
+
constraints["critic"] = (critic,)
|
|
288
|
+
sources.append("critic")
|
|
289
|
+
executor = executor_raw.strip().lower()
|
|
290
|
+
if executor:
|
|
291
|
+
constraints["implementer"] = (executor,)
|
|
292
|
+
sources.append("executor")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _reconcile_legacy_with_canonical(
|
|
296
|
+
canonical_counts: Mapping[str, int],
|
|
297
|
+
canonical_models: Mapping[str, tuple[str, ...]],
|
|
298
|
+
legacy: CanonicalModelSelection,
|
|
299
|
+
) -> CanonicalModelSelection:
|
|
300
|
+
"""충돌하는 레거시 입력을 거부하고, 같은 값 겹침은 정본만 남긴다."""
|
|
301
|
+
canonical_roles = set(canonical_counts) | set(canonical_models)
|
|
302
|
+
legacy_roles = (
|
|
303
|
+
set(legacy.role_counts)
|
|
304
|
+
| set(legacy.provider_constraints)
|
|
305
|
+
| set(legacy.legacy_model_values)
|
|
306
|
+
)
|
|
307
|
+
overlap = sorted(canonical_roles & legacy_roles)
|
|
308
|
+
if not overlap:
|
|
309
|
+
return legacy
|
|
310
|
+
|
|
311
|
+
counts = dict(legacy.role_counts)
|
|
312
|
+
constraints = dict(legacy.provider_constraints)
|
|
313
|
+
values = {
|
|
314
|
+
role: dict(models)
|
|
315
|
+
for role, models in legacy.legacy_model_values.items()
|
|
316
|
+
}
|
|
317
|
+
for role in overlap:
|
|
318
|
+
if not _legacy_matches_canonical(
|
|
319
|
+
role,
|
|
320
|
+
canonical_counts,
|
|
321
|
+
canonical_models,
|
|
322
|
+
legacy,
|
|
323
|
+
):
|
|
324
|
+
raise ModelSelectionInputError(
|
|
325
|
+
"canonical and legacy model selections conflict for role "
|
|
326
|
+
f"{role!r}"
|
|
327
|
+
)
|
|
328
|
+
# 정본 모델이 있으면 레거시 공급자 제약을 접어 materialize 덮어쓰기를 막는다.
|
|
329
|
+
if role in canonical_models:
|
|
330
|
+
constraints.pop(role, None)
|
|
331
|
+
values.pop(role, None)
|
|
332
|
+
if role in canonical_counts:
|
|
333
|
+
counts.pop(role, None)
|
|
334
|
+
return CanonicalModelSelection(
|
|
335
|
+
counts,
|
|
336
|
+
{},
|
|
337
|
+
constraints,
|
|
338
|
+
values,
|
|
339
|
+
legacy.legacy_sources,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _legacy_matches_canonical(
|
|
344
|
+
role: str,
|
|
345
|
+
canonical_counts: Mapping[str, int],
|
|
346
|
+
canonical_models: Mapping[str, tuple[str, ...]],
|
|
347
|
+
legacy: CanonicalModelSelection,
|
|
348
|
+
) -> bool:
|
|
349
|
+
"""레거시(--workers 등)가 변환 후 정본과 같은 값으로 수렴하는지 본다."""
|
|
350
|
+
leg_count = legacy.role_counts.get(role)
|
|
351
|
+
can_count = canonical_counts.get(role)
|
|
352
|
+
if can_count is not None and leg_count is not None and can_count != leg_count:
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
leg_providers = legacy.provider_constraints.get(role)
|
|
356
|
+
can_models = canonical_models.get(role)
|
|
357
|
+
if can_models is not None and leg_providers is not None:
|
|
358
|
+
can_providers = tuple(
|
|
359
|
+
_provider_from_model_ref(model_ref) for model_ref in can_models
|
|
360
|
+
)
|
|
361
|
+
if can_providers != tuple(
|
|
362
|
+
provider.lower() for provider in leg_providers
|
|
363
|
+
):
|
|
364
|
+
return False
|
|
365
|
+
# 워커 목록은 칸 전체 순서다. 칸 수가 다르면 다른 선택이다.
|
|
366
|
+
if len(can_models) != len(leg_providers):
|
|
367
|
+
return False
|
|
368
|
+
leg_models = legacy.legacy_model_values.get(role, {})
|
|
369
|
+
for provider, model_ref in zip(leg_providers, can_models):
|
|
370
|
+
explicit = leg_models.get(provider)
|
|
371
|
+
if not explicit:
|
|
372
|
+
continue
|
|
373
|
+
model_id = (
|
|
374
|
+
model_ref.split("/", 1)[1] if "/" in model_ref else model_ref
|
|
375
|
+
)
|
|
376
|
+
if explicit != model_id and model_ref != f"{provider}/{explicit}":
|
|
377
|
+
return False
|
|
378
|
+
return True
|
|
379
|
+
|
|
380
|
+
if can_count is not None and leg_providers is not None:
|
|
381
|
+
if can_count != len(leg_providers):
|
|
382
|
+
return False
|
|
383
|
+
if (
|
|
384
|
+
can_models is not None
|
|
385
|
+
and leg_count is not None
|
|
386
|
+
and leg_providers is None
|
|
387
|
+
and len(can_models) > leg_count
|
|
388
|
+
):
|
|
389
|
+
return False
|
|
390
|
+
return True
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _provider_from_model_ref(model_ref: str) -> str:
|
|
394
|
+
provider, separator, _model = model_ref.partition("/")
|
|
395
|
+
if separator:
|
|
396
|
+
return provider.strip().lower()
|
|
397
|
+
return model_ref.strip().lower()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _validate_selection(
|
|
401
|
+
profile: RoleProfile,
|
|
402
|
+
counts: Mapping[str, int],
|
|
403
|
+
models: Mapping[str, tuple[str, ...]],
|
|
404
|
+
explicit_counts: Mapping[str, int],
|
|
405
|
+
) -> None:
|
|
406
|
+
requirements = {row.role: row for row in profile.roles if not row.dynamic}
|
|
407
|
+
requirements["leader"] = RoleRequirement("leader", 1, 1, 1, "lead")
|
|
408
|
+
for role in sorted(set(counts) | set(models)):
|
|
409
|
+
requirement = requirements.get(role)
|
|
410
|
+
if requirement is None:
|
|
411
|
+
raise ModelSelectionInputError(
|
|
412
|
+
f"role {role!r} is not static in the profile"
|
|
413
|
+
)
|
|
414
|
+
_validate_role_count(
|
|
415
|
+
role,
|
|
416
|
+
counts,
|
|
417
|
+
requirement,
|
|
418
|
+
explicit=role in explicit_counts,
|
|
419
|
+
)
|
|
420
|
+
selected_count = counts.get(role, requirement.recommended_count)
|
|
421
|
+
model_count = len(models.get(role, ()))
|
|
422
|
+
if model_count > selected_count:
|
|
423
|
+
if role not in explicit_counts:
|
|
424
|
+
raise ModelSelectionInputError(
|
|
425
|
+
f"set --role-count {role}={model_count} before assigning "
|
|
426
|
+
f"{model_count} models"
|
|
427
|
+
)
|
|
428
|
+
raise ModelSelectionInputError(
|
|
429
|
+
f"role {role!r} has {model_count} models for {selected_count} instances"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _validate_role_count(
|
|
434
|
+
role: str,
|
|
435
|
+
counts: Mapping[str, int],
|
|
436
|
+
requirement: RoleRequirement,
|
|
437
|
+
*,
|
|
438
|
+
explicit: bool,
|
|
439
|
+
) -> None:
|
|
440
|
+
if role not in counts:
|
|
441
|
+
return
|
|
442
|
+
count = counts[role]
|
|
443
|
+
fixed = requirement.min_count == requirement.max_count
|
|
444
|
+
if explicit and fixed:
|
|
445
|
+
raise ModelSelectionInputError(
|
|
446
|
+
f"role {role!r} has a fixed quantity and cannot receive --role-count"
|
|
447
|
+
)
|
|
448
|
+
if fixed and count != requirement.min_count:
|
|
449
|
+
raise ModelSelectionInputError(
|
|
450
|
+
f"role {role!r} has a fixed quantity of {requirement.min_count}"
|
|
451
|
+
)
|
|
452
|
+
if count < requirement.min_count or count > requirement.max_count:
|
|
453
|
+
raise ModelSelectionInputError(
|
|
454
|
+
f"role {role!r} count must be in "
|
|
455
|
+
f"{requirement.min_count}..{requirement.max_count}: {count}"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _requirement(profile: RoleProfile, role: str) -> RoleRequirement | None:
|
|
460
|
+
return next((row for row in profile.roles if row.role == role), None)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def serialize_host_session_context(context: HostSessionContext) -> str:
|
|
464
|
+
"""Serialize host-owned session facts as stable canonical JSON."""
|
|
465
|
+
current = context.current_model
|
|
466
|
+
payload = {
|
|
467
|
+
"availableFunctions": sorted(context.available_functions),
|
|
468
|
+
"currentModel": {
|
|
469
|
+
"effort": current.effort,
|
|
470
|
+
"level": current.level,
|
|
471
|
+
"normalizedModelRef": current.normalized_model_ref,
|
|
472
|
+
"observedModel": current.observed_model,
|
|
473
|
+
"providerId": current.provider_id,
|
|
474
|
+
"source": current.source,
|
|
475
|
+
},
|
|
476
|
+
"entryMode": context.entry_mode,
|
|
477
|
+
"hostId": context.host_id,
|
|
478
|
+
"interactionSurface": context.interaction_surface,
|
|
479
|
+
}
|
|
480
|
+
return json.dumps(
|
|
481
|
+
payload,
|
|
482
|
+
ensure_ascii=False,
|
|
483
|
+
separators=(",", ":"),
|
|
484
|
+
sort_keys=True,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def deserialize_host_session_context(payload: str) -> HostSessionContext:
|
|
489
|
+
"""Deserialize only the shared context schema; never infer model identity."""
|
|
490
|
+
if not payload:
|
|
491
|
+
return HostSessionContext(
|
|
492
|
+
host_id="",
|
|
493
|
+
entry_mode="spawn-process",
|
|
494
|
+
available_functions=frozenset(),
|
|
495
|
+
interaction_surface="unavailable",
|
|
496
|
+
current_model=CurrentSessionModelAttestation.unknown(""),
|
|
497
|
+
)
|
|
498
|
+
try:
|
|
499
|
+
raw = json.loads(payload)
|
|
500
|
+
except json.JSONDecodeError as exc:
|
|
501
|
+
raise ModelSelectionInputError("invalid host session context JSON") from exc
|
|
502
|
+
if not isinstance(raw, dict):
|
|
503
|
+
raise ModelSelectionInputError("host session context must be a JSON object")
|
|
504
|
+
current = _deserialize_attestation(raw.get("currentModel"))
|
|
505
|
+
return HostSessionContext(
|
|
506
|
+
host_id=_required_string(raw, "hostId"),
|
|
507
|
+
entry_mode=_enum(raw, "entryMode", {"spawn-process", "current-session"}),
|
|
508
|
+
available_functions=frozenset(_string_list(raw, "availableFunctions")),
|
|
509
|
+
interaction_surface=_required_string(raw, "interactionSurface"),
|
|
510
|
+
current_model=current,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _deserialize_attestation(raw: object) -> CurrentSessionModelAttestation:
|
|
515
|
+
if raw is None:
|
|
516
|
+
return CurrentSessionModelAttestation.unknown("")
|
|
517
|
+
if not isinstance(raw, dict):
|
|
518
|
+
raise ModelSelectionInputError("currentModel must be a JSON object")
|
|
519
|
+
level = _enum(raw, "level", {"exact", "channel", "unknown"})
|
|
520
|
+
source = _enum(
|
|
521
|
+
raw,
|
|
522
|
+
"source",
|
|
523
|
+
{"host-session-metadata", "native-api", "cli-handoff", "unavailable"},
|
|
524
|
+
)
|
|
525
|
+
observed = _nullable_string(raw, "observedModel")
|
|
526
|
+
normalized = _nullable_string(raw, "normalizedModelRef")
|
|
527
|
+
# 키 없으면 null. 호스트 어댑터가 아직 안 채운 경우를 허용한다.
|
|
528
|
+
effort = _nullable_string(raw, "effort")
|
|
529
|
+
if level == "unknown" and (observed is not None or normalized is not None):
|
|
530
|
+
raise ModelSelectionInputError("unknown currentModel must use null model fields")
|
|
531
|
+
if level != "unknown" and normalized is None:
|
|
532
|
+
raise ModelSelectionInputError(
|
|
533
|
+
"attested currentModel requires normalizedModelRef"
|
|
534
|
+
)
|
|
535
|
+
return CurrentSessionModelAttestation(
|
|
536
|
+
_required_string(raw, "providerId"),
|
|
537
|
+
observed,
|
|
538
|
+
normalized,
|
|
539
|
+
level,
|
|
540
|
+
source,
|
|
541
|
+
effort,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _required_string(raw: Mapping[str, object], key: str) -> str:
|
|
546
|
+
value = raw.get(key)
|
|
547
|
+
if not isinstance(value, str):
|
|
548
|
+
raise ModelSelectionInputError(f"host session context {key} must be a string")
|
|
549
|
+
return value
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _nullable_string(raw: Mapping[str, object], key: str) -> str | None:
|
|
553
|
+
value = raw.get(key)
|
|
554
|
+
if value is not None and not isinstance(value, str):
|
|
555
|
+
raise ModelSelectionInputError(
|
|
556
|
+
f"host session context {key} must be a string or null"
|
|
557
|
+
)
|
|
558
|
+
return value
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _enum(raw: Mapping[str, object], key: str, allowed: set[str]) -> str:
|
|
562
|
+
value = _required_string(raw, key)
|
|
563
|
+
if value not in allowed:
|
|
564
|
+
raise ModelSelectionInputError(
|
|
565
|
+
f"host session context {key} must be one of: {', '.join(sorted(allowed))}"
|
|
566
|
+
)
|
|
567
|
+
return value
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _string_list(raw: Mapping[str, object], key: str) -> tuple[str, ...]:
|
|
571
|
+
value = raw.get(key)
|
|
572
|
+
if not isinstance(value, list) or not all(
|
|
573
|
+
isinstance(item, str) for item in value
|
|
574
|
+
):
|
|
575
|
+
raise ModelSelectionInputError(
|
|
576
|
+
f"host session context {key} must be an array of strings"
|
|
577
|
+
)
|
|
578
|
+
return tuple(value)
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from okstra_project import TASKS_RELATIVE, StateError, parse_task_key, read_task_catalog, read_task_manifest, slugify
|
|
7
7
|
|
|
8
|
+
from . import next_phase
|
|
8
9
|
from .manager_paths import children_json_path, projects_json_path, snapshots_json_path, task_manifest_path
|
|
9
10
|
from .manager_store import _now_iso, _read_json, _read_json_default, _write_json
|
|
10
11
|
|
|
@@ -84,7 +85,9 @@ def _snapshot_child(project_root: Path, child: dict) -> dict:
|
|
|
84
85
|
"currentPhase": str(workflow.get("currentPhase") or ""),
|
|
85
86
|
"currentPhaseState": str(workflow.get("currentPhaseState") or ""),
|
|
86
87
|
"lastCompletedPhase": str(workflow.get("lastCompletedPhase") or ""),
|
|
87
|
-
"nextRecommendedPhase":
|
|
88
|
+
"nextRecommendedPhase": next_phase.promote(
|
|
89
|
+
workflow.get("nextRecommendedPhase")
|
|
90
|
+
),
|
|
88
91
|
"latestRunStatus": str(manifest.get("latestRunStatus") or manifest.get("currentStatus") or ""),
|
|
89
92
|
"latestReportPath": str(manifest.get("latestReportPath") or ""),
|
|
90
93
|
"finalVerdict": str(outcome.get("finalVerdict") or ""),
|