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
|
@@ -26,7 +26,7 @@ import tempfile
|
|
|
26
26
|
from dataclasses import dataclass, field
|
|
27
27
|
from datetime import datetime, timezone
|
|
28
28
|
from pathlib import Path
|
|
29
|
-
from typing import Sequence
|
|
29
|
+
from typing import Mapping, Sequence
|
|
30
30
|
|
|
31
31
|
from okstra_project import project_json_path, upsert_project_json
|
|
32
32
|
from okstra_project.state import slugify
|
|
@@ -68,19 +68,44 @@ from .material import (
|
|
|
68
68
|
from .final_report_schema import load_schema_version
|
|
69
69
|
from .final_report_paths import final_report_data_path as _final_report_data_path
|
|
70
70
|
from .lead_events import LeadEvent, append_lead_event
|
|
71
|
-
from .
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
from .assignment_environment import load_assignment_context
|
|
72
|
+
from .assignment_resolver import (
|
|
73
|
+
AssignmentContext,
|
|
74
|
+
AssignmentPlan,
|
|
75
|
+
AssignmentResolutionError,
|
|
76
|
+
ResolvedAssignment,
|
|
77
|
+
TerminalBackend,
|
|
78
|
+
resolve_assignments,
|
|
79
|
+
resolve_context_lead_provider,
|
|
80
|
+
resolve_dispatch_assignment,
|
|
81
|
+
resolve_model_selection,
|
|
74
82
|
)
|
|
75
83
|
from .domain.host import (
|
|
84
|
+
CurrentSessionModelAttestation,
|
|
76
85
|
HostCapabilityMismatch,
|
|
77
86
|
HostNotRegistered,
|
|
87
|
+
HostSessionContext,
|
|
78
88
|
ProviderUnavailable,
|
|
79
89
|
)
|
|
80
|
-
from .
|
|
90
|
+
from .domain.provider import ServedModelAttestation
|
|
91
|
+
from .execution_identity import (
|
|
92
|
+
ExecutionManifest,
|
|
93
|
+
ParticipantAssignment,
|
|
94
|
+
RoleExecution,
|
|
95
|
+
execution_label,
|
|
96
|
+
model_spec_digest,
|
|
97
|
+
)
|
|
98
|
+
from .execution_manifest import persist_static_execution_identity
|
|
99
|
+
from .legacy_model_selection import (
|
|
100
|
+
CanonicalModelSelection,
|
|
101
|
+
ModelSelectionInputError,
|
|
102
|
+
deserialize_host_session_context,
|
|
103
|
+
normalize_model_selection_inputs,
|
|
104
|
+
serialize_host_session_context,
|
|
105
|
+
)
|
|
106
|
+
from .model_defaults import ModelDefaultScopes
|
|
81
107
|
from .worker_prompt_policy import ANALYSIS_DUTY_BY_TASK_TYPE
|
|
82
108
|
from .models import (
|
|
83
|
-
ModelAssignment,
|
|
84
109
|
UnknownProviderError,
|
|
85
110
|
default_model,
|
|
86
111
|
lead_launch_argv,
|
|
@@ -89,12 +114,11 @@ from .models import (
|
|
|
89
114
|
provider_ids,
|
|
90
115
|
provider_spec,
|
|
91
116
|
provider_supports_role,
|
|
92
|
-
resolve_model_metadata,
|
|
93
117
|
)
|
|
94
118
|
from .schema_excerpt import build_schema_excerpt
|
|
95
119
|
from .registry.host_registry import default_host_registry
|
|
96
120
|
from .registry.provider_registry import default_provider_registry
|
|
97
|
-
from .
|
|
121
|
+
from .role_requirements import RoleProfile, RoleProfileError, load_role_profile
|
|
98
122
|
from .agent_invocation import (
|
|
99
123
|
AgentInstruction,
|
|
100
124
|
AgentInstructionSource,
|
|
@@ -103,6 +127,7 @@ from .agent_invocation import (
|
|
|
103
127
|
AgentInvocationError,
|
|
104
128
|
agent_model_assignment_from_payload,
|
|
105
129
|
digest_duty_catalog,
|
|
130
|
+
invocation_execution_identity_from_manifest,
|
|
106
131
|
prepare_agent_invocation,
|
|
107
132
|
verify_agent_invocation,
|
|
108
133
|
)
|
|
@@ -125,7 +150,11 @@ from .render import (
|
|
|
125
150
|
)
|
|
126
151
|
from okstra_project.dirs import okstra_home
|
|
127
152
|
|
|
128
|
-
from .dispatch_state import
|
|
153
|
+
from .dispatch_state import (
|
|
154
|
+
BACKEND_CMUX_PANE,
|
|
155
|
+
detect_terminal_backend,
|
|
156
|
+
generate_claude_session_id,
|
|
157
|
+
)
|
|
129
158
|
from .run_context import (
|
|
130
159
|
compute_and_write_run_context,
|
|
131
160
|
refresh_run_context_snapshot,
|
|
@@ -139,7 +168,6 @@ from .seeding import (
|
|
|
139
168
|
verify_installation,
|
|
140
169
|
)
|
|
141
170
|
from .session import (
|
|
142
|
-
generate_claude_session_id,
|
|
143
171
|
resolve_inproc_lead_session_id,
|
|
144
172
|
write_claude_resume_command_file,
|
|
145
173
|
)
|
|
@@ -296,6 +324,43 @@ def _validate_data_json_approval_consistency(
|
|
|
296
324
|
)
|
|
297
325
|
|
|
298
326
|
|
|
327
|
+
def _validate_approved_plan_conformance(path: Path) -> None:
|
|
328
|
+
"""승인 계획의 stage conformance 선언 형식을 승인 경계에서 판정한다.
|
|
329
|
+
|
|
330
|
+
같은 판정이 지금까지는 구현 런의 마지막 `validate-run` 에서만 나왔다. 그때는
|
|
331
|
+
워커 배치와 수렴이 이미 끝난 뒤라 런 하나를 통째로 버려야 했다 — 실측된
|
|
332
|
+
실패에서 승인 계획의 stage 2~7 이 기계 형식이 아니라 산문이었다.
|
|
333
|
+
|
|
334
|
+
선언이 없는 stage 는 건드리지 않는다(`conformance.malformed_conformance_stages`
|
|
335
|
+
참조): XOR 부재는 계획 단계의 check S11 이 이미 막고, 구현 진입 게이트도
|
|
336
|
+
validate-run 이 계속 요구한다. 여기서 다시 하면 같은 규칙이 두 곳이 된다.
|
|
337
|
+
"""
|
|
338
|
+
from .conformance import malformed_conformance_stages
|
|
339
|
+
|
|
340
|
+
loaded = _load_final_report_data_if_present(path)
|
|
341
|
+
if loaded is None:
|
|
342
|
+
return
|
|
343
|
+
data_path, data = loaded
|
|
344
|
+
bad = malformed_conformance_stages(data)
|
|
345
|
+
if not bad:
|
|
346
|
+
return
|
|
347
|
+
# 한 stage 씩 고치고 다시 막히는 왕복을 피하려면 전부 나열해야 한다 —
|
|
348
|
+
# 실패한 런에서 검증기는 stage 2 만 지목했지만 실제로는 2~7 전부였다.
|
|
349
|
+
stages = ", ".join(str(number) for number in bad)
|
|
350
|
+
raise PrepareError(
|
|
351
|
+
f"approved plan data.json has malformed conformanceTests for stage(s) "
|
|
352
|
+
f"{stages}: {data_path}\n"
|
|
353
|
+
" each declaring stage must read "
|
|
354
|
+
"`<task_root>/qa/scripts/stage-<N>.<ext> "
|
|
355
|
+
"(requires=[db|io|http|external,...])` after the "
|
|
356
|
+
"`Conformance tests: stage-<N> — ` prefix "
|
|
357
|
+
"(prompts/profiles/implementation-planning.md), or carry "
|
|
358
|
+
"`Conformance exemption: <reason>` instead.\n"
|
|
359
|
+
" re-run implementation-planning so the declaration is regenerated in "
|
|
360
|
+
"that form, then approve it."
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
299
364
|
def _set_data_json_approved_true_if_present(path: Path) -> bool:
|
|
300
365
|
loaded = _load_final_report_data_if_present(path)
|
|
301
366
|
if loaded is None:
|
|
@@ -382,6 +447,17 @@ class PrepareInputs:
|
|
|
382
447
|
evidence_inputs_raw: str = ""
|
|
383
448
|
directive: str = ""
|
|
384
449
|
workers_override: str = ""
|
|
450
|
+
role_counts_raw: tuple[str, ...] = ()
|
|
451
|
+
role_models_raw: tuple[str, ...] = ()
|
|
452
|
+
host_session_context: HostSessionContext = field(
|
|
453
|
+
default_factory=lambda: HostSessionContext(
|
|
454
|
+
host_id="",
|
|
455
|
+
entry_mode="spawn-process",
|
|
456
|
+
available_functions=frozenset(),
|
|
457
|
+
interaction_surface="unavailable",
|
|
458
|
+
current_model=CurrentSessionModelAttestation.unknown(""),
|
|
459
|
+
)
|
|
460
|
+
)
|
|
385
461
|
lead_provider: str = ""
|
|
386
462
|
lead_model: str = ""
|
|
387
463
|
claude_model: str = ""
|
|
@@ -474,6 +550,7 @@ def _validate_approved_plan(path: str) -> None:
|
|
|
474
550
|
)
|
|
475
551
|
_reject_blocking_plan_body_gate(p, body, action="approved plan validation")
|
|
476
552
|
_validate_data_json_approval_consistency(p, markdown_approved=True)
|
|
553
|
+
_validate_approved_plan_conformance(p)
|
|
477
554
|
# frontmatter approved == true 상태. §1 Clarification Items 의
|
|
478
555
|
# Blocks=approval 행이 아직 open/answered 면 승인을 무효화한다.
|
|
479
556
|
scan = scan_approval_gate(p)
|
|
@@ -665,6 +742,18 @@ def _ensure_task_directories(ctx: dict) -> None:
|
|
|
665
742
|
Path(ctx[key]).mkdir(parents=True, exist_ok=True)
|
|
666
743
|
|
|
667
744
|
|
|
745
|
+
def _role_execution_refs_from_ctx(ctx: dict) -> list[str]:
|
|
746
|
+
try:
|
|
747
|
+
payload = json.loads(ctx.get("EXECUTION_IDENTITY_JSON") or "{}")
|
|
748
|
+
except (TypeError, ValueError):
|
|
749
|
+
return []
|
|
750
|
+
return [
|
|
751
|
+
str(row.get("roleExecutionRef"))
|
|
752
|
+
for row in payload.get("roleExecutions") or []
|
|
753
|
+
if isinstance(row, dict) and row.get("roleExecutionRef")
|
|
754
|
+
]
|
|
755
|
+
|
|
756
|
+
|
|
668
757
|
def _record_start(
|
|
669
758
|
*,
|
|
670
759
|
workspace_root: Path,
|
|
@@ -721,6 +810,8 @@ def _record_start(
|
|
|
721
810
|
okstra_version=ctx.get("OKSTRA_VERSION", ""),
|
|
722
811
|
initial_status=initial_status,
|
|
723
812
|
brief_sha256=brief_sha256,
|
|
813
|
+
execution_manifest_path=ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
|
|
814
|
+
role_execution_refs=_role_execution_refs_from_ctx(ctx),
|
|
724
815
|
)
|
|
725
816
|
|
|
726
817
|
|
|
@@ -785,7 +876,15 @@ def _record_artifact_runtime_render_only_event(inp: PrepareInputs, ctx: dict) ->
|
|
|
785
876
|
|
|
786
877
|
def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
787
878
|
"""rerun 충실 재현을 위한 canonical argv 재구성."""
|
|
788
|
-
|
|
879
|
+
has_role_selection = bool(inp.role_counts_raw or inp.role_models_raw)
|
|
880
|
+
legacy_fallbacks = {} if has_role_selection else ctx
|
|
881
|
+
lead_fallbacks = (
|
|
882
|
+
{} if inp.host_session_context.entry_mode == "current-session"
|
|
883
|
+
else legacy_fallbacks
|
|
884
|
+
)
|
|
885
|
+
workers = inp.workers_override or (
|
|
886
|
+
legacy_fallbacks.get("RECOMMENDED_ANALYSERS", "")
|
|
887
|
+
)
|
|
789
888
|
pairs = [
|
|
790
889
|
("--task-type", inp.task_type),
|
|
791
890
|
("--project-id", inp.project_id),
|
|
@@ -800,20 +899,44 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
800
899
|
("--clarification-response", inp.clarification_response_path),
|
|
801
900
|
("--selected-direction", inp.selected_direction_path),
|
|
802
901
|
("--workers", workers),
|
|
803
|
-
(
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
(
|
|
902
|
+
(
|
|
903
|
+
"--lead-provider",
|
|
904
|
+
inp.lead_provider or lead_fallbacks.get("LEAD_PROVIDER", ""),
|
|
905
|
+
),
|
|
906
|
+
(
|
|
907
|
+
"--lead-model",
|
|
908
|
+
inp.lead_model or lead_fallbacks.get("LEAD_MODEL", ""),
|
|
909
|
+
),
|
|
910
|
+
(
|
|
911
|
+
"--claude-model",
|
|
912
|
+
inp.claude_model or legacy_fallbacks.get("CLAUDE_WORKER_MODEL", ""),
|
|
913
|
+
),
|
|
914
|
+
(
|
|
915
|
+
"--codex-model",
|
|
916
|
+
inp.codex_model or legacy_fallbacks.get("CODEX_WORKER_MODEL", ""),
|
|
917
|
+
),
|
|
918
|
+
(
|
|
919
|
+
"--antigravity-model",
|
|
920
|
+
inp.antigravity_model
|
|
921
|
+
or legacy_fallbacks.get("ANTIGRAVITY_WORKER_MODEL", ""),
|
|
922
|
+
),
|
|
808
923
|
("--worker-model", inp.worker_models_raw),
|
|
809
924
|
(
|
|
810
925
|
"--report-writer-provider",
|
|
811
|
-
inp.report_writer_provider
|
|
926
|
+
inp.report_writer_provider
|
|
927
|
+
or legacy_fallbacks.get("REPORT_WRITER_PROVIDER", ""),
|
|
928
|
+
),
|
|
929
|
+
(
|
|
930
|
+
"--report-writer-model",
|
|
931
|
+
inp.report_writer_model
|
|
932
|
+
or legacy_fallbacks.get("REPORT_WRITER_MODEL", ""),
|
|
812
933
|
),
|
|
813
|
-
("--report-writer-model", inp.report_writer_model or ctx.get("REPORT_WRITER_MODEL", "")),
|
|
814
934
|
("--lead-runtime", inp.lead_runtime if inp.lead_runtime != "claude-code" else ""),
|
|
815
|
-
(
|
|
816
|
-
|
|
935
|
+
(
|
|
936
|
+
"--executor",
|
|
937
|
+
inp.executor or legacy_fallbacks.get("EXECUTOR_PROVIDER", ""),
|
|
938
|
+
),
|
|
939
|
+
("--critic", inp.critic or legacy_fallbacks.get("CRITIC_CHOICE", "")),
|
|
817
940
|
("--related-tasks", inp.related_tasks_raw),
|
|
818
941
|
("--work-category", inp.work_category),
|
|
819
942
|
]
|
|
@@ -821,6 +944,15 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
821
944
|
for flag, val in pairs:
|
|
822
945
|
if val:
|
|
823
946
|
argv.extend([flag, val])
|
|
947
|
+
for role_count in inp.role_counts_raw:
|
|
948
|
+
argv.extend(["--role-count", role_count])
|
|
949
|
+
for role_model in inp.role_models_raw:
|
|
950
|
+
argv.extend(["--role-model", role_model])
|
|
951
|
+
if inp.host_session_context.host_id:
|
|
952
|
+
argv.extend([
|
|
953
|
+
"--host-session-context-json",
|
|
954
|
+
serialize_host_session_context(inp.host_session_context),
|
|
955
|
+
])
|
|
824
956
|
if inp.render_only:
|
|
825
957
|
argv.append("--render-only")
|
|
826
958
|
if not inp.plan_verification_enabled:
|
|
@@ -1645,12 +1777,484 @@ def _resolve_roster(inp: PrepareInputs, profile_file: Path) -> tuple[list[str],
|
|
|
1645
1777
|
workers = normalize_workers(inp.workers_override or profile_workers_csv)
|
|
1646
1778
|
if inp.workers_override.strip():
|
|
1647
1779
|
validate_workers_against_profile(workers, profile_workers, optional_workers)
|
|
1780
|
+
try:
|
|
1781
|
+
role_profile = load_role_profile(profile_file)
|
|
1782
|
+
except RoleProfileError as exc:
|
|
1783
|
+
raise PrepareError(str(exc)) from exc
|
|
1784
|
+
requires_report_writer = any(
|
|
1785
|
+
row.role == "report-writer" and row.count == 1
|
|
1786
|
+
for row in role_profile.roles
|
|
1787
|
+
)
|
|
1788
|
+
if requires_report_writer and "report-writer" not in workers:
|
|
1789
|
+
workers.append("report-writer")
|
|
1648
1790
|
if not workers:
|
|
1649
1791
|
raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
|
|
1650
1792
|
_validate_option_selection_roster(inp.task_type, workers)
|
|
1651
1793
|
return workers, ",".join(workers)
|
|
1652
1794
|
|
|
1653
1795
|
|
|
1796
|
+
def _profile_requires_executor(profile_file: Path) -> bool:
|
|
1797
|
+
try:
|
|
1798
|
+
profile = load_role_profile(profile_file)
|
|
1799
|
+
except RoleProfileError as exc:
|
|
1800
|
+
raise PrepareError(str(exc)) from exc
|
|
1801
|
+
return any(
|
|
1802
|
+
requirement.role == "implementer" and requirement.count > 0
|
|
1803
|
+
for requirement in profile.roles
|
|
1804
|
+
)
|
|
1805
|
+
|
|
1806
|
+
|
|
1807
|
+
def _normalize_prepare_model_selection(
|
|
1808
|
+
inp: PrepareInputs,
|
|
1809
|
+
profile_file: Path,
|
|
1810
|
+
) -> tuple[CanonicalModelSelection, RoleProfile]:
|
|
1811
|
+
try:
|
|
1812
|
+
_validate_critic_choice(inp.critic)
|
|
1813
|
+
profile = load_role_profile(profile_file)
|
|
1814
|
+
profile_workers = resolve_profile_workers(profile_file)
|
|
1815
|
+
has_worker_models = bool(
|
|
1816
|
+
inp.worker_models_raw.strip()
|
|
1817
|
+
or inp.claude_model.strip()
|
|
1818
|
+
or inp.codex_model.strip()
|
|
1819
|
+
or inp.antigravity_model.strip()
|
|
1820
|
+
)
|
|
1821
|
+
workers = inp.workers_override
|
|
1822
|
+
if not workers and has_worker_models:
|
|
1823
|
+
workers = ",".join(profile_workers)
|
|
1824
|
+
selection = normalize_model_selection_inputs(
|
|
1825
|
+
profile=profile,
|
|
1826
|
+
role_counts_raw=inp.role_counts_raw,
|
|
1827
|
+
role_models_raw=inp.role_models_raw,
|
|
1828
|
+
workers=workers,
|
|
1829
|
+
worker_model=inp.worker_models_raw,
|
|
1830
|
+
critic=inp.critic,
|
|
1831
|
+
executor=inp.executor,
|
|
1832
|
+
lead_provider=inp.lead_provider,
|
|
1833
|
+
lead_model=inp.lead_model,
|
|
1834
|
+
report_writer_provider=inp.report_writer_provider,
|
|
1835
|
+
report_writer_model=inp.report_writer_model,
|
|
1836
|
+
provider_models={
|
|
1837
|
+
"claude": inp.claude_model,
|
|
1838
|
+
"codex": inp.codex_model,
|
|
1839
|
+
"antigravity": inp.antigravity_model,
|
|
1840
|
+
},
|
|
1841
|
+
)
|
|
1842
|
+
except (ModelSelectionInputError, RoleProfileError) as exc:
|
|
1843
|
+
raise PrepareError(str(exc)) from exc
|
|
1844
|
+
return selection, profile
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
def _effective_host_session_context(
|
|
1848
|
+
inp: PrepareInputs,
|
|
1849
|
+
lead_runtime: str,
|
|
1850
|
+
) -> HostSessionContext:
|
|
1851
|
+
context = inp.host_session_context
|
|
1852
|
+
if context.host_id:
|
|
1853
|
+
return context
|
|
1854
|
+
descriptor = default_host_registry().resolve(lead_runtime).descriptor
|
|
1855
|
+
current_model = context.current_model
|
|
1856
|
+
if not current_model.provider_id:
|
|
1857
|
+
current_model = CurrentSessionModelAttestation.unknown(
|
|
1858
|
+
descriptor.native_provider_id
|
|
1859
|
+
)
|
|
1860
|
+
return HostSessionContext(
|
|
1861
|
+
host_id=descriptor.id,
|
|
1862
|
+
entry_mode=context.entry_mode,
|
|
1863
|
+
available_functions=context.available_functions,
|
|
1864
|
+
interaction_surface=context.interaction_surface,
|
|
1865
|
+
current_model=current_model,
|
|
1866
|
+
)
|
|
1867
|
+
|
|
1868
|
+
|
|
1869
|
+
def _resolve_prepare_assignments(
|
|
1870
|
+
*,
|
|
1871
|
+
inp: PrepareInputs,
|
|
1872
|
+
profile: RoleProfile,
|
|
1873
|
+
selection: CanonicalModelSelection,
|
|
1874
|
+
workers: list[str],
|
|
1875
|
+
lead_runtime: str,
|
|
1876
|
+
terminal_backend: TerminalBackend,
|
|
1877
|
+
requires_executor: bool,
|
|
1878
|
+
) -> tuple[AssignmentContext, AssignmentPlan]:
|
|
1879
|
+
scopes = _model_default_scopes(Path(inp.project_root))
|
|
1880
|
+
provider_ids_for_snapshot = _selection_provider_ids(
|
|
1881
|
+
inp,
|
|
1882
|
+
profile,
|
|
1883
|
+
selection,
|
|
1884
|
+
workers,
|
|
1885
|
+
scopes,
|
|
1886
|
+
requires_executor,
|
|
1887
|
+
)
|
|
1888
|
+
context = load_assignment_context(
|
|
1889
|
+
host_runtime=lead_runtime,
|
|
1890
|
+
terminal_backend=terminal_backend,
|
|
1891
|
+
execution_provider_ids=provider_ids_for_snapshot,
|
|
1892
|
+
)
|
|
1893
|
+
host = _effective_host_session_context(inp, lead_runtime)
|
|
1894
|
+
role_models = _materialize_selection_models(selection, context, host)
|
|
1895
|
+
role_counts = _resolver_role_counts(profile, selection.role_counts)
|
|
1896
|
+
try:
|
|
1897
|
+
plan = resolve_assignments(
|
|
1898
|
+
profile=profile,
|
|
1899
|
+
role_counts=role_counts,
|
|
1900
|
+
role_models=role_models,
|
|
1901
|
+
scopes=scopes,
|
|
1902
|
+
pool=context.pool,
|
|
1903
|
+
host=host,
|
|
1904
|
+
environment=context.environment,
|
|
1905
|
+
)
|
|
1906
|
+
except AssignmentResolutionError as exc:
|
|
1907
|
+
raise PrepareError(_assignment_resolution_message(exc)) from exc
|
|
1908
|
+
except (ValueError, RuntimeError) as exc:
|
|
1909
|
+
raise PrepareError(str(exc)) from exc
|
|
1910
|
+
return context, plan
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
def _build_static_execution_manifest(
|
|
1914
|
+
plan: AssignmentPlan,
|
|
1915
|
+
context: AssignmentContext,
|
|
1916
|
+
lead: ResolvedAssignment,
|
|
1917
|
+
) -> ExecutionManifest:
|
|
1918
|
+
participants: list[ParticipantAssignment] = []
|
|
1919
|
+
roles: list[RoleExecution] = []
|
|
1920
|
+
assignments = (
|
|
1921
|
+
lead,
|
|
1922
|
+
*(row for row in plan.assignments if row.role != "leader"),
|
|
1923
|
+
)
|
|
1924
|
+
for index, assignment in enumerate(assignments, start=1):
|
|
1925
|
+
participant_ref = f"participant-{index:03d}"
|
|
1926
|
+
participants.append(_participant_assignment(assignment, participant_ref))
|
|
1927
|
+
roles.append(
|
|
1928
|
+
_static_role_execution(
|
|
1929
|
+
assignment,
|
|
1930
|
+
participant_ref,
|
|
1931
|
+
context,
|
|
1932
|
+
)
|
|
1933
|
+
)
|
|
1934
|
+
return ExecutionManifest(
|
|
1935
|
+
entry_mode=lead.entry_mode,
|
|
1936
|
+
participant_assignments=tuple(participants),
|
|
1937
|
+
role_executions=tuple(roles),
|
|
1938
|
+
)
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
def _participant_assignment(
|
|
1942
|
+
assignment: ResolvedAssignment,
|
|
1943
|
+
participant_ref: str,
|
|
1944
|
+
) -> ParticipantAssignment:
|
|
1945
|
+
binding = assignment.binding
|
|
1946
|
+
return ParticipantAssignment(
|
|
1947
|
+
participant_ref=participant_ref,
|
|
1948
|
+
provider=assignment.provider_id,
|
|
1949
|
+
model_ref=assignment.model_ref,
|
|
1950
|
+
model_id=assignment.model_id,
|
|
1951
|
+
runner=binding.runner if binding is not None else "current-session",
|
|
1952
|
+
host_runtime=assignment.host_runtime,
|
|
1953
|
+
host_model_value=binding.host_model_value if binding is not None else None,
|
|
1954
|
+
session_ref=None,
|
|
1955
|
+
window_ref=None,
|
|
1956
|
+
worker_write_capability=(
|
|
1957
|
+
binding.worker_write_capability if binding is not None else None
|
|
1958
|
+
),
|
|
1959
|
+
entry_mode=assignment.entry_mode,
|
|
1960
|
+
status="prepared",
|
|
1961
|
+
)
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
def _static_role_execution(
|
|
1965
|
+
assignment: ResolvedAssignment,
|
|
1966
|
+
participant_ref: str,
|
|
1967
|
+
context: AssignmentContext,
|
|
1968
|
+
) -> RoleExecution:
|
|
1969
|
+
binding = assignment.binding
|
|
1970
|
+
digest = None
|
|
1971
|
+
if assignment.model_ref is not None and binding is not None:
|
|
1972
|
+
digest = model_spec_digest(context.pool.resolve(assignment.model_ref), binding)
|
|
1973
|
+
return RoleExecution(
|
|
1974
|
+
role_execution_ref=(
|
|
1975
|
+
f"role-exec-{assignment.role}-{assignment.ordinal:03d}"
|
|
1976
|
+
),
|
|
1977
|
+
participant_ref=participant_ref,
|
|
1978
|
+
source_role_execution_ref=None,
|
|
1979
|
+
role=assignment.role,
|
|
1980
|
+
provider=assignment.provider_id,
|
|
1981
|
+
model_ref=assignment.model_ref,
|
|
1982
|
+
model_id=assignment.model_id,
|
|
1983
|
+
ordinal=assignment.ordinal,
|
|
1984
|
+
execution_label=execution_label(
|
|
1985
|
+
assignment.role,
|
|
1986
|
+
assignment.provider_id,
|
|
1987
|
+
assignment.model_id,
|
|
1988
|
+
assignment.ordinal,
|
|
1989
|
+
),
|
|
1990
|
+
model_spec_digest=digest,
|
|
1991
|
+
binding=binding,
|
|
1992
|
+
served_model_attestation=ServedModelAttestation.unknown(),
|
|
1993
|
+
status="prepared",
|
|
1994
|
+
)
|
|
1995
|
+
|
|
1996
|
+
|
|
1997
|
+
def _assignment_resolution_message(exc: AssignmentResolutionError) -> str:
|
|
1998
|
+
lines = [str(exc)]
|
|
1999
|
+
for candidate in exc.candidates:
|
|
2000
|
+
if candidate.available:
|
|
2001
|
+
continue
|
|
2002
|
+
lines.append(
|
|
2003
|
+
f"- {candidate.role}#{candidate.ordinal} {candidate.model_ref}: "
|
|
2004
|
+
f"{candidate.reason}"
|
|
2005
|
+
)
|
|
2006
|
+
for constraint in exc.unsatisfied_constraints:
|
|
2007
|
+
lines.append(
|
|
2008
|
+
f"- {constraint.role} {constraint.constraint}: "
|
|
2009
|
+
f"required={constraint.required}, available={constraint.available}"
|
|
2010
|
+
)
|
|
2011
|
+
return "\n".join(lines)
|
|
2012
|
+
|
|
2013
|
+
|
|
2014
|
+
def _selection_provider_ids(
|
|
2015
|
+
inp: PrepareInputs,
|
|
2016
|
+
profile: RoleProfile,
|
|
2017
|
+
selection: CanonicalModelSelection,
|
|
2018
|
+
workers: list[str],
|
|
2019
|
+
scopes: ModelDefaultScopes,
|
|
2020
|
+
requires_executor: bool,
|
|
2021
|
+
) -> tuple[str, ...]:
|
|
2022
|
+
default_roles = _roles_requiring_defaults(
|
|
2023
|
+
inp,
|
|
2024
|
+
profile,
|
|
2025
|
+
selection,
|
|
2026
|
+
)
|
|
2027
|
+
has_canonical_role_selection = bool(
|
|
2028
|
+
inp.role_counts_raw or inp.role_models_raw
|
|
2029
|
+
)
|
|
2030
|
+
if has_canonical_role_selection:
|
|
2031
|
+
selected = _canonical_selection_provider_ids(
|
|
2032
|
+
inp,
|
|
2033
|
+
profile,
|
|
2034
|
+
selection,
|
|
2035
|
+
workers,
|
|
2036
|
+
scopes,
|
|
2037
|
+
default_roles,
|
|
2038
|
+
requires_executor,
|
|
2039
|
+
)
|
|
2040
|
+
else:
|
|
2041
|
+
selected = list(
|
|
2042
|
+
_selected_execution_provider_ids(inp, workers, requires_executor)
|
|
2043
|
+
)
|
|
2044
|
+
selected.extend(
|
|
2045
|
+
provider
|
|
2046
|
+
for providers in selection.provider_constraints.values()
|
|
2047
|
+
for provider in providers
|
|
2048
|
+
if provider
|
|
2049
|
+
)
|
|
2050
|
+
selected.extend(_providers_from_model_refs(selection.role_models.values()))
|
|
2051
|
+
selected.extend(
|
|
2052
|
+
_providers_from_model_refs(
|
|
2053
|
+
_selected_scoped_default_groups(default_roles, scopes)
|
|
2054
|
+
)
|
|
2055
|
+
)
|
|
2056
|
+
current_provider = inp.host_session_context.current_model.provider_id
|
|
2057
|
+
if current_provider:
|
|
2058
|
+
selected.append(current_provider)
|
|
2059
|
+
return tuple(dict.fromkeys(selected))
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
def _selected_scoped_default_groups(
|
|
2063
|
+
roles: Sequence[str],
|
|
2064
|
+
scopes: ModelDefaultScopes,
|
|
2065
|
+
) -> tuple[tuple[str, ...], ...]:
|
|
2066
|
+
groups: list[tuple[str, ...]] = []
|
|
2067
|
+
for role in roles:
|
|
2068
|
+
if role in scopes.project:
|
|
2069
|
+
groups.append(scopes.project[role])
|
|
2070
|
+
elif role in scopes.global_:
|
|
2071
|
+
groups.append(scopes.global_[role])
|
|
2072
|
+
return tuple(groups)
|
|
2073
|
+
|
|
2074
|
+
|
|
2075
|
+
def _roles_requiring_defaults(
|
|
2076
|
+
inp: PrepareInputs,
|
|
2077
|
+
profile: RoleProfile,
|
|
2078
|
+
selection: CanonicalModelSelection,
|
|
2079
|
+
) -> tuple[str, ...]:
|
|
2080
|
+
roles: list[str] = []
|
|
2081
|
+
if (
|
|
2082
|
+
inp.host_session_context.entry_mode != "current-session"
|
|
2083
|
+
and not _explicit_selection_count(selection, "leader")
|
|
2084
|
+
):
|
|
2085
|
+
roles.append("leader")
|
|
2086
|
+
for requirement in profile.roles:
|
|
2087
|
+
if requirement.dynamic:
|
|
2088
|
+
continue
|
|
2089
|
+
desired = selection.role_counts.get(
|
|
2090
|
+
requirement.role,
|
|
2091
|
+
requirement.count,
|
|
2092
|
+
)
|
|
2093
|
+
explicit = _explicit_selection_count(selection, requirement.role)
|
|
2094
|
+
if explicit < desired:
|
|
2095
|
+
roles.append(requirement.role)
|
|
2096
|
+
return tuple(roles)
|
|
2097
|
+
|
|
2098
|
+
|
|
2099
|
+
def _explicit_selection_count(
|
|
2100
|
+
selection: CanonicalModelSelection,
|
|
2101
|
+
role: str,
|
|
2102
|
+
) -> int:
|
|
2103
|
+
return max(
|
|
2104
|
+
len(selection.role_models.get(role, ())),
|
|
2105
|
+
len(selection.provider_constraints.get(role, ())),
|
|
2106
|
+
)
|
|
2107
|
+
|
|
2108
|
+
|
|
2109
|
+
def _canonical_selection_provider_ids(
|
|
2110
|
+
inp: PrepareInputs,
|
|
2111
|
+
profile: RoleProfile,
|
|
2112
|
+
selection: CanonicalModelSelection,
|
|
2113
|
+
workers: list[str],
|
|
2114
|
+
scopes: ModelDefaultScopes,
|
|
2115
|
+
default_roles: Sequence[str],
|
|
2116
|
+
requires_executor: bool,
|
|
2117
|
+
) -> list[str]:
|
|
2118
|
+
selected = [inp.lead_provider]
|
|
2119
|
+
report_writer_uses_bundled_default = (
|
|
2120
|
+
"report-writer" in default_roles
|
|
2121
|
+
and "report-writer" not in scopes.project
|
|
2122
|
+
and "report-writer" not in scopes.global_
|
|
2123
|
+
)
|
|
2124
|
+
if report_writer_uses_bundled_default:
|
|
2125
|
+
selected.append("claude")
|
|
2126
|
+
implementer_uses_bundled_default = (
|
|
2127
|
+
requires_executor
|
|
2128
|
+
and "implementer" in default_roles
|
|
2129
|
+
and "implementer" not in scopes.project
|
|
2130
|
+
and "implementer" not in scopes.global_
|
|
2131
|
+
)
|
|
2132
|
+
if implementer_uses_bundled_default:
|
|
2133
|
+
selected.append(
|
|
2134
|
+
_default("OKSTRA_DEFAULT_EXECUTOR", "claude")
|
|
2135
|
+
)
|
|
2136
|
+
if _needs_profile_worker_candidates(profile, selection, scopes):
|
|
2137
|
+
selected.extend(
|
|
2138
|
+
worker for worker in workers if worker != "report-writer"
|
|
2139
|
+
)
|
|
2140
|
+
return selected
|
|
2141
|
+
|
|
2142
|
+
|
|
2143
|
+
def _needs_profile_worker_candidates(
|
|
2144
|
+
profile: RoleProfile,
|
|
2145
|
+
selection: CanonicalModelSelection,
|
|
2146
|
+
scopes: ModelDefaultScopes,
|
|
2147
|
+
) -> bool:
|
|
2148
|
+
for requirement in profile.roles:
|
|
2149
|
+
if requirement.dynamic or requirement.role == "report-writer":
|
|
2150
|
+
continue
|
|
2151
|
+
desired = selection.role_counts.get(
|
|
2152
|
+
requirement.role,
|
|
2153
|
+
requirement.count,
|
|
2154
|
+
)
|
|
2155
|
+
explicit = _explicit_selection_count(selection, requirement.role)
|
|
2156
|
+
has_scoped_default = (
|
|
2157
|
+
requirement.role in scopes.project
|
|
2158
|
+
or requirement.role in scopes.global_
|
|
2159
|
+
)
|
|
2160
|
+
if explicit < desired and not has_scoped_default:
|
|
2161
|
+
return True
|
|
2162
|
+
return False
|
|
2163
|
+
|
|
2164
|
+
|
|
2165
|
+
def _providers_from_model_refs(
|
|
2166
|
+
model_groups: Sequence[Sequence[str]],
|
|
2167
|
+
) -> tuple[str, ...]:
|
|
2168
|
+
return tuple(
|
|
2169
|
+
model_ref.split("/", 1)[0]
|
|
2170
|
+
for models in model_groups
|
|
2171
|
+
for model_ref in models
|
|
2172
|
+
if "/" in model_ref
|
|
2173
|
+
)
|
|
2174
|
+
|
|
2175
|
+
|
|
2176
|
+
def _materialize_selection_models(
|
|
2177
|
+
selection: CanonicalModelSelection,
|
|
2178
|
+
context: AssignmentContext,
|
|
2179
|
+
host: HostSessionContext,
|
|
2180
|
+
) -> dict[str, tuple[str, ...]]:
|
|
2181
|
+
role_models = dict(selection.role_models)
|
|
2182
|
+
for role, providers in selection.provider_constraints.items():
|
|
2183
|
+
model_values = selection.legacy_model_values.get(role, {})
|
|
2184
|
+
resolved: list[str] = []
|
|
2185
|
+
for raw_provider in providers:
|
|
2186
|
+
provider = raw_provider or _fallback_legacy_provider(role, context, host)
|
|
2187
|
+
raw_model = model_values.get(provider, model_values.get("", ""))
|
|
2188
|
+
try:
|
|
2189
|
+
model = resolve_model_selection(
|
|
2190
|
+
context.pool,
|
|
2191
|
+
provider,
|
|
2192
|
+
role,
|
|
2193
|
+
raw_model,
|
|
2194
|
+
)
|
|
2195
|
+
except (ValueError, RuntimeError) as exc:
|
|
2196
|
+
raise PrepareError(str(exc)) from exc
|
|
2197
|
+
resolved.append(str(model.model_ref))
|
|
2198
|
+
role_models[role] = tuple(resolved)
|
|
2199
|
+
return role_models
|
|
2200
|
+
|
|
2201
|
+
|
|
2202
|
+
def _fallback_legacy_provider(
|
|
2203
|
+
role: str,
|
|
2204
|
+
context: AssignmentContext,
|
|
2205
|
+
host: HostSessionContext,
|
|
2206
|
+
) -> str:
|
|
2207
|
+
if role == "leader":
|
|
2208
|
+
return (
|
|
2209
|
+
host.current_model.provider_id
|
|
2210
|
+
or context.environment.host_descriptor.native_provider_id
|
|
2211
|
+
)
|
|
2212
|
+
return "claude"
|
|
2213
|
+
|
|
2214
|
+
|
|
2215
|
+
def _resolver_role_counts(
|
|
2216
|
+
profile: RoleProfile,
|
|
2217
|
+
counts: Mapping[str, int],
|
|
2218
|
+
) -> dict[str, int]:
|
|
2219
|
+
adjustable = {
|
|
2220
|
+
requirement.role
|
|
2221
|
+
for requirement in profile.roles
|
|
2222
|
+
if not requirement.dynamic and requirement.optional_count > 0
|
|
2223
|
+
}
|
|
2224
|
+
return {role: count for role, count in counts.items() if role in adjustable}
|
|
2225
|
+
|
|
2226
|
+
|
|
2227
|
+
def _model_default_scopes(project_root: Path) -> ModelDefaultScopes:
|
|
2228
|
+
return ModelDefaultScopes(
|
|
2229
|
+
project=_read_model_defaults(project_json_path(project_root)),
|
|
2230
|
+
global_=_read_model_defaults(okstra_home() / "config.json"),
|
|
2231
|
+
bundled={},
|
|
2232
|
+
)
|
|
2233
|
+
|
|
2234
|
+
|
|
2235
|
+
def _read_model_defaults(path: Path) -> dict[str, tuple[str, ...]]:
|
|
2236
|
+
if not path.is_file():
|
|
2237
|
+
return {}
|
|
2238
|
+
try:
|
|
2239
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
2240
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
2241
|
+
raise PrepareError(f"model defaults file is invalid: {path}: {exc}") from exc
|
|
2242
|
+
raw_defaults = payload.get("modelDefaults", {}) if isinstance(payload, dict) else {}
|
|
2243
|
+
if not isinstance(raw_defaults, dict):
|
|
2244
|
+
raise PrepareError(f"modelDefaults must be an object: {path}")
|
|
2245
|
+
defaults: dict[str, tuple[str, ...]] = {}
|
|
2246
|
+
for role, models in raw_defaults.items():
|
|
2247
|
+
if (
|
|
2248
|
+
not isinstance(role, str)
|
|
2249
|
+
or not isinstance(models, list)
|
|
2250
|
+
or not models
|
|
2251
|
+
or not all(isinstance(model, str) and model for model in models)
|
|
2252
|
+
):
|
|
2253
|
+
raise PrepareError(f"modelDefaults entries must be non-empty string arrays: {path}")
|
|
2254
|
+
defaults[role] = tuple(models)
|
|
2255
|
+
return defaults
|
|
2256
|
+
|
|
2257
|
+
|
|
1654
2258
|
def _validate_option_selection_roster(
|
|
1655
2259
|
task_type: str, workers: Sequence[str]
|
|
1656
2260
|
) -> None:
|
|
@@ -1715,23 +2319,43 @@ class _ModelBindings:
|
|
|
1715
2319
|
해소한다 — prepare 가 run seq 를 할당하거나 worktree 를 만들기 전에.
|
|
1716
2320
|
"""
|
|
1717
2321
|
|
|
1718
|
-
lead:
|
|
1719
|
-
cw:
|
|
1720
|
-
co:
|
|
1721
|
-
ge:
|
|
1722
|
-
rw:
|
|
2322
|
+
lead: ResolvedAssignment
|
|
2323
|
+
cw: "_LegacyModelProjection"
|
|
2324
|
+
co: "_LegacyModelProjection"
|
|
2325
|
+
ge: "_LegacyModelProjection"
|
|
2326
|
+
rw: ResolvedAssignment
|
|
1723
2327
|
critic_choice: str
|
|
1724
2328
|
critic_model_execution: str
|
|
1725
2329
|
executor_display_name: str
|
|
1726
2330
|
codex_worker_execution: str
|
|
1727
2331
|
antigravity_worker_execution: str
|
|
1728
|
-
executor_assignment: RoleAssignment
|
|
2332
|
+
executor_assignment: RoleAssignment | None
|
|
1729
2333
|
lead_assignment: RoleAssignment
|
|
1730
2334
|
worker_assignments: tuple[RoleAssignment, ...]
|
|
1731
2335
|
invocation_assignments: dict[str, dict[str, object]]
|
|
1732
2336
|
|
|
1733
2337
|
|
|
1734
|
-
|
|
2338
|
+
@dataclass(frozen=True)
|
|
2339
|
+
class _LegacyModelProjection:
|
|
2340
|
+
"""Display/catalog values retained for the fixed v1 manifest fields."""
|
|
2341
|
+
|
|
2342
|
+
model_id: str
|
|
2343
|
+
display: str
|
|
2344
|
+
execution: str
|
|
2345
|
+
|
|
2346
|
+
|
|
2347
|
+
def _legacy_claude_model_override(provider: str, env_name: str) -> str:
|
|
2348
|
+
"""Read a legacy Claude model override only for a Claude assignment."""
|
|
2349
|
+
if provider.strip().lower() != "claude":
|
|
2350
|
+
return ""
|
|
2351
|
+
return _default(env_name, "")
|
|
2352
|
+
|
|
2353
|
+
|
|
2354
|
+
def recommended_role_models(
|
|
2355
|
+
*,
|
|
2356
|
+
lead_provider: str = "",
|
|
2357
|
+
report_writer_provider: str = "",
|
|
2358
|
+
) -> dict[str, str]:
|
|
1735
2359
|
"""역할 → 추천 모델 display 값 (env override 반영). prepare 의 모델 해소와
|
|
1736
2360
|
wizard 의 안내 표기가 공유하는 단일 기준점.
|
|
1737
2361
|
|
|
@@ -1741,18 +2365,28 @@ def recommended_role_models(*, lead_provider: str = "") -> dict[str, str]:
|
|
|
1741
2365
|
role 별 레거시 기본값(claude 계열)으로 떨어지는데, 그 값을 codex 호스트
|
|
1742
2366
|
화면에 그대로 쓰면 안내는 `opus` 인데 prepare 는 `gpt-5.6-sol` 을 배정한다.
|
|
1743
2367
|
"""
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2368
|
+
lead_provider_id = lead_provider or "claude"
|
|
2369
|
+
report_writer_provider_id = report_writer_provider or "claude"
|
|
2370
|
+
lead_default = (
|
|
2371
|
+
_legacy_claude_model_override(
|
|
2372
|
+
lead_provider_id,
|
|
2373
|
+
"OKSTRA_DEFAULT_LEAD_MODEL",
|
|
2374
|
+
)
|
|
2375
|
+
or provider_default_model(lead_provider_id, "lead")
|
|
2376
|
+
)
|
|
2377
|
+
report_writer_default = (
|
|
2378
|
+
_legacy_claude_model_override(
|
|
2379
|
+
report_writer_provider_id,
|
|
2380
|
+
"OKSTRA_DEFAULT_REPORT_WRITER_MODEL",
|
|
2381
|
+
)
|
|
2382
|
+
or provider_default_model(report_writer_provider_id, "report-writer")
|
|
1749
2383
|
)
|
|
1750
2384
|
recommendations = {
|
|
1751
2385
|
"lead": lead_default,
|
|
1752
2386
|
"claude": _default("OKSTRA_DEFAULT_CLAUDE_MODEL", default_model("claude")),
|
|
1753
2387
|
"codex": _default("OKSTRA_DEFAULT_CODEX_MODEL", default_model("codex")),
|
|
1754
2388
|
"antigravity": _default("OKSTRA_DEFAULT_ANTIGRAVITY_MODEL", default_model("antigravity")),
|
|
1755
|
-
"report-writer":
|
|
2389
|
+
"report-writer": report_writer_default,
|
|
1756
2390
|
}
|
|
1757
2391
|
for provider in provider_ids("analyser"):
|
|
1758
2392
|
recommendations.setdefault(
|
|
@@ -1761,7 +2395,10 @@ def recommended_role_models(*, lead_provider: str = "") -> dict[str, str]:
|
|
|
1761
2395
|
return recommendations
|
|
1762
2396
|
|
|
1763
2397
|
|
|
1764
|
-
def _parse_worker_model_overrides(
|
|
2398
|
+
def _parse_worker_model_overrides(
|
|
2399
|
+
raw_value: str,
|
|
2400
|
+
context: AssignmentContext,
|
|
2401
|
+
) -> dict[str, str]:
|
|
1765
2402
|
overrides: dict[str, str] = {}
|
|
1766
2403
|
for item in (raw_value or "").split(","):
|
|
1767
2404
|
item = item.strip()
|
|
@@ -1776,65 +2413,71 @@ def _parse_worker_model_overrides(raw_value: str) -> dict[str, str]:
|
|
|
1776
2413
|
)
|
|
1777
2414
|
if provider in overrides:
|
|
1778
2415
|
raise PrepareError(f"duplicate --worker-model provider: {provider}")
|
|
1779
|
-
|
|
2416
|
+
context.pool.resolve_provider(provider)
|
|
1780
2417
|
overrides[provider] = model
|
|
1781
2418
|
return overrides
|
|
1782
2419
|
|
|
1783
2420
|
|
|
1784
2421
|
def _model_for_role(
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
default_display=default_value,
|
|
1794
|
-
default_execution=default_value,
|
|
1795
|
-
)
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
def _assignment_runner(lead_runtime: str, provider: str, role: str) -> str:
|
|
1799
|
-
host_registry = default_host_registry()
|
|
1800
|
-
provider_registry = default_provider_registry()
|
|
2422
|
+
*,
|
|
2423
|
+
context: AssignmentContext,
|
|
2424
|
+
provider: str,
|
|
2425
|
+
raw_value: str,
|
|
2426
|
+
role: str,
|
|
2427
|
+
default_override: str = "",
|
|
2428
|
+
duty_id: str = "",
|
|
2429
|
+
) -> ResolvedAssignment:
|
|
1801
2430
|
try:
|
|
1802
|
-
return
|
|
1803
|
-
|
|
1804
|
-
|
|
2431
|
+
return resolve_dispatch_assignment(
|
|
2432
|
+
context=context,
|
|
2433
|
+
host_runtime=context.environment.host_descriptor.id,
|
|
1805
2434
|
role=role,
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
UnknownProviderError,
|
|
1812
|
-
) as exc:
|
|
2435
|
+
duty_id=duty_id or role,
|
|
2436
|
+
provider=provider,
|
|
2437
|
+
model=raw_value or default_override,
|
|
2438
|
+
)
|
|
2439
|
+
except (ValueError, RuntimeError) as exc:
|
|
1813
2440
|
raise PrepareError(str(exc)) from exc
|
|
1814
2441
|
|
|
1815
2442
|
|
|
1816
|
-
def _resolve_worker_models(
|
|
2443
|
+
def _resolve_worker_models(
|
|
2444
|
+
inp: PrepareInputs,
|
|
2445
|
+
workers: list[str],
|
|
2446
|
+
context: AssignmentContext,
|
|
2447
|
+
) -> dict:
|
|
1817
2448
|
"""Resolve provider models while preserving legacy fixed-provider projections."""
|
|
1818
|
-
|
|
1819
|
-
overrides = _parse_worker_model_overrides(inp.worker_models_raw)
|
|
2449
|
+
overrides = _parse_worker_model_overrides(inp.worker_models_raw, context)
|
|
1820
2450
|
legacy = {
|
|
1821
2451
|
"claude": inp.claude_model,
|
|
1822
2452
|
"codex": inp.codex_model,
|
|
1823
2453
|
"antigravity": inp.antigravity_model,
|
|
1824
2454
|
}
|
|
2455
|
+
legacy_default_env = {
|
|
2456
|
+
"claude": "OKSTRA_DEFAULT_CLAUDE_MODEL",
|
|
2457
|
+
"codex": "OKSTRA_DEFAULT_CODEX_MODEL",
|
|
2458
|
+
"antigravity": "OKSTRA_DEFAULT_ANTIGRAVITY_MODEL",
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
def worker_model_input(provider: str) -> str:
|
|
2462
|
+
selected = overrides.get(provider, legacy.get(provider, ""))
|
|
2463
|
+
env_name = legacy_default_env.get(provider)
|
|
2464
|
+
return selected or (_default(env_name, "") if env_name else "")
|
|
2465
|
+
|
|
2466
|
+
selected_worker_providers = tuple(dict.fromkeys(
|
|
2467
|
+
worker for worker in workers if worker != "report-writer"
|
|
2468
|
+
))
|
|
1825
2469
|
provider_models = {}
|
|
1826
|
-
for provider in
|
|
1827
|
-
default_override = rec.get(provider, "")
|
|
2470
|
+
for provider in selected_worker_providers:
|
|
1828
2471
|
provider_models[provider] = _model_for_role(
|
|
1829
|
-
|
|
1830
|
-
|
|
2472
|
+
context=context,
|
|
2473
|
+
provider=provider,
|
|
2474
|
+
raw_value=worker_model_input(provider),
|
|
2475
|
+
role="analyser",
|
|
1831
2476
|
)
|
|
1832
2477
|
try:
|
|
1833
|
-
lead_assignment =
|
|
1834
|
-
|
|
2478
|
+
lead_assignment = resolve_context_lead_provider(
|
|
2479
|
+
context=context,
|
|
1835
2480
|
requested_provider=inp.lead_provider,
|
|
1836
|
-
host_registry=default_host_registry(),
|
|
1837
|
-
provider_registry=default_provider_registry(),
|
|
1838
2481
|
)
|
|
1839
2482
|
except (
|
|
1840
2483
|
HostCapabilityMismatch,
|
|
@@ -1845,119 +2488,127 @@ def _resolve_worker_models(inp: PrepareInputs) -> dict:
|
|
|
1845
2488
|
raise PrepareError(str(exc)) from exc
|
|
1846
2489
|
lead_provider = lead_assignment.provider
|
|
1847
2490
|
report_writer_provider = inp.report_writer_provider.strip().lower() or "claude"
|
|
1848
|
-
if not
|
|
2491
|
+
if not context.pool.resolve_provider(report_writer_provider).supports_role(
|
|
2492
|
+
"report-writer"
|
|
2493
|
+
):
|
|
1849
2494
|
raise PrepareError(
|
|
1850
2495
|
f"provider {report_writer_provider!r} does not support the report-writer role"
|
|
1851
2496
|
)
|
|
2497
|
+
|
|
2498
|
+
def legacy_projection(provider: str) -> _LegacyModelProjection:
|
|
2499
|
+
assigned = provider_models.get(provider)
|
|
2500
|
+
if assigned is not None:
|
|
2501
|
+
return _LegacyModelProjection(
|
|
2502
|
+
model_id=assigned.model_id,
|
|
2503
|
+
display=assigned.display,
|
|
2504
|
+
execution=assigned.execution,
|
|
2505
|
+
)
|
|
2506
|
+
try:
|
|
2507
|
+
selected = resolve_model_selection(
|
|
2508
|
+
context.pool,
|
|
2509
|
+
provider,
|
|
2510
|
+
"analyser",
|
|
2511
|
+
worker_model_input(provider),
|
|
2512
|
+
)
|
|
2513
|
+
except (ValueError, RuntimeError) as exc:
|
|
2514
|
+
raise PrepareError(str(exc)) from exc
|
|
2515
|
+
return _LegacyModelProjection(
|
|
2516
|
+
model_id=selected.model_ref.model_id,
|
|
2517
|
+
display=selected.display_name,
|
|
2518
|
+
execution=selected.execution_value,
|
|
2519
|
+
)
|
|
2520
|
+
|
|
1852
2521
|
return {
|
|
1853
2522
|
"lead_provider": lead_provider,
|
|
1854
2523
|
"report_writer_provider": report_writer_provider,
|
|
1855
2524
|
"lead": _model_for_role(
|
|
1856
|
-
|
|
1857
|
-
|
|
2525
|
+
context=context,
|
|
2526
|
+
provider=lead_provider,
|
|
2527
|
+
raw_value=inp.lead_model,
|
|
2528
|
+
role="leader",
|
|
2529
|
+
default_override=_legacy_claude_model_override(
|
|
2530
|
+
lead_provider,
|
|
2531
|
+
"OKSTRA_DEFAULT_LEAD_MODEL",
|
|
2532
|
+
),
|
|
2533
|
+
duty_id="lead",
|
|
1858
2534
|
),
|
|
1859
|
-
"cw":
|
|
1860
|
-
"co":
|
|
1861
|
-
"ge":
|
|
2535
|
+
"cw": legacy_projection("claude"),
|
|
2536
|
+
"co": legacy_projection("codex"),
|
|
2537
|
+
"ge": legacy_projection("antigravity"),
|
|
1862
2538
|
"rw": _model_for_role(
|
|
1863
|
-
|
|
1864
|
-
|
|
2539
|
+
context=context,
|
|
2540
|
+
provider=report_writer_provider,
|
|
2541
|
+
raw_value=inp.report_writer_model,
|
|
2542
|
+
role="report-writer",
|
|
2543
|
+
default_override=_legacy_claude_model_override(
|
|
2544
|
+
report_writer_provider,
|
|
2545
|
+
"OKSTRA_DEFAULT_REPORT_WRITER_MODEL",
|
|
2546
|
+
),
|
|
1865
2547
|
),
|
|
1866
2548
|
"providers": provider_models,
|
|
1867
2549
|
"worker_model_inputs": {
|
|
1868
|
-
provider:
|
|
1869
|
-
for provider in
|
|
2550
|
+
provider: worker_model_input(provider)
|
|
2551
|
+
for provider in dict.fromkeys((
|
|
2552
|
+
*selected_worker_providers,
|
|
2553
|
+
*overrides,
|
|
2554
|
+
*legacy,
|
|
2555
|
+
))
|
|
1870
2556
|
},
|
|
1871
2557
|
}
|
|
1872
2558
|
|
|
1873
2559
|
|
|
1874
|
-
def _resolve_model_bindings(
|
|
2560
|
+
def _resolve_model_bindings(
|
|
2561
|
+
inp: PrepareInputs,
|
|
2562
|
+
workers: list[str],
|
|
2563
|
+
context: AssignmentContext,
|
|
2564
|
+
requires_executor: bool,
|
|
2565
|
+
) -> _ModelBindings:
|
|
1875
2566
|
"""worker 모델 + critic 선택 + executor 바인딩을 한 묶음으로 해소·검증한다."""
|
|
1876
|
-
m = _resolve_worker_models(inp)
|
|
1877
|
-
critic_choice = (inp.critic
|
|
1878
|
-
allowed_critics = ["", "off", *provider_ids("critic")]
|
|
1879
|
-
if critic_choice not in allowed_critics:
|
|
1880
|
-
raise PrepareError(
|
|
1881
|
-
f"--critic must be one of: {', '.join(allowed_critics[1:])} "
|
|
1882
|
-
f"(got: {critic_choice!r})"
|
|
1883
|
-
)
|
|
2567
|
+
m = _resolve_worker_models(inp, workers, context)
|
|
2568
|
+
critic_choice = _validate_critic_choice(inp.critic)
|
|
1884
2569
|
critic_model_execution = ""
|
|
1885
2570
|
critic_meta = None
|
|
1886
2571
|
if critic_choice in provider_ids("critic"):
|
|
1887
2572
|
critic_meta = _model_for_role(
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
"
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
critic_meta, provider=critic_choice, role="critic",
|
|
1894
|
-
)
|
|
1895
|
-
executor_default = _default("OKSTRA_DEFAULT_EXECUTOR", "claude")
|
|
1896
|
-
executor_provider = (inp.executor or executor_default).strip().lower()
|
|
1897
|
-
allowed_executors = provider_ids("executor")
|
|
1898
|
-
if executor_provider not in allowed_executors:
|
|
1899
|
-
raise PrepareError(
|
|
1900
|
-
f"--executor must be one of: {', '.join(allowed_executors)} "
|
|
1901
|
-
f"(got: {executor_provider!r})"
|
|
1902
|
-
)
|
|
1903
|
-
if inp.task_type == "implementation" and executor_provider not in workers:
|
|
1904
|
-
raise PrepareError(
|
|
1905
|
-
f"--executor {executor_provider} requires {executor_provider!r} in "
|
|
1906
|
-
f"--workers, but resolved roster is {workers!r}. "
|
|
1907
|
-
f"Add it explicitly, e.g. --workers {','.join(sorted(set(workers + [executor_provider])))}."
|
|
2573
|
+
context=context,
|
|
2574
|
+
provider=critic_choice,
|
|
2575
|
+
raw_value=m["worker_model_inputs"].get(critic_choice, ""),
|
|
2576
|
+
role="critic",
|
|
2577
|
+
duty_id="scope-critic",
|
|
1908
2578
|
)
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2579
|
+
critic_model_execution = critic_meta.execution
|
|
2580
|
+
executor_assignment = None
|
|
2581
|
+
display_name = ""
|
|
2582
|
+
if requires_executor:
|
|
2583
|
+
executor_assignment, display_name = _resolve_executor_assignment(
|
|
2584
|
+
inp,
|
|
2585
|
+
workers,
|
|
2586
|
+
context,
|
|
2587
|
+
m["worker_model_inputs"],
|
|
1917
2588
|
)
|
|
1918
2589
|
lead_assignment = _role_assignment(
|
|
1919
|
-
|
|
2590
|
+
resolved=m["lead"],
|
|
1920
2591
|
role="lead",
|
|
1921
|
-
provider=lead_provider,
|
|
1922
|
-
model=m["lead"],
|
|
1923
|
-
execution=lead_execution,
|
|
1924
|
-
runner_role="lead",
|
|
1925
2592
|
)
|
|
1926
|
-
worker_assignments = _build_worker_assignments(
|
|
2593
|
+
worker_assignments = _build_worker_assignments(workers, m)
|
|
1927
2594
|
critic_assignment = (
|
|
1928
2595
|
_role_assignment(
|
|
1929
|
-
|
|
2596
|
+
resolved=critic_meta,
|
|
1930
2597
|
role="critic",
|
|
1931
|
-
provider=critic_choice,
|
|
1932
|
-
model=critic_meta,
|
|
1933
|
-
execution=critic_model_execution,
|
|
1934
|
-
runner_role="critic",
|
|
1935
2598
|
)
|
|
1936
2599
|
if critic_meta is not None
|
|
1937
2600
|
else None
|
|
1938
2601
|
)
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
role="translator",
|
|
2602
|
+
translator_meta = _model_for_role(
|
|
2603
|
+
context=context,
|
|
1942
2604
|
provider=m["report_writer_provider"],
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
provider=m["report_writer_provider"],
|
|
1947
|
-
role="report-writer",
|
|
1948
|
-
),
|
|
1949
|
-
runner_role="report-writer",
|
|
2605
|
+
raw_value=m["rw"].model_id,
|
|
2606
|
+
role="translator",
|
|
2607
|
+
duty_id="translator",
|
|
1950
2608
|
)
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
role="
|
|
1954
|
-
provider=executor_provider,
|
|
1955
|
-
model=model_meta,
|
|
1956
|
-
execution=_normalized_execution(
|
|
1957
|
-
model_meta, provider=executor_provider, role="executor"
|
|
1958
|
-
),
|
|
1959
|
-
runner_role="executor",
|
|
1960
|
-
worker_id=executor_provider,
|
|
2609
|
+
translator_assignment = _role_assignment(
|
|
2610
|
+
resolved=translator_meta,
|
|
2611
|
+
role="translator",
|
|
1961
2612
|
)
|
|
1962
2613
|
invocation_assignments = _build_invocation_assignments(
|
|
1963
2614
|
lead_assignment,
|
|
@@ -1965,15 +2616,24 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
|
|
|
1965
2616
|
critic_assignment,
|
|
1966
2617
|
translator_assignment,
|
|
1967
2618
|
)
|
|
2619
|
+
worker_by_provider = {
|
|
2620
|
+
row.provider: row for row in worker_assignments
|
|
2621
|
+
}
|
|
1968
2622
|
return _ModelBindings(
|
|
1969
2623
|
lead=m["lead"], cw=m["cw"], co=m["co"], ge=m["ge"], rw=m["rw"],
|
|
1970
2624
|
critic_choice=critic_choice,
|
|
1971
2625
|
critic_model_execution=critic_model_execution,
|
|
1972
2626
|
executor_display_name=display_name,
|
|
1973
|
-
codex_worker_execution=
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
m["
|
|
2627
|
+
codex_worker_execution=(
|
|
2628
|
+
worker_by_provider["codex"].model_execution_value
|
|
2629
|
+
if "codex" in worker_by_provider
|
|
2630
|
+
else m["co"].execution
|
|
2631
|
+
),
|
|
2632
|
+
antigravity_worker_execution=(
|
|
2633
|
+
worker_by_provider["antigravity"].model_execution_value
|
|
2634
|
+
if "antigravity" in worker_by_provider
|
|
2635
|
+
else m["ge"].execution
|
|
2636
|
+
),
|
|
1977
2637
|
executor_assignment=executor_assignment,
|
|
1978
2638
|
lead_assignment=lead_assignment,
|
|
1979
2639
|
worker_assignments=worker_assignments,
|
|
@@ -1981,8 +2641,245 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
|
|
|
1981
2641
|
)
|
|
1982
2642
|
|
|
1983
2643
|
|
|
2644
|
+
def _validate_critic_choice(raw_value: str) -> str:
|
|
2645
|
+
critic_choice = (raw_value or "").strip().lower()
|
|
2646
|
+
allowed_critics = ["", "off", *provider_ids("critic")]
|
|
2647
|
+
if critic_choice not in allowed_critics:
|
|
2648
|
+
raise PrepareError(
|
|
2649
|
+
f"--critic must be one of: {', '.join(allowed_critics[1:])} "
|
|
2650
|
+
f"(got: {critic_choice!r})"
|
|
2651
|
+
)
|
|
2652
|
+
return critic_choice
|
|
2653
|
+
|
|
2654
|
+
|
|
2655
|
+
def _uses_canonical_assignment_projection(inp: PrepareInputs) -> bool:
|
|
2656
|
+
return bool(
|
|
2657
|
+
inp.role_counts_raw
|
|
2658
|
+
or inp.role_models_raw
|
|
2659
|
+
or inp.host_session_context.entry_mode == "current-session"
|
|
2660
|
+
)
|
|
2661
|
+
|
|
2662
|
+
|
|
2663
|
+
def _project_assignment_plan(
|
|
2664
|
+
inp: PrepareInputs,
|
|
2665
|
+
plan: AssignmentPlan,
|
|
2666
|
+
context: AssignmentContext,
|
|
2667
|
+
) -> tuple[list[str], _ModelBindings]:
|
|
2668
|
+
lead = plan.by_role("leader")[0]
|
|
2669
|
+
report_rows = plan.by_role("report-writer")
|
|
2670
|
+
report_writer = (
|
|
2671
|
+
report_rows[0]
|
|
2672
|
+
if report_rows
|
|
2673
|
+
else _model_for_role(
|
|
2674
|
+
context=context,
|
|
2675
|
+
provider=inp.report_writer_provider.strip().lower() or "claude",
|
|
2676
|
+
raw_value=inp.report_writer_model,
|
|
2677
|
+
role="report-writer",
|
|
2678
|
+
)
|
|
2679
|
+
)
|
|
2680
|
+
worker_rows = tuple(
|
|
2681
|
+
row for row in plan.assignments
|
|
2682
|
+
if row.role not in {"leader", "critic", "implementer", "report-writer"}
|
|
2683
|
+
)
|
|
2684
|
+
worker_assignments = tuple(
|
|
2685
|
+
_role_assignment(
|
|
2686
|
+
resolved=row,
|
|
2687
|
+
role=row.role,
|
|
2688
|
+
worker_id=_canonical_worker_id(row, worker_rows),
|
|
2689
|
+
)
|
|
2690
|
+
for row in worker_rows
|
|
2691
|
+
)
|
|
2692
|
+
if report_rows:
|
|
2693
|
+
worker_assignments = (
|
|
2694
|
+
*worker_assignments,
|
|
2695
|
+
_role_assignment(
|
|
2696
|
+
resolved=report_writer,
|
|
2697
|
+
role="report-writer",
|
|
2698
|
+
worker_id="report-writer",
|
|
2699
|
+
),
|
|
2700
|
+
)
|
|
2701
|
+
models = _project_model_bindings(
|
|
2702
|
+
inp,
|
|
2703
|
+
plan,
|
|
2704
|
+
context,
|
|
2705
|
+
lead,
|
|
2706
|
+
report_writer,
|
|
2707
|
+
worker_assignments,
|
|
2708
|
+
)
|
|
2709
|
+
return [row.worker_id for row in worker_assignments], models
|
|
2710
|
+
|
|
2711
|
+
|
|
2712
|
+
def _canonical_worker_id(
|
|
2713
|
+
assignment: ResolvedAssignment,
|
|
2714
|
+
rows: Sequence[ResolvedAssignment],
|
|
2715
|
+
) -> str:
|
|
2716
|
+
prior = sum(
|
|
2717
|
+
row.provider_id == assignment.provider_id and row.ordinal < assignment.ordinal
|
|
2718
|
+
for row in rows
|
|
2719
|
+
)
|
|
2720
|
+
return assignment.provider_id if prior == 0 else f"{assignment.provider_id}-{prior + 1}"
|
|
2721
|
+
|
|
2722
|
+
|
|
2723
|
+
def _project_model_bindings(
|
|
2724
|
+
inp: PrepareInputs,
|
|
2725
|
+
plan: AssignmentPlan,
|
|
2726
|
+
context: AssignmentContext,
|
|
2727
|
+
lead: ResolvedAssignment,
|
|
2728
|
+
report_writer: ResolvedAssignment,
|
|
2729
|
+
workers: tuple[RoleAssignment, ...],
|
|
2730
|
+
) -> _ModelBindings:
|
|
2731
|
+
lead_assignment = _role_assignment(resolved=lead, role="lead")
|
|
2732
|
+
critic_rows = plan.by_role("critic")
|
|
2733
|
+
critic_assignment = (
|
|
2734
|
+
_role_assignment(resolved=critic_rows[0], role="critic")
|
|
2735
|
+
if critic_rows
|
|
2736
|
+
else None
|
|
2737
|
+
)
|
|
2738
|
+
executor_rows = plan.by_role("implementer")
|
|
2739
|
+
executor_assignment = (
|
|
2740
|
+
_role_assignment(
|
|
2741
|
+
resolved=executor_rows[0],
|
|
2742
|
+
role="executor",
|
|
2743
|
+
worker_id=executor_rows[0].provider_id,
|
|
2744
|
+
)
|
|
2745
|
+
if executor_rows
|
|
2746
|
+
else None
|
|
2747
|
+
)
|
|
2748
|
+
translator = _model_for_role(
|
|
2749
|
+
context=context,
|
|
2750
|
+
provider=report_writer.provider_id,
|
|
2751
|
+
raw_value=report_writer.model_id,
|
|
2752
|
+
role="translator",
|
|
2753
|
+
duty_id="translator",
|
|
2754
|
+
)
|
|
2755
|
+
projections = {
|
|
2756
|
+
provider: _legacy_projection_from_plan(provider, plan, context)
|
|
2757
|
+
for provider in ("claude", "codex", "antigravity")
|
|
2758
|
+
}
|
|
2759
|
+
invocation_assignments = _build_invocation_assignments(
|
|
2760
|
+
lead_assignment,
|
|
2761
|
+
workers,
|
|
2762
|
+
critic_assignment,
|
|
2763
|
+
_role_assignment(resolved=translator, role="translator"),
|
|
2764
|
+
)
|
|
2765
|
+
worker_by_provider = {row.provider: row for row in workers}
|
|
2766
|
+
return _ModelBindings(
|
|
2767
|
+
lead=lead,
|
|
2768
|
+
cw=projections["claude"],
|
|
2769
|
+
co=projections["codex"],
|
|
2770
|
+
ge=projections["antigravity"],
|
|
2771
|
+
rw=report_writer,
|
|
2772
|
+
critic_choice=(critic_rows[0].provider_id if critic_rows else "off"),
|
|
2773
|
+
critic_model_execution=(
|
|
2774
|
+
critic_assignment.model_execution_value if critic_assignment else ""
|
|
2775
|
+
),
|
|
2776
|
+
executor_display_name=(
|
|
2777
|
+
f"{executor_rows[0].display_name} executor" if executor_rows else ""
|
|
2778
|
+
),
|
|
2779
|
+
codex_worker_execution=(
|
|
2780
|
+
worker_by_provider["codex"].model_execution_value
|
|
2781
|
+
if "codex" in worker_by_provider
|
|
2782
|
+
else projections["codex"].execution
|
|
2783
|
+
),
|
|
2784
|
+
antigravity_worker_execution=(
|
|
2785
|
+
worker_by_provider["antigravity"].model_execution_value
|
|
2786
|
+
if "antigravity" in worker_by_provider
|
|
2787
|
+
else projections["antigravity"].execution
|
|
2788
|
+
),
|
|
2789
|
+
executor_assignment=executor_assignment,
|
|
2790
|
+
lead_assignment=lead_assignment,
|
|
2791
|
+
worker_assignments=workers,
|
|
2792
|
+
invocation_assignments=invocation_assignments,
|
|
2793
|
+
)
|
|
2794
|
+
|
|
2795
|
+
|
|
2796
|
+
def _legacy_projection_from_plan(
|
|
2797
|
+
provider: str,
|
|
2798
|
+
plan: AssignmentPlan,
|
|
2799
|
+
context: AssignmentContext,
|
|
2800
|
+
) -> _LegacyModelProjection:
|
|
2801
|
+
selected = next(
|
|
2802
|
+
(
|
|
2803
|
+
row for row in plan.assignments
|
|
2804
|
+
if row.provider_id == provider and row.role != "leader"
|
|
2805
|
+
),
|
|
2806
|
+
None,
|
|
2807
|
+
)
|
|
2808
|
+
if selected is not None:
|
|
2809
|
+
return _LegacyModelProjection(
|
|
2810
|
+
selected.model_id,
|
|
2811
|
+
selected.display_name,
|
|
2812
|
+
selected.execution,
|
|
2813
|
+
)
|
|
2814
|
+
model = resolve_model_selection(context.pool, provider, "analyser", "")
|
|
2815
|
+
return _LegacyModelProjection(
|
|
2816
|
+
model.model_ref.model_id,
|
|
2817
|
+
model.display_name,
|
|
2818
|
+
model.execution_value,
|
|
2819
|
+
)
|
|
2820
|
+
|
|
2821
|
+
|
|
2822
|
+
def _selected_execution_provider_ids(
|
|
2823
|
+
inp: PrepareInputs,
|
|
2824
|
+
workers: list[str],
|
|
2825
|
+
requires_executor: bool,
|
|
2826
|
+
) -> tuple[str, ...]:
|
|
2827
|
+
selected = [
|
|
2828
|
+
inp.lead_provider,
|
|
2829
|
+
*(worker for worker in workers if worker != "report-writer"),
|
|
2830
|
+
inp.report_writer_provider.strip().lower() or "claude",
|
|
2831
|
+
]
|
|
2832
|
+
critic = inp.critic.strip().lower()
|
|
2833
|
+
if critic not in {"", "off"}:
|
|
2834
|
+
selected.append(critic)
|
|
2835
|
+
if requires_executor:
|
|
2836
|
+
selected.append(
|
|
2837
|
+
inp.executor.strip().lower()
|
|
2838
|
+
or _default("OKSTRA_DEFAULT_EXECUTOR", "claude")
|
|
2839
|
+
)
|
|
2840
|
+
return tuple(dict.fromkeys(selected))
|
|
2841
|
+
|
|
2842
|
+
|
|
2843
|
+
def _resolve_executor_assignment(
|
|
2844
|
+
inp: PrepareInputs,
|
|
2845
|
+
workers: list[str],
|
|
2846
|
+
context: AssignmentContext,
|
|
2847
|
+
worker_model_inputs: dict[str, str],
|
|
2848
|
+
) -> tuple[RoleAssignment, str]:
|
|
2849
|
+
executor_default = _default("OKSTRA_DEFAULT_EXECUTOR", "claude")
|
|
2850
|
+
executor_provider = (inp.executor or executor_default).strip().lower()
|
|
2851
|
+
allowed_executors = provider_ids("executor")
|
|
2852
|
+
if executor_provider not in allowed_executors:
|
|
2853
|
+
raise PrepareError(
|
|
2854
|
+
f"--executor must be one of: {', '.join(allowed_executors)} "
|
|
2855
|
+
f"(got: {executor_provider!r})"
|
|
2856
|
+
)
|
|
2857
|
+
if executor_provider not in workers:
|
|
2858
|
+
raise PrepareError(
|
|
2859
|
+
f"--executor {executor_provider} requires {executor_provider!r} in "
|
|
2860
|
+
f"--workers, but resolved roster is {workers!r}. "
|
|
2861
|
+
f"Add it explicitly, e.g. --workers {','.join(sorted(set(workers + [executor_provider])))}."
|
|
2862
|
+
)
|
|
2863
|
+
model = _model_for_role(
|
|
2864
|
+
context=context,
|
|
2865
|
+
provider=executor_provider,
|
|
2866
|
+
raw_value=worker_model_inputs.get(executor_provider, ""),
|
|
2867
|
+
role="implementer",
|
|
2868
|
+
duty_id="implementation-executor",
|
|
2869
|
+
)
|
|
2870
|
+
assignment = _role_assignment(
|
|
2871
|
+
resolved=model,
|
|
2872
|
+
role="executor",
|
|
2873
|
+
worker_id=executor_provider,
|
|
2874
|
+
)
|
|
2875
|
+
display_name = (
|
|
2876
|
+
f"{context.pool.resolve_provider(executor_provider).display_label} executor"
|
|
2877
|
+
)
|
|
2878
|
+
return assignment, display_name
|
|
2879
|
+
|
|
2880
|
+
|
|
1984
2881
|
def _build_worker_assignments(
|
|
1985
|
-
|
|
2882
|
+
workers: list[str], models: dict,
|
|
1986
2883
|
) -> tuple[RoleAssignment, ...]:
|
|
1987
2884
|
assignments: list[RoleAssignment] = []
|
|
1988
2885
|
for worker_id in workers:
|
|
@@ -1990,18 +2887,12 @@ def _build_worker_assignments(
|
|
|
1990
2887
|
provider = (
|
|
1991
2888
|
models["report_writer_provider"] if is_report_writer else worker_id
|
|
1992
2889
|
)
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
execution = _worker_execution_value(
|
|
1996
|
-
meta, provider, "worker", [provider],
|
|
2890
|
+
selected = (
|
|
2891
|
+
models["rw"] if is_report_writer else models["providers"][provider]
|
|
1997
2892
|
)
|
|
1998
2893
|
assignments.append(_role_assignment(
|
|
1999
|
-
|
|
2000
|
-
role=
|
|
2001
|
-
provider=provider,
|
|
2002
|
-
model=meta,
|
|
2003
|
-
execution=execution,
|
|
2004
|
-
runner_role=role,
|
|
2894
|
+
resolved=selected,
|
|
2895
|
+
role="report-writer" if is_report_writer else "analyser",
|
|
2005
2896
|
worker_id=worker_id,
|
|
2006
2897
|
))
|
|
2007
2898
|
return tuple(assignments)
|
|
@@ -2009,55 +2900,40 @@ def _build_worker_assignments(
|
|
|
2009
2900
|
|
|
2010
2901
|
def _role_assignment(
|
|
2011
2902
|
*,
|
|
2012
|
-
|
|
2903
|
+
resolved: ResolvedAssignment,
|
|
2013
2904
|
role: str,
|
|
2014
|
-
provider: str,
|
|
2015
|
-
model: ModelAssignment,
|
|
2016
|
-
execution: str,
|
|
2017
|
-
runner_role: str,
|
|
2018
2905
|
worker_id: str = "",
|
|
2019
2906
|
) -> RoleAssignment:
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2907
|
+
binding = resolved.binding
|
|
2908
|
+
if binding is None:
|
|
2909
|
+
if resolved.role != "leader" or resolved.entry_mode != "current-session":
|
|
2910
|
+
raise PrepareError("legacy assignment projection requires a model binding")
|
|
2911
|
+
return RoleAssignment(
|
|
2912
|
+
role=role,
|
|
2913
|
+
provider=resolved.provider_id,
|
|
2914
|
+
model_display=resolved.display_name,
|
|
2915
|
+
model_execution_value="unknown",
|
|
2916
|
+
runner="cli-wrapper",
|
|
2917
|
+
host_runtime=resolved.host_runtime,
|
|
2918
|
+
host_model_value=None,
|
|
2919
|
+
worker_id=worker_id,
|
|
2920
|
+
)
|
|
2028
2921
|
return RoleAssignment(
|
|
2029
2922
|
role=role,
|
|
2030
|
-
provider=
|
|
2031
|
-
model_display=
|
|
2032
|
-
model_execution_value=
|
|
2033
|
-
runner=
|
|
2034
|
-
|
|
2035
|
-
|
|
2923
|
+
provider=resolved.provider_id,
|
|
2924
|
+
model_display=resolved.display_name,
|
|
2925
|
+
model_execution_value=binding.resolved_execution_value,
|
|
2926
|
+
runner=(
|
|
2927
|
+
"native-session"
|
|
2928
|
+
if binding.runner == "current-session"
|
|
2929
|
+
else binding.runner
|
|
2930
|
+
),
|
|
2931
|
+
host_runtime=resolved.host_runtime,
|
|
2932
|
+
host_model_value=binding.host_model_value,
|
|
2036
2933
|
worker_id=worker_id,
|
|
2037
2934
|
)
|
|
2038
2935
|
|
|
2039
2936
|
|
|
2040
|
-
def _resolve_host_model_value(
|
|
2041
|
-
*,
|
|
2042
|
-
host_runtime: str,
|
|
2043
|
-
provider: str,
|
|
2044
|
-
model: ModelAssignment,
|
|
2045
|
-
execution: str,
|
|
2046
|
-
runner: str,
|
|
2047
|
-
) -> str | None:
|
|
2048
|
-
try:
|
|
2049
|
-
port = default_host_registry().resolve(host_runtime).host_model()
|
|
2050
|
-
return port.resolve(HostModelBindingRequest(
|
|
2051
|
-
host_runtime=host_runtime,
|
|
2052
|
-
provider=provider,
|
|
2053
|
-
model=model.display,
|
|
2054
|
-
model_execution_value=execution,
|
|
2055
|
-
runner=runner,
|
|
2056
|
-
))
|
|
2057
|
-
except (HostModelBindingError, HostNotRegistered) as exc:
|
|
2058
|
-
raise PrepareError(str(exc)) from exc
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
2937
|
def _build_invocation_assignments(
|
|
2062
2938
|
lead: RoleAssignment,
|
|
2063
2939
|
workers: tuple[RoleAssignment, ...],
|
|
@@ -2079,27 +2955,6 @@ def _build_invocation_assignments(
|
|
|
2079
2955
|
return assignments
|
|
2080
2956
|
|
|
2081
2957
|
|
|
2082
|
-
def _normalized_execution(meta: ModelAssignment, provider: str, role: str) -> str:
|
|
2083
|
-
"""Route a role's catalog execution value through the CLI-identity normalizer
|
|
2084
|
-
so the manifest carries the dispatch-correct spelling. SSOT for the mapping
|
|
2085
|
-
lives in model_discovery; this is only the run.py call seam."""
|
|
2086
|
-
return normalize_execution_for_dispatch(
|
|
2087
|
-
provider=provider, execution=meta.execution, role=role,
|
|
2088
|
-
)
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
def _worker_execution_value(
|
|
2092
|
-
meta: ModelAssignment, provider: str, role: str, workers: list[str],
|
|
2093
|
-
) -> str:
|
|
2094
|
-
"""Normalize a worker role's execution value only when that provider's
|
|
2095
|
-
worker is in the resolved roster; otherwise return the raw catalog value.
|
|
2096
|
-
A worker that will not be dispatched must not probe its CLI or hard-fail
|
|
2097
|
-
the whole prep (the CLI-identity check belongs only to workers that run)."""
|
|
2098
|
-
if provider in workers:
|
|
2099
|
-
return _normalized_execution(meta, provider=provider, role=role)
|
|
2100
|
-
return meta.execution
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
2958
|
StageRunClaim = _implementation_stage.StageRunClaim
|
|
2104
2959
|
|
|
2105
2960
|
|
|
@@ -2488,11 +3343,18 @@ def _render_lead_prompt_and_snapshot(
|
|
|
2488
3343
|
assignment = _agent_model_assignment(
|
|
2489
3344
|
json.loads(ctx["INVOCATION_ASSIGNMENTS_JSON"])["lead"]
|
|
2490
3345
|
)
|
|
3346
|
+
identity = invocation_execution_identity_from_manifest(
|
|
3347
|
+
json.loads(Path(ctx["RUN_MANIFEST_PATH"]).read_text(encoding="utf-8")),
|
|
3348
|
+
assignment=assignment,
|
|
3349
|
+
assignment_ref="lead",
|
|
3350
|
+
duty_id="lead",
|
|
3351
|
+
)
|
|
3352
|
+
if identity is None:
|
|
3353
|
+
raise PrepareError("lead v2 execution identity is missing")
|
|
3354
|
+
invocation_id = f"{ctx['TASK_TYPE_SEGMENT']}-{ctx['RUN_PROMPTS_SEQ']}-lead"
|
|
2491
3355
|
prepared = prepare_agent_invocation(AgentInvocationRequest(
|
|
2492
|
-
invocation_id=
|
|
2493
|
-
|
|
2494
|
-
),
|
|
2495
|
-
worker_id="lead",
|
|
3356
|
+
invocation_id=invocation_id,
|
|
3357
|
+
worker_id=None,
|
|
2496
3358
|
audience="lead",
|
|
2497
3359
|
assignment_ref="lead",
|
|
2498
3360
|
purpose=None,
|
|
@@ -2516,6 +3378,11 @@ def _render_lead_prompt_and_snapshot(
|
|
|
2516
3378
|
prompt_path=Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]),
|
|
2517
3379
|
metadata_path=Path(ctx["LEAD_PROMPT_METADATA_PATH"]),
|
|
2518
3380
|
dispatch_kind="lead",
|
|
3381
|
+
participant_ref=identity.participant_ref,
|
|
3382
|
+
role_execution_ref=identity.role_execution_ref,
|
|
3383
|
+
duty_id=identity.duty_id,
|
|
3384
|
+
invocation_ref=invocation_id,
|
|
3385
|
+
attempt=1,
|
|
2519
3386
|
))
|
|
2520
3387
|
return prepared.prompt_path.read_text(encoding="utf-8")
|
|
2521
3388
|
|
|
@@ -2540,12 +3407,7 @@ def _persist_run_inputs(
|
|
|
2540
3407
|
if inp.approved_plan_path
|
|
2541
3408
|
else ""
|
|
2542
3409
|
)
|
|
2543
|
-
|
|
2544
|
-
project_root=Path(inp.project_root),
|
|
2545
|
-
run_manifests_dir=Path(ctx["RUN_MANIFESTS_DIR"]),
|
|
2546
|
-
task_type_segment=ctx["TASK_TYPE_SEGMENT"],
|
|
2547
|
-
seq=ctx["RUN_MANIFESTS_SEQ"],
|
|
2548
|
-
inputs={
|
|
3410
|
+
inputs = {
|
|
2549
3411
|
"taskBriefPath": brief_relative,
|
|
2550
3412
|
"taskBriefAbsolutePath": str(inp.brief_path),
|
|
2551
3413
|
"directive": inp.directive,
|
|
@@ -2567,7 +3429,11 @@ def _persist_run_inputs(
|
|
|
2567
3429
|
inp.report_writer_provider or "claude",
|
|
2568
3430
|
),
|
|
2569
3431
|
"reportWriterModel": models.rw.display,
|
|
2570
|
-
"executor":
|
|
3432
|
+
"executor": (
|
|
3433
|
+
models.executor_assignment.provider
|
|
3434
|
+
if models.executor_assignment is not None
|
|
3435
|
+
else ""
|
|
3436
|
+
),
|
|
2571
3437
|
"relatedTasks": inp.related_tasks_raw,
|
|
2572
3438
|
"approvedPlanPath": approved_plan_path,
|
|
2573
3439
|
"clarificationResponsePath": inp.clarification_response_path,
|
|
@@ -2577,7 +3443,13 @@ def _persist_run_inputs(
|
|
|
2577
3443
|
),
|
|
2578
3444
|
"evidenceInputs": json.loads(ctx.get("EVIDENCE_INPUTS_JSON", "[]")),
|
|
2579
3445
|
"renderOnly": inp.render_only,
|
|
2580
|
-
|
|
3446
|
+
}
|
|
3447
|
+
run_inputs_path = _write_run_inputs_once(
|
|
3448
|
+
project_root=Path(inp.project_root),
|
|
3449
|
+
run_manifests_dir=Path(ctx["RUN_MANIFESTS_DIR"]),
|
|
3450
|
+
task_type_segment=ctx["TASK_TYPE_SEGMENT"],
|
|
3451
|
+
seq=ctx["RUN_MANIFESTS_SEQ"],
|
|
3452
|
+
inputs=inputs,
|
|
2581
3453
|
)
|
|
2582
3454
|
ctx["RUN_INPUTS_PATH"] = str(run_inputs_path)
|
|
2583
3455
|
ctx["RUN_INPUTS_RELATIVE_PATH"] = relative_to_project_root(
|
|
@@ -2585,6 +3457,29 @@ def _persist_run_inputs(
|
|
|
2585
3457
|
)
|
|
2586
3458
|
|
|
2587
3459
|
|
|
3460
|
+
def _write_run_inputs_once(
|
|
3461
|
+
*,
|
|
3462
|
+
project_root: Path,
|
|
3463
|
+
run_manifests_dir: Path,
|
|
3464
|
+
task_type_segment: str,
|
|
3465
|
+
seq: str,
|
|
3466
|
+
inputs: dict,
|
|
3467
|
+
) -> Path:
|
|
3468
|
+
path = run_manifests_dir / f"run-inputs-{task_type_segment}-{seq}.json"
|
|
3469
|
+
existing = _read_existing_manifest(path)
|
|
3470
|
+
if existing.get("schemaVersion") == "2.0":
|
|
3471
|
+
if existing.get("inputs") != inputs:
|
|
3472
|
+
raise PrepareError("input snapshot is immutable after v2 creation")
|
|
3473
|
+
return path
|
|
3474
|
+
return write_run_inputs(
|
|
3475
|
+
project_root=project_root,
|
|
3476
|
+
run_manifests_dir=run_manifests_dir,
|
|
3477
|
+
task_type_segment=task_type_segment,
|
|
3478
|
+
seq=seq,
|
|
3479
|
+
inputs=inputs,
|
|
3480
|
+
)
|
|
3481
|
+
|
|
3482
|
+
|
|
2588
3483
|
def _persist_pre_dispatch_run_manifest(ctx: dict) -> None:
|
|
2589
3484
|
"""Publish and re-read the complete invocation authority before prompts."""
|
|
2590
3485
|
render_run_manifest(ctx["RUN_MANIFEST_PATH"], ctx)
|
|
@@ -2982,24 +3877,38 @@ def _model_ctx(models: "_ModelBindings") -> dict[str, str]:
|
|
|
2982
3877
|
),
|
|
2983
3878
|
"",
|
|
2984
3879
|
),
|
|
2985
|
-
|
|
2986
|
-
"
|
|
3880
|
+
**_executor_model_ctx(models),
|
|
3881
|
+
"CRITIC_CHOICE": models.critic_choice,
|
|
3882
|
+
"CRITIC_MODEL_EXECUTION_VALUE": models.critic_model_execution,
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
|
|
3886
|
+
def _executor_model_ctx(models: "_ModelBindings") -> dict[str, str]:
|
|
3887
|
+
assignment = models.executor_assignment
|
|
3888
|
+
if assignment is None:
|
|
3889
|
+
return {
|
|
3890
|
+
"EXECUTOR_WORKER_ID": "",
|
|
3891
|
+
"EXECUTOR_PROVIDER": "",
|
|
3892
|
+
"EXECUTOR_DISPLAY_NAME": "",
|
|
3893
|
+
"EXECUTOR_MODEL_DISPLAY": "",
|
|
3894
|
+
"EXECUTOR_MODEL_EXECUTION_VALUE": "",
|
|
3895
|
+
"EXECUTOR_HOST_MODEL_VALUE": "",
|
|
3896
|
+
"EXECUTOR_RUNNER": "",
|
|
3897
|
+
"EXECUTOR_DISPATCH_MODE": "",
|
|
3898
|
+
}
|
|
3899
|
+
return {
|
|
3900
|
+
"EXECUTOR_WORKER_ID": assignment.worker_id,
|
|
3901
|
+
"EXECUTOR_PROVIDER": assignment.provider,
|
|
2987
3902
|
"EXECUTOR_DISPLAY_NAME": models.executor_display_name,
|
|
2988
|
-
"EXECUTOR_MODEL_DISPLAY":
|
|
2989
|
-
"EXECUTOR_MODEL_EXECUTION_VALUE":
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
"EXECUTOR_HOST_MODEL_VALUE": (
|
|
2993
|
-
models.executor_assignment.host_model_value or ""
|
|
2994
|
-
),
|
|
2995
|
-
"EXECUTOR_RUNNER": models.executor_assignment.runner,
|
|
3903
|
+
"EXECUTOR_MODEL_DISPLAY": assignment.model_display,
|
|
3904
|
+
"EXECUTOR_MODEL_EXECUTION_VALUE": assignment.model_execution_value,
|
|
3905
|
+
"EXECUTOR_HOST_MODEL_VALUE": assignment.host_model_value or "",
|
|
3906
|
+
"EXECUTOR_RUNNER": assignment.runner,
|
|
2996
3907
|
"EXECUTOR_DISPATCH_MODE": (
|
|
2997
3908
|
"host-native"
|
|
2998
|
-
if
|
|
3909
|
+
if assignment.runner == "native-session"
|
|
2999
3910
|
else "worker-dispatch"
|
|
3000
3911
|
),
|
|
3001
|
-
"CRITIC_CHOICE": models.critic_choice,
|
|
3002
|
-
"CRITIC_MODEL_EXECUTION_VALUE": models.critic_model_execution,
|
|
3003
3912
|
}
|
|
3004
3913
|
|
|
3005
3914
|
|
|
@@ -3106,6 +4015,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
3106
4015
|
# 조립 코드가 단일 흐름으로 읽히도록 유지한다.
|
|
3107
4016
|
assets = _resolve_runtime_assets(workspace_root, inp)
|
|
3108
4017
|
profile_file = assets.profile_file
|
|
4018
|
+
requires_executor = _profile_requires_executor(profile_file)
|
|
4019
|
+
selection, role_profile = _normalize_prepare_model_selection(inp, profile_file)
|
|
3109
4020
|
prompt_template = assets.prompt_template
|
|
3110
4021
|
task_index_template = assets.task_index_template
|
|
3111
4022
|
final_report_template = assets.final_report_template
|
|
@@ -3127,13 +4038,45 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
3127
4038
|
handoff_tokens = _materialize_release_handoff_input(
|
|
3128
4039
|
workspace_root, project_root, inp)
|
|
3129
4040
|
|
|
3130
|
-
verify_installation(workspace_root)
|
|
3131
|
-
_register_and_check_project(project_root, inp)
|
|
3132
|
-
|
|
3133
4041
|
workers, selected_reviewers = _resolve_roster(inp, profile_file)
|
|
3134
4042
|
pr_template_path_str, pr_template_source = _resolve_pr_template(inp)
|
|
3135
4043
|
|
|
3136
|
-
|
|
4044
|
+
assignment_context, assignment_plan = _resolve_prepare_assignments(
|
|
4045
|
+
inp=inp,
|
|
4046
|
+
profile=role_profile,
|
|
4047
|
+
selection=selection,
|
|
4048
|
+
workers=workers,
|
|
4049
|
+
lead_runtime=lead_runtime,
|
|
4050
|
+
terminal_backend=terminal_backend,
|
|
4051
|
+
requires_executor=requires_executor,
|
|
4052
|
+
)
|
|
4053
|
+
if _uses_canonical_assignment_projection(inp):
|
|
4054
|
+
workers, models = _project_assignment_plan(
|
|
4055
|
+
inp,
|
|
4056
|
+
assignment_plan,
|
|
4057
|
+
assignment_context,
|
|
4058
|
+
)
|
|
4059
|
+
selected_reviewers = ",".join(workers)
|
|
4060
|
+
else:
|
|
4061
|
+
models = _resolve_model_bindings(
|
|
4062
|
+
inp,
|
|
4063
|
+
workers,
|
|
4064
|
+
assignment_context,
|
|
4065
|
+
requires_executor,
|
|
4066
|
+
)
|
|
4067
|
+
execution_manifest = _build_static_execution_manifest(
|
|
4068
|
+
assignment_plan,
|
|
4069
|
+
assignment_context,
|
|
4070
|
+
models.lead,
|
|
4071
|
+
)
|
|
4072
|
+
dynamic_roles = tuple(
|
|
4073
|
+
requirement.role
|
|
4074
|
+
for requirement in role_profile.roles
|
|
4075
|
+
if requirement.dynamic
|
|
4076
|
+
)
|
|
4077
|
+
|
|
4078
|
+
verify_installation(workspace_root)
|
|
4079
|
+
_register_and_check_project(project_root, inp)
|
|
3137
4080
|
|
|
3138
4081
|
# ---- paths under per-task mutex (writes run-context-*.json) ----
|
|
3139
4082
|
# OKSTRA_RUN_SEQ_OVERRIDE: okstra-ctl rerun / 테스트 hook 이 미리 reserve
|
|
@@ -3286,6 +4229,12 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
3286
4229
|
"LATEST_REPORT_RELATIVE_PATH": "",
|
|
3287
4230
|
"RENDER_ONLY": "true" if inp.render_only else "false",
|
|
3288
4231
|
"OKSTRA_VERSION": installed_version(),
|
|
4232
|
+
"EXECUTION_IDENTITY_JSON": json.dumps(
|
|
4233
|
+
execution_manifest.to_payload(), ensure_ascii=False,
|
|
4234
|
+
),
|
|
4235
|
+
"DYNAMIC_EXECUTION_ROLES_JSON": json.dumps(
|
|
4236
|
+
dynamic_roles, ensure_ascii=False,
|
|
4237
|
+
),
|
|
3289
4238
|
})
|
|
3290
4239
|
refresh_run_context_snapshot(ctx)
|
|
3291
4240
|
if lead_runtime == "codex":
|
|
@@ -3325,6 +4274,15 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
3325
4274
|
_finalize_status_and_render_manifests(
|
|
3326
4275
|
inp, ctx, task_index_template, forbidden_by_phase
|
|
3327
4276
|
)
|
|
4277
|
+
persist_static_execution_identity(
|
|
4278
|
+
run_manifest_path=Path(ctx["RUN_MANIFEST_PATH"]),
|
|
4279
|
+
task_manifest_path=Path(ctx["TASK_MANIFEST_PATH"]),
|
|
4280
|
+
input_snapshot_path=Path(ctx["RUN_INPUTS_PATH"]),
|
|
4281
|
+
manifest=execution_manifest,
|
|
4282
|
+
task_key=ctx["TASK_KEY"],
|
|
4283
|
+
run_ref=ctx["RUN_MANIFEST_RELATIVE_PATH"],
|
|
4284
|
+
dynamic_roles=dynamic_roles,
|
|
4285
|
+
)
|
|
3328
4286
|
_record_artifact_runtime_render_only_event(inp, ctx)
|
|
3329
4287
|
|
|
3330
4288
|
_record_run_in_central_index(inp, ctx, workspace_root, run_seq_override)
|
|
@@ -3348,10 +4306,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
3348
4306
|
)
|
|
3349
4307
|
|
|
3350
4308
|
|
|
3351
|
-
def
|
|
3352
|
-
"""
|
|
3353
|
-
(the same flags `okstra.sh` accepts), runs prepare_task_bundle, prints
|
|
3354
|
-
summary lines + prompt to stdout."""
|
|
4309
|
+
def build_prepare_argument_parser():
|
|
4310
|
+
"""Build the shared flat-argument parser used by public prepare shims."""
|
|
3355
4311
|
import argparse
|
|
3356
4312
|
|
|
3357
4313
|
p = argparse.ArgumentParser()
|
|
@@ -3381,6 +4337,13 @@ def main(argv: list[str]) -> int:
|
|
|
3381
4337
|
),
|
|
3382
4338
|
)
|
|
3383
4339
|
p.add_argument("--workers", default="", dest="workers_override")
|
|
4340
|
+
p.add_argument("--role-count", action="append", default=[], dest="role_counts_raw")
|
|
4341
|
+
p.add_argument("--role-model", action="append", default=[], dest="role_models_raw")
|
|
4342
|
+
p.add_argument(
|
|
4343
|
+
"--host-session-context-json",
|
|
4344
|
+
default="",
|
|
4345
|
+
dest="host_session_context_json",
|
|
4346
|
+
)
|
|
3384
4347
|
p.add_argument("--lead-provider", default="")
|
|
3385
4348
|
p.add_argument("--lead-model", default="")
|
|
3386
4349
|
p.add_argument("--claude-model", default="")
|
|
@@ -3524,7 +4487,12 @@ def main(argv: list[str]) -> int:
|
|
|
3524
4487
|
"`main` / `dev` / `staging` / `preprod` / `prod` or any local ref."
|
|
3525
4488
|
),
|
|
3526
4489
|
)
|
|
3527
|
-
|
|
4490
|
+
return p
|
|
4491
|
+
|
|
4492
|
+
|
|
4493
|
+
def main(argv: list[str]) -> int:
|
|
4494
|
+
"""Parse one public prepare invocation and emit its rendered bundle."""
|
|
4495
|
+
args = build_prepare_argument_parser().parse_args(argv)
|
|
3528
4496
|
|
|
3529
4497
|
project_root = Path(args.project_root).expanduser().resolve()
|
|
3530
4498
|
if args.task_type == "release-handoff":
|
|
@@ -3568,6 +4536,25 @@ def main(argv: list[str]) -> int:
|
|
|
3568
4536
|
return 1
|
|
3569
4537
|
selected_direction_abs = str(selected)
|
|
3570
4538
|
|
|
4539
|
+
try:
|
|
4540
|
+
host_session_context = deserialize_host_session_context(
|
|
4541
|
+
args.host_session_context_json
|
|
4542
|
+
)
|
|
4543
|
+
except ModelSelectionInputError as exc:
|
|
4544
|
+
print(str(exc), file=__import__("sys").stderr)
|
|
4545
|
+
return 1
|
|
4546
|
+
if not args.host_session_context_json:
|
|
4547
|
+
adapter = default_host_registry().resolve(args.lead_runtime)
|
|
4548
|
+
host_session_context = HostSessionContext(
|
|
4549
|
+
host_id=adapter.descriptor.id,
|
|
4550
|
+
entry_mode="spawn-process",
|
|
4551
|
+
available_functions=frozenset(),
|
|
4552
|
+
interaction_surface="unavailable",
|
|
4553
|
+
current_model=CurrentSessionModelAttestation.unknown(
|
|
4554
|
+
adapter.descriptor.native_provider_id
|
|
4555
|
+
),
|
|
4556
|
+
)
|
|
4557
|
+
|
|
3571
4558
|
inputs = PrepareInputs(
|
|
3572
4559
|
workspace_root=Path(args.workspace_root).resolve(),
|
|
3573
4560
|
project_root=project_root,
|
|
@@ -3580,6 +4567,9 @@ def main(argv: list[str]) -> int:
|
|
|
3580
4567
|
evidence_inputs_raw=args.evidence_inputs_raw,
|
|
3581
4568
|
directive=args.directive,
|
|
3582
4569
|
workers_override=args.workers_override,
|
|
4570
|
+
role_counts_raw=tuple(args.role_counts_raw),
|
|
4571
|
+
role_models_raw=tuple(args.role_models_raw),
|
|
4572
|
+
host_session_context=host_session_context,
|
|
3583
4573
|
lead_provider=args.lead_provider,
|
|
3584
4574
|
lead_model=args.lead_model,
|
|
3585
4575
|
claude_model=args.claude_model,
|