okstra 0.174.0 → 0.176.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -9
- package/bin/okstra +8 -4
- package/dist/cli-registry.d.mts +16 -0
- package/dist/cli-registry.mjs +560 -0
- package/dist/cli-registry.mjs.map +1 -0
- package/dist/commands/execute/agent-prompt.d.mts +1 -0
- package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
- package/dist/commands/execute/agent-prompt.mjs.map +1 -0
- package/dist/commands/execute/codex-dispatch.d.mts +3 -0
- package/dist/commands/execute/codex-dispatch.mjs +6 -0
- package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
- package/dist/commands/execute/codex-run.d.mts +3 -0
- package/{src → dist}/commands/execute/codex-run.mjs +30 -37
- package/dist/commands/execute/codex-run.mjs.map +1 -0
- package/dist/commands/execute/convergence.d.mts +1 -0
- package/{src → dist}/commands/execute/convergence.mjs +15 -16
- package/dist/commands/execute/convergence.mjs.map +1 -0
- package/dist/commands/execute/error-log.d.mts +1 -0
- package/{src → dist}/commands/execute/error-log.mjs +2 -3
- package/dist/commands/execute/error-log.mjs.map +1 -0
- package/dist/commands/execute/git-reconcile.d.mts +1 -0
- package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
- package/dist/commands/execute/git-reconcile.mjs.map +1 -0
- package/dist/commands/execute/handoff.d.mts +1 -0
- package/{src → dist}/commands/execute/handoff.mjs +10 -11
- package/dist/commands/execute/handoff.mjs.map +1 -0
- package/dist/commands/execute/incremental-carry.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
- package/dist/commands/execute/incremental-carry.mjs.map +1 -0
- package/dist/commands/execute/incremental-scope.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
- package/dist/commands/execute/incremental-scope.mjs.map +1 -0
- package/dist/commands/execute/integrate-stages.d.mts +1 -0
- package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
- package/dist/commands/execute/integrate-stages.mjs.map +1 -0
- package/dist/commands/execute/pane-title.d.mts +1 -0
- package/dist/commands/execute/pane-title.mjs +20 -0
- package/dist/commands/execute/pane-title.mjs.map +1 -0
- package/dist/commands/execute/plan-items.d.mts +1 -0
- package/dist/commands/execute/plan-items.mjs +9 -0
- package/dist/commands/execute/plan-items.mjs.map +1 -0
- package/dist/commands/execute/plan-validate.d.mts +1 -0
- package/dist/commands/execute/plan-validate.mjs +68 -0
- package/dist/commands/execute/plan-validate.mjs.map +1 -0
- package/dist/commands/execute/plan-verify.d.mts +1 -0
- package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
- package/dist/commands/execute/plan-verify.mjs.map +1 -0
- package/dist/commands/execute/render-bundle.d.mts +3 -0
- package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
- package/dist/commands/execute/render-bundle.mjs.map +1 -0
- package/dist/commands/execute/run.d.mts +24 -0
- package/dist/commands/execute/run.mjs +196 -0
- package/dist/commands/execute/run.mjs.map +1 -0
- package/dist/commands/execute/spawn-followups.d.mts +1 -0
- package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
- package/dist/commands/execute/spawn-followups.mjs.map +1 -0
- package/dist/commands/execute/team.d.mts +3 -0
- package/{src → dist}/commands/execute/team.mjs +29 -36
- package/dist/commands/execute/team.mjs.map +1 -0
- package/dist/commands/execute/token-usage.d.mts +1 -0
- package/{src → dist}/commands/execute/token-usage.mjs +2 -3
- package/dist/commands/execute/token-usage.mjs.map +1 -0
- package/dist/commands/execute/wizard.d.mts +8 -0
- package/dist/commands/execute/wizard.mjs +191 -0
- package/dist/commands/execute/wizard.mjs.map +1 -0
- package/dist/commands/execute/worker-audit-check.d.mts +1 -0
- package/{src → dist}/commands/execute/worker-audit-check.mjs +11 -12
- package/dist/commands/execute/worker-audit-check.mjs.map +1 -0
- package/dist/commands/execute/worker-dispatch.d.mts +7 -0
- package/{src → dist}/commands/execute/worker-dispatch.mjs +34 -46
- package/dist/commands/execute/worker-dispatch.mjs.map +1 -0
- package/dist/commands/execute/worker-state.d.mts +1 -0
- package/{src → dist}/commands/execute/worker-state.mjs +15 -16
- package/dist/commands/execute/worker-state.mjs.map +1 -0
- package/dist/commands/execute/worktree-lookup.d.mts +1 -0
- package/{src → dist}/commands/execute/worktree-lookup.mjs +38 -37
- package/dist/commands/execute/worktree-lookup.mjs.map +1 -0
- package/dist/commands/execute/worktree-status.d.mts +1 -0
- package/{src → dist}/commands/execute/worktree-status.mjs +52 -46
- package/dist/commands/execute/worktree-status.mjs.map +1 -0
- package/dist/commands/inspect/code-review.d.mts +1 -0
- package/{src → dist}/commands/inspect/code-review.mjs +16 -19
- package/dist/commands/inspect/code-review.mjs.map +1 -0
- package/dist/commands/inspect/container.d.mts +1 -0
- package/{src → dist}/commands/inspect/container.mjs +11 -13
- package/dist/commands/inspect/container.mjs.map +1 -0
- package/dist/commands/inspect/context-cost.d.mts +1 -0
- package/{src → dist}/commands/inspect/context-cost.mjs +11 -13
- package/dist/commands/inspect/context-cost.mjs.map +1 -0
- package/dist/commands/inspect/design-prep.d.mts +1 -0
- package/{src → dist}/commands/inspect/design-prep.mjs +10 -11
- package/dist/commands/inspect/design-prep.mjs.map +1 -0
- package/dist/commands/inspect/error-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/error-report.mjs +11 -13
- package/dist/commands/inspect/error-report.mjs.map +1 -0
- package/dist/commands/inspect/error-zip.d.mts +1 -0
- package/{src → dist}/commands/inspect/error-zip.mjs +11 -12
- package/dist/commands/inspect/error-zip.mjs.map +1 -0
- package/dist/commands/inspect/log-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/log-report.mjs +11 -13
- package/dist/commands/inspect/log-report.mjs.map +1 -0
- package/dist/commands/inspect/profile-show.d.mts +1 -0
- package/{src → dist}/commands/inspect/profile-show.mjs +10 -11
- package/dist/commands/inspect/profile-show.mjs.map +1 -0
- package/dist/commands/inspect/recap.d.mts +1 -0
- package/{src → dist}/commands/inspect/recap.mjs +11 -13
- package/dist/commands/inspect/recap.mjs.map +1 -0
- package/dist/commands/inspect/resolve-task-key.d.mts +1 -0
- package/{src → dist}/commands/inspect/resolve-task-key.mjs +11 -13
- package/dist/commands/inspect/resolve-task-key.mjs.map +1 -0
- package/dist/commands/inspect/rollup.d.mts +1 -0
- package/{src → dist}/commands/inspect/rollup.mjs +11 -13
- package/dist/commands/inspect/rollup.mjs.map +1 -0
- package/dist/commands/inspect/run-audit.d.mts +1 -0
- package/{src → dist}/commands/inspect/run-audit.mjs +11 -12
- package/dist/commands/inspect/run-audit.mjs.map +1 -0
- package/dist/commands/inspect/set-work-status.d.mts +1 -0
- package/{src → dist}/commands/inspect/set-work-status.mjs +11 -13
- package/dist/commands/inspect/set-work-status.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.d.mts +1 -0
- package/dist/commands/inspect/stage-map.mjs +110 -0
- package/dist/commands/inspect/stage-map.mjs.map +1 -0
- package/dist/commands/inspect/task-list.d.mts +1 -0
- package/dist/commands/inspect/task-list.mjs +103 -0
- package/dist/commands/inspect/task-list.mjs.map +1 -0
- package/dist/commands/inspect/task-show.d.mts +1 -0
- package/dist/commands/inspect/task-show.mjs +108 -0
- package/dist/commands/inspect/task-show.mjs.map +1 -0
- package/dist/commands/inspect/time-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/time-report.mjs +11 -13
- package/dist/commands/inspect/time-report.mjs.map +1 -0
- package/dist/commands/inspect/usage-report.d.mts +1 -0
- package/{src → dist}/commands/inspect/usage-report.mjs +11 -13
- package/dist/commands/inspect/usage-report.mjs.map +1 -0
- package/dist/commands/inspect/user-response.d.mts +1 -0
- package/{src → dist}/commands/inspect/user-response.mjs +10 -11
- package/dist/commands/inspect/user-response.mjs.map +1 -0
- package/dist/commands/inspect/worker-liveness.d.mts +1 -0
- package/{src → dist}/commands/inspect/worker-liveness.mjs +11 -13
- package/dist/commands/inspect/worker-liveness.mjs.map +1 -0
- package/dist/commands/lifecycle/check-project.d.mts +21 -0
- package/dist/commands/lifecycle/check-project.mjs +175 -0
- package/dist/commands/lifecycle/check-project.mjs.map +1 -0
- package/dist/commands/lifecycle/config.d.mts +1 -0
- package/dist/commands/lifecycle/config.mjs +369 -0
- package/dist/commands/lifecycle/config.mjs.map +1 -0
- package/dist/commands/lifecycle/doctor.d.mts +10 -0
- package/dist/commands/lifecycle/doctor.mjs +317 -0
- package/dist/commands/lifecycle/doctor.mjs.map +1 -0
- package/dist/commands/lifecycle/install.d.mts +17 -0
- package/dist/commands/lifecycle/install.mjs +1077 -0
- package/dist/commands/lifecycle/install.mjs.map +1 -0
- package/dist/commands/lifecycle/migrate.d.mts +1 -0
- package/{src → dist}/commands/lifecycle/migrate.mjs +10 -11
- package/dist/commands/lifecycle/migrate.mjs.map +1 -0
- package/dist/commands/lifecycle/model.d.mts +1 -0
- package/dist/commands/lifecycle/model.mjs +22 -0
- package/dist/commands/lifecycle/model.mjs.map +1 -0
- package/dist/commands/lifecycle/paths.d.mts +1 -0
- package/dist/commands/lifecycle/paths.mjs +102 -0
- package/dist/commands/lifecycle/paths.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.d.mts +1 -0
- package/dist/commands/lifecycle/preflight.mjs +124 -0
- package/dist/commands/lifecycle/preflight.mjs.map +1 -0
- package/dist/commands/lifecycle/setup.d.mts +1 -0
- package/dist/commands/lifecycle/setup.mjs +297 -0
- package/dist/commands/lifecycle/setup.mjs.map +1 -0
- package/dist/commands/lifecycle/uninstall.d.mts +4 -0
- package/dist/commands/lifecycle/uninstall.mjs +262 -0
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -0
- package/dist/commands/manager.d.mts +3 -0
- package/{src → dist}/commands/manager.mjs +25 -32
- package/dist/commands/manager.mjs.map +1 -0
- package/dist/commands/memory/memory.d.mts +1 -0
- package/dist/commands/memory/memory.mjs +569 -0
- package/dist/commands/memory/memory.mjs.map +1 -0
- package/dist/commands/pr/pr.d.mts +33 -0
- package/dist/commands/pr/pr.mjs +296 -0
- package/dist/commands/pr/pr.mjs.map +1 -0
- package/dist/commands/report/agent-activity.d.mts +1 -0
- package/{src → dist}/commands/report/agent-activity.mjs +11 -12
- package/dist/commands/report/agent-activity.mjs.map +1 -0
- package/dist/commands/report/finalize.d.mts +4 -0
- package/{src → dist}/commands/report/finalize.mjs +25 -32
- package/dist/commands/report/finalize.mjs.map +1 -0
- package/dist/commands/report/inject-report-index.d.mts +1 -0
- package/{src → dist}/commands/report/inject-report-index.mjs +6 -7
- package/dist/commands/report/inject-report-index.mjs.map +1 -0
- package/dist/commands/report/render-final-report.d.mts +1 -0
- package/{src → dist}/commands/report/render-final-report.mjs +6 -7
- package/dist/commands/report/render-final-report.mjs.map +1 -0
- package/dist/commands/report/render-views.d.mts +1 -0
- package/{src → dist}/commands/report/render-views.mjs +6 -7
- package/dist/commands/report/render-views.mjs.map +1 -0
- package/dist/commands/report/translate.d.mts +1 -0
- package/{src → dist}/commands/report/translate.mjs +6 -7
- package/dist/commands/report/translate.mjs.map +1 -0
- package/dist/lib/helper-scripts.d.mts +3 -0
- package/{src → dist}/lib/helper-scripts.mjs +11 -14
- package/dist/lib/helper-scripts.mjs.map +1 -0
- package/dist/lib/host-registry-client.d.mts +25 -0
- package/dist/lib/host-registry-client.mjs +152 -0
- package/dist/lib/host-registry-client.mjs.map +1 -0
- package/dist/lib/install-assets.d.mts +8 -0
- package/{src → dist}/lib/install-assets.mjs +17 -20
- package/dist/lib/install-assets.mjs.map +1 -0
- package/dist/lib/okstra-dirs.d.mts +4 -0
- package/{src → dist}/lib/okstra-dirs.mjs +3 -6
- package/dist/lib/okstra-dirs.mjs.map +1 -0
- package/dist/lib/paths.d.mts +4 -0
- package/dist/lib/paths.mjs +78 -0
- package/dist/lib/paths.mjs.map +1 -0
- package/dist/lib/proc.d.mts +3 -0
- package/dist/lib/proc.mjs +31 -0
- package/dist/lib/proc.mjs.map +1 -0
- package/dist/lib/python-helper.d.mts +8 -0
- package/dist/lib/python-helper.mjs +123 -0
- package/dist/lib/python-helper.mjs.map +1 -0
- package/dist/lib/runtime-manifest.d.mts +7 -0
- package/dist/lib/runtime-manifest.mjs +73 -0
- package/dist/lib/runtime-manifest.mjs.map +1 -0
- package/dist/lib/skill-catalog.d.mts +4 -0
- package/dist/lib/skill-catalog.mjs +50 -0
- package/dist/lib/skill-catalog.mjs.map +1 -0
- package/dist/lib/types.d.mts +129 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/lib/types.mjs.map +1 -0
- package/dist/lib/version.d.mts +2 -0
- package/dist/lib/version.mjs +27 -0
- package/dist/lib/version.mjs.map +1 -0
- package/docs/architecture/storage-model.md +6 -2
- package/docs/architecture.md +29 -20
- package/docs/cli.md +40 -16
- package/docs/contributor-change-matrix.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -3
- package/docs/for-ai/skills/okstra-rollup.md +1 -0
- package/docs/project-structure-overview.md +72 -62
- package/docs/task-process/README.md +12 -8
- package/docs/task-process/common-flow.md +13 -16
- package/docs/task-process/error-analysis.md +9 -10
- package/docs/task-process/final-verification.md +7 -7
- package/docs/task-process/implementation-planning.md +9 -9
- package/docs/task-process/implementation.md +6 -6
- package/docs/task-process/release-handoff.md +8 -7
- package/docs/task-process/requirements-discovery.md +8 -8
- package/package.json +11 -5
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +2 -1
- package/runtime/agents/workers/report-writer-worker.md +3 -1
- package/runtime/agents/workers/translator-worker.md +3 -1
- package/runtime/bin/lib/okstra/cli.sh +12 -0
- package/runtime/bin/lib/okstra/globals.sh +3 -0
- package/runtime/bin/lib/okstra/interactive.sh +12 -6
- package/runtime/bin/lib/okstra/usage.sh +7 -6
- package/runtime/bin/okstra-error-log.py +27 -0
- package/runtime/bin/okstra-provider-exec.py +217 -5
- package/runtime/bin/okstra-spawn-followups.py +4 -2
- package/runtime/bin/okstra.sh +10 -0
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/lead/context-loader.md +1 -2
- package/runtime/prompts/lead/convergence.md +27 -18
- package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
- package/runtime/prompts/lead/plan-body-verification.md +10 -3
- package/runtime/prompts/lead/report-writer.md +15 -1
- package/runtime/prompts/lead/team-contract.md +17 -13
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +0 -3
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -7
- package/runtime/prompts/profiles/change-impact-analysis.md +20 -0
- package/runtime/prompts/profiles/error-analysis.md +27 -1
- package/runtime/prompts/profiles/feature-analysis.md +20 -0
- package/runtime/prompts/profiles/final-verification.md +20 -1
- package/runtime/prompts/profiles/implementation-option-selection.md +20 -0
- package/runtime/prompts/profiles/implementation-planning.md +26 -0
- package/runtime/prompts/profiles/implementation.md +19 -0
- package/runtime/prompts/profiles/improvement-discovery.md +21 -1
- package/runtime/prompts/profiles/project-analysis.md +20 -0
- package/runtime/prompts/profiles/release-handoff.md +4 -0
- package/runtime/prompts/profiles/requirements-discovery.md +27 -1
- package/runtime/prompts/wizard/prompts.ko.json +35 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +95 -25
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
- package/runtime/python/okstra_ctl/adapters/runtime/__init__.py +1 -0
- package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +27 -0
- package/runtime/python/okstra_ctl/adapters/runtime/cli_wrapper.py +49 -0
- package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +74 -0
- package/runtime/python/okstra_ctl/agent_activity.py +5 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
- package/runtime/python/okstra_ctl/application/open_worker.py +29 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
- package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
- package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
- package/runtime/python/okstra_ctl/convergence.py +409 -3
- package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
- package/runtime/python/okstra_ctl/convergence_store.py +170 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +1204 -155
- package/runtime/python/okstra_ctl/dispatch_state.py +696 -11
- package/runtime/python/okstra_ctl/doctor.py +14 -0
- package/runtime/python/okstra_ctl/domain/host.py +34 -1
- package/runtime/python/okstra_ctl/domain/provider.py +212 -6
- package/runtime/python/okstra_ctl/domain/role.py +125 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +18 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +34 -1
- package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +44 -0
- package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
- package/runtime/python/okstra_ctl/error_log_write.py +9 -0
- package/runtime/python/okstra_ctl/error_report.py +21 -1
- package/runtime/python/okstra_ctl/execution_identity.py +529 -0
- package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
- package/runtime/python/okstra_ctl/implementation_outcome.py +21 -5
- package/runtime/python/okstra_ctl/index.py +6 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
- package/runtime/python/okstra_ctl/legacy_model_selection.py +578 -0
- package/runtime/python/okstra_ctl/manager_sync.py +4 -1
- package/runtime/python/okstra_ctl/model_cli.py +278 -0
- package/runtime/python/okstra_ctl/model_defaults.py +46 -0
- package/runtime/python/okstra_ctl/model_discovery.py +26 -7
- package/runtime/python/okstra_ctl/model_pool.py +501 -0
- package/runtime/python/okstra_ctl/models.py +47 -27
- package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
- package/runtime/python/okstra_ctl/next_phase.py +236 -0
- package/runtime/python/okstra_ctl/pane_title.py +151 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
- package/runtime/python/okstra_ctl/ports/worker_runtime.py +26 -0
- package/runtime/python/okstra_ctl/recap.py +4 -1
- package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
- package/runtime/python/okstra_ctl/render.py +124 -57
- package/runtime/python/okstra_ctl/render_final_report.py +19 -0
- package/runtime/python/okstra_ctl/report_contract.py +39 -0
- package/runtime/python/okstra_ctl/report_finalize.py +16 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -0
- package/runtime/python/okstra_ctl/role_requirements.py +210 -0
- package/runtime/python/okstra_ctl/rollup.py +4 -1
- package/runtime/python/okstra_ctl/run.py +1210 -258
- package/runtime/python/okstra_ctl/run_index_row.py +7 -1
- package/runtime/python/okstra_ctl/stage_fix_carry.py +17 -1
- package/runtime/python/okstra_ctl/team.py +14 -18
- package/runtime/python/okstra_ctl/time_report.py +38 -4
- package/runtime/python/okstra_ctl/usage_identity.py +86 -0
- package/runtime/python/okstra_ctl/usage_report.py +15 -3
- package/runtime/python/okstra_ctl/wizard.py +1449 -66
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
- package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -2
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +40 -9
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +16 -1
- package/runtime/python/okstra_ctl/worker_request.py +7 -1
- package/runtime/python/okstra_ctl/worker_runner.py +130 -5
- package/runtime/python/okstra_ctl/workers.py +7 -5
- package/runtime/python/okstra_ctl/workflow.py +18 -32
- package/runtime/python/okstra_ctl/worktree.py +3 -3
- package/runtime/python/okstra_ctl/worktree_registry.py +5 -4
- package/runtime/python/okstra_ctl/write_policy.py +571 -0
- package/runtime/python/okstra_project/state.py +54 -6
- package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
- package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
- package/runtime/schemas/final-report-v2.0.schema.json +68 -5
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -0
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +15 -13
- package/runtime/skills/okstra-rollup/SKILL.md +1 -0
- package/runtime/skills/okstra-run/SKILL.md +14 -14
- package/runtime/templates/implementation-worker-preamble.md +3 -18
- package/runtime/templates/project-docs/task-index.template.md +0 -1
- package/runtime/templates/report-writer-prompt-preamble.md +2 -0
- package/runtime/templates/reports/final-report-v2.template.md +7 -1
- package/runtime/templates/reports/html/base.template.html +21 -0
- package/runtime/templates/reports/html/i18n/en.json +4 -0
- package/runtime/templates/reports/html/i18n/ko.json +4 -0
- package/runtime/templates/reports/html/macros/forms.html +5 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +1 -1
- package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
- package/runtime/templates/worker-prompt-preamble.md +6 -35
- package/runtime/validators/lib/fixtures.sh +49 -16
- package/runtime/validators/validate-run.py +486 -134
- package/runtime/validators/validate_session_conformance.py +25 -7
- package/src/cli-registry.mjs +0 -539
- package/src/commands/execute/codex-dispatch.mjs +0 -10
- package/src/commands/execute/plan-items.mjs +0 -9
- package/src/commands/execute/plan-validate.mjs +0 -68
- package/src/commands/execute/run.mjs +0 -208
- package/src/commands/execute/wizard.mjs +0 -167
- package/src/commands/inspect/stage-map.mjs +0 -98
- package/src/commands/inspect/task-list.mjs +0 -97
- package/src/commands/inspect/task-show.mjs +0 -100
- package/src/commands/lifecycle/check-project.mjs +0 -187
- package/src/commands/lifecycle/config.mjs +0 -385
- package/src/commands/lifecycle/doctor.mjs +0 -308
- package/src/commands/lifecycle/install.mjs +0 -1170
- package/src/commands/lifecycle/paths.mjs +0 -104
- package/src/commands/lifecycle/preflight.mjs +0 -125
- package/src/commands/lifecycle/setup.mjs +0 -333
- package/src/commands/lifecycle/uninstall.mjs +0 -279
- package/src/commands/memory/memory.mjs +0 -524
- package/src/commands/pr/pr.mjs +0 -261
- package/src/lib/host-registry-client.mjs +0 -176
- package/src/lib/paths.mjs +0 -86
- package/src/lib/proc.mjs +0 -31
- package/src/lib/python-helper.mjs +0 -142
- package/src/lib/runtime-manifest.mjs +0 -70
- package/src/lib/skill-catalog.mjs +0 -53
- package/src/lib/version.mjs +0 -20
- /package/{src → dist}/commands/pr/default.md +0 -0
|
@@ -20,6 +20,7 @@ imports this module directly.
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import copy
|
|
23
|
+
import hashlib
|
|
23
24
|
import json
|
|
24
25
|
import math
|
|
25
26
|
import re
|
|
@@ -31,7 +32,16 @@ from typing import Any, Callable, Optional
|
|
|
31
32
|
|
|
32
33
|
from okstra_ctl.application.advance_wizard import plan_prompt
|
|
33
34
|
from okstra_ctl.application.resolve_assignment import resolve_lead_provider
|
|
35
|
+
from okstra_ctl.assignment_environment import load_assignment_context
|
|
36
|
+
from okstra_ctl.assignment_resolver import (
|
|
37
|
+
AssignmentContext,
|
|
38
|
+
AssignmentResolutionError,
|
|
39
|
+
RoleInstance,
|
|
40
|
+
resolve_assignments,
|
|
41
|
+
resolve_model_assignment,
|
|
42
|
+
)
|
|
34
43
|
from okstra_ctl.domain.host import (
|
|
44
|
+
CurrentSessionModelAttestation,
|
|
35
45
|
HostCapabilityMismatch,
|
|
36
46
|
HostNotRegistered,
|
|
37
47
|
HostSessionContext,
|
|
@@ -44,8 +54,19 @@ from okstra_ctl.domain.wizard.interaction import (
|
|
|
44
54
|
WizardPrompt,
|
|
45
55
|
normalize_planned_answer,
|
|
46
56
|
)
|
|
57
|
+
from okstra_ctl.dispatch_state import detect_terminal_backend
|
|
47
58
|
from okstra_ctl.registry.host_registry import default_host_registry
|
|
48
59
|
from okstra_ctl.registry.provider_registry import default_provider_registry
|
|
60
|
+
from okstra_ctl.model_defaults import ModelDefaultScopes, default_candidates
|
|
61
|
+
from okstra_ctl.model_pool import ModelPool
|
|
62
|
+
from okstra_ctl import next_phase
|
|
63
|
+
from okstra_ctl.legacy_model_selection import serialize_host_session_context
|
|
64
|
+
from okstra_ctl.role_requirements import (
|
|
65
|
+
RoleProfile,
|
|
66
|
+
RoleProfileError,
|
|
67
|
+
RoleRequirement,
|
|
68
|
+
load_role_profile,
|
|
69
|
+
)
|
|
49
70
|
from okstra_ctl.ids import slugify_task_segment
|
|
50
71
|
from okstra_ctl.brief_frontmatter import read_brief_frontmatter
|
|
51
72
|
from okstra_ctl.analysis_inputs import (
|
|
@@ -72,6 +93,7 @@ from okstra_ctl.clarification_items import (
|
|
|
72
93
|
sidecar_answers,
|
|
73
94
|
user_response_sidecars,
|
|
74
95
|
)
|
|
96
|
+
from okstra_ctl.convergence_store import write_json_atomic
|
|
75
97
|
from okstra_ctl.incremental_scope import (
|
|
76
98
|
parse_stage_graph,
|
|
77
99
|
preview_link_availability_for_report,
|
|
@@ -96,8 +118,10 @@ from okstra_ctl.run import (
|
|
|
96
118
|
_apply_cli_implementation_option,
|
|
97
119
|
_extract_frontmatter_block,
|
|
98
120
|
_load_final_report_data_if_present,
|
|
121
|
+
_assignment_resolution_message,
|
|
99
122
|
_reject_blocking_plan_body_gate,
|
|
100
123
|
_set_data_json_approved_true_if_present,
|
|
124
|
+
_model_default_scopes,
|
|
101
125
|
recommended_role_models,
|
|
102
126
|
)
|
|
103
127
|
from okstra_ctl.stage_map import (
|
|
@@ -406,6 +430,12 @@ _STEP_TO_GROUP: dict[str, str] = {
|
|
|
406
430
|
|
|
407
431
|
@dataclass
|
|
408
432
|
class WizardState:
|
|
433
|
+
# execution identity v2: role-first canonical model selections
|
|
434
|
+
execution_identity_version: int = 1
|
|
435
|
+
role_counts: dict[str, int] = field(default_factory=dict)
|
|
436
|
+
role_models: dict[str, list[str]] = field(default_factory=dict)
|
|
437
|
+
role_selection_order: list[str] = field(default_factory=list)
|
|
438
|
+
|
|
409
439
|
# bootstrap
|
|
410
440
|
workspace_root: str = ""
|
|
411
441
|
project_root: str = ""
|
|
@@ -524,11 +554,19 @@ class WizardState:
|
|
|
524
554
|
answered: list[str] = field(default_factory=list)
|
|
525
555
|
|
|
526
556
|
def to_json(self) -> dict[str, Any]:
|
|
527
|
-
|
|
557
|
+
payload = asdict(self)
|
|
558
|
+
payload.pop("execution_identity_version")
|
|
559
|
+
payload["executionIdentityVersion"] = 2
|
|
560
|
+
payload["roleCounts"] = payload.pop("role_counts")
|
|
561
|
+
payload["roleModels"] = payload.pop("role_models")
|
|
562
|
+
payload["roleSelectionOrder"] = payload.pop("role_selection_order")
|
|
563
|
+
for field_name in _V1_PROVIDER_STATE_FIELDS:
|
|
564
|
+
payload.pop(field_name, None)
|
|
565
|
+
return payload
|
|
528
566
|
|
|
529
567
|
@classmethod
|
|
530
568
|
def from_json(cls, d: dict[str, Any]) -> "WizardState":
|
|
531
|
-
return
|
|
569
|
+
return _wizard_state_from_json(d)
|
|
532
570
|
|
|
533
571
|
|
|
534
572
|
@dataclass
|
|
@@ -550,8 +588,14 @@ class Prompt:
|
|
|
550
588
|
# only meaningful when kind == "pick_group": one entry per AskUserQuestion tab
|
|
551
589
|
questions: list["Prompt"] = field(default_factory=list)
|
|
552
590
|
|
|
591
|
+
@property
|
|
592
|
+
def id(self) -> str:
|
|
593
|
+
"""Stable public identifier shared by static and role-instance prompts."""
|
|
594
|
+
return self.step
|
|
595
|
+
|
|
553
596
|
def to_json(self) -> dict[str, Any]:
|
|
554
597
|
out = {
|
|
598
|
+
"id": self.step,
|
|
555
599
|
"step": self.step,
|
|
556
600
|
"kind": self.kind,
|
|
557
601
|
"label": self.label,
|
|
@@ -959,6 +1003,981 @@ def _profile_path(workspace_root: Path, task_type: str) -> Path:
|
|
|
959
1003
|
return workspace_root / "prompts" / "profiles" / f"{task_type}.md"
|
|
960
1004
|
|
|
961
1005
|
|
|
1006
|
+
_V2_STATE_FIELDS = (
|
|
1007
|
+
"roleCounts",
|
|
1008
|
+
"roleModels",
|
|
1009
|
+
"roleSelectionOrder",
|
|
1010
|
+
)
|
|
1011
|
+
_V1_PROVIDER_STATE_FIELDS = (
|
|
1012
|
+
"workers_override",
|
|
1013
|
+
"lead_provider",
|
|
1014
|
+
"lead_model",
|
|
1015
|
+
"claude_model",
|
|
1016
|
+
"codex_model",
|
|
1017
|
+
"antigravity_model",
|
|
1018
|
+
"grok_model",
|
|
1019
|
+
"kimi_model",
|
|
1020
|
+
"report_writer_provider",
|
|
1021
|
+
"report_writer_model",
|
|
1022
|
+
"executor",
|
|
1023
|
+
"critic",
|
|
1024
|
+
)
|
|
1025
|
+
_V1_PROVIDER_STATE_JSON_FIELDS = frozenset({
|
|
1026
|
+
*_V1_PROVIDER_STATE_FIELDS,
|
|
1027
|
+
"workers",
|
|
1028
|
+
"leadProvider",
|
|
1029
|
+
"leadModel",
|
|
1030
|
+
"claudeModel",
|
|
1031
|
+
"codexModel",
|
|
1032
|
+
"antigravityModel",
|
|
1033
|
+
"grokModel",
|
|
1034
|
+
"kimiModel",
|
|
1035
|
+
"reportWriterProvider",
|
|
1036
|
+
"reportWriterModel",
|
|
1037
|
+
})
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def _load_role_profile_for_state(state: WizardState) -> RoleProfile:
|
|
1041
|
+
try:
|
|
1042
|
+
return load_role_profile(
|
|
1043
|
+
_profile_path(Path(state.workspace_root), state.task_type)
|
|
1044
|
+
)
|
|
1045
|
+
except RoleProfileError as exc:
|
|
1046
|
+
raise WizardError(str(exc)) from exc
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _role_selection_enabled(state: WizardState) -> bool:
|
|
1050
|
+
if (
|
|
1051
|
+
state.execution_identity_version != 2
|
|
1052
|
+
or not state.workspace_root
|
|
1053
|
+
or not state.task_type
|
|
1054
|
+
):
|
|
1055
|
+
return False
|
|
1056
|
+
try:
|
|
1057
|
+
load_role_profile(_profile_path(Path(state.workspace_root), state.task_type))
|
|
1058
|
+
except (OSError, RoleProfileError):
|
|
1059
|
+
return False
|
|
1060
|
+
return True
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _legacy_value(payload: dict[str, Any], snake: str, camel: str) -> Any:
|
|
1064
|
+
return payload.get(snake, payload.get(camel, ""))
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _legacy_model_ref(
|
|
1068
|
+
pool: ModelPool,
|
|
1069
|
+
provider: str,
|
|
1070
|
+
role: str,
|
|
1071
|
+
raw_model: Any,
|
|
1072
|
+
) -> str | None:
|
|
1073
|
+
provider_id = provider.strip().lower()
|
|
1074
|
+
if not provider_id:
|
|
1075
|
+
return None
|
|
1076
|
+
model_value = raw_model.strip() if isinstance(raw_model, str) else ""
|
|
1077
|
+
try:
|
|
1078
|
+
model = (
|
|
1079
|
+
pool.resolve_alias(provider_id, model_value)
|
|
1080
|
+
if model_value
|
|
1081
|
+
else pool.default_candidate(provider_id, role)
|
|
1082
|
+
)
|
|
1083
|
+
except (ValueError, RuntimeError):
|
|
1084
|
+
return None
|
|
1085
|
+
if model is None:
|
|
1086
|
+
return None
|
|
1087
|
+
availability = pool.availability(str(model.model_ref), role)
|
|
1088
|
+
return str(model.model_ref) if availability.available else None
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def _legacy_worker_providers(payload: dict[str, Any]) -> list[str]:
|
|
1092
|
+
raw = _legacy_value(payload, "workers_override", "workers")
|
|
1093
|
+
if isinstance(raw, list):
|
|
1094
|
+
values = raw
|
|
1095
|
+
elif isinstance(raw, str):
|
|
1096
|
+
values = raw.split(",")
|
|
1097
|
+
else:
|
|
1098
|
+
values = []
|
|
1099
|
+
return [
|
|
1100
|
+
value.strip().lower()
|
|
1101
|
+
for value in values
|
|
1102
|
+
if isinstance(value, str)
|
|
1103
|
+
and value.strip()
|
|
1104
|
+
and value.strip().lower() != "report-writer"
|
|
1105
|
+
]
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _initial_cross_verification_requirement(
|
|
1109
|
+
profile: RoleProfile,
|
|
1110
|
+
) -> RoleRequirement | None:
|
|
1111
|
+
return next((
|
|
1112
|
+
requirement
|
|
1113
|
+
for requirement in profile.roles
|
|
1114
|
+
if not requirement.dynamic
|
|
1115
|
+
and requirement.role in {
|
|
1116
|
+
"analyser",
|
|
1117
|
+
"designer",
|
|
1118
|
+
"planner",
|
|
1119
|
+
"verifier",
|
|
1120
|
+
}
|
|
1121
|
+
), None)
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def _convert_v1_provider_selections(
|
|
1125
|
+
state: WizardState,
|
|
1126
|
+
payload: dict[str, Any],
|
|
1127
|
+
) -> None:
|
|
1128
|
+
if not _role_selection_enabled(state):
|
|
1129
|
+
return
|
|
1130
|
+
profile = _load_role_profile_for_state(state)
|
|
1131
|
+
pool = ModelPool.from_registry(default_provider_registry())
|
|
1132
|
+
cross_requirement = _initial_cross_verification_requirement(profile)
|
|
1133
|
+
providers = _legacy_worker_providers(payload)
|
|
1134
|
+
if cross_requirement is not None:
|
|
1135
|
+
selected_count = cross_requirement.recommended_count
|
|
1136
|
+
if providers and cross_requirement.min_count < cross_requirement.max_count:
|
|
1137
|
+
selected_count = min(
|
|
1138
|
+
cross_requirement.max_count,
|
|
1139
|
+
max(cross_requirement.min_count, len(providers)),
|
|
1140
|
+
)
|
|
1141
|
+
if cross_requirement.min_count < cross_requirement.max_count:
|
|
1142
|
+
state.role_counts[cross_requirement.role] = selected_count
|
|
1143
|
+
count_id = f"role-count:{cross_requirement.role}"
|
|
1144
|
+
state.role_selection_order.append(count_id)
|
|
1145
|
+
if not providers:
|
|
1146
|
+
providers = [
|
|
1147
|
+
model.provider_id
|
|
1148
|
+
for model in pool.default_candidates(cross_requirement.role)
|
|
1149
|
+
]
|
|
1150
|
+
for ordinal, provider in enumerate(providers[:selected_count], start=1):
|
|
1151
|
+
raw_model = _legacy_value(
|
|
1152
|
+
payload,
|
|
1153
|
+
f"{provider}_model",
|
|
1154
|
+
f"{provider}Model",
|
|
1155
|
+
)
|
|
1156
|
+
model_ref = _legacy_model_ref(
|
|
1157
|
+
pool,
|
|
1158
|
+
provider,
|
|
1159
|
+
cross_requirement.role,
|
|
1160
|
+
raw_model,
|
|
1161
|
+
)
|
|
1162
|
+
if model_ref is None:
|
|
1163
|
+
break
|
|
1164
|
+
state.role_models.setdefault(cross_requirement.role, []).append(model_ref)
|
|
1165
|
+
state.role_selection_order.append(
|
|
1166
|
+
f"role-model:{cross_requirement.role}:{ordinal}"
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
provider_rows = (
|
|
1170
|
+
(
|
|
1171
|
+
"leader",
|
|
1172
|
+
_legacy_value(payload, "lead_provider", "leadProvider") or "claude",
|
|
1173
|
+
_legacy_value(payload, "lead_model", "leadModel"),
|
|
1174
|
+
),
|
|
1175
|
+
(
|
|
1176
|
+
"report-writer",
|
|
1177
|
+
_legacy_value(
|
|
1178
|
+
payload,
|
|
1179
|
+
"report_writer_provider",
|
|
1180
|
+
"reportWriterProvider",
|
|
1181
|
+
) or "claude",
|
|
1182
|
+
_legacy_value(
|
|
1183
|
+
payload,
|
|
1184
|
+
"report_writer_model",
|
|
1185
|
+
"reportWriterModel",
|
|
1186
|
+
),
|
|
1187
|
+
),
|
|
1188
|
+
(
|
|
1189
|
+
"implementer",
|
|
1190
|
+
_legacy_value(payload, "executor", "executor") or "claude",
|
|
1191
|
+
_legacy_value(
|
|
1192
|
+
payload,
|
|
1193
|
+
f"{_legacy_value(payload, 'executor', 'executor') or 'claude'}_model",
|
|
1194
|
+
f"{_legacy_value(payload, 'executor', 'executor') or 'claude'}Model",
|
|
1195
|
+
),
|
|
1196
|
+
),
|
|
1197
|
+
(
|
|
1198
|
+
"critic",
|
|
1199
|
+
_legacy_value(payload, "critic", "critic"),
|
|
1200
|
+
_legacy_value(
|
|
1201
|
+
payload,
|
|
1202
|
+
f"{_legacy_value(payload, 'critic', 'critic')}_model",
|
|
1203
|
+
f"{_legacy_value(payload, 'critic', 'critic')}Model",
|
|
1204
|
+
),
|
|
1205
|
+
),
|
|
1206
|
+
)
|
|
1207
|
+
static_roles = {
|
|
1208
|
+
requirement.role
|
|
1209
|
+
for requirement in profile.roles
|
|
1210
|
+
if not requirement.dynamic and requirement.min_count > 0
|
|
1211
|
+
}
|
|
1212
|
+
if state.host_entry_mode == "spawn-process":
|
|
1213
|
+
static_roles.add("leader")
|
|
1214
|
+
for role, provider, raw_model in provider_rows:
|
|
1215
|
+
if role not in static_roles or not isinstance(provider, str) or not provider:
|
|
1216
|
+
continue
|
|
1217
|
+
model_ref = _legacy_model_ref(pool, provider, role, raw_model)
|
|
1218
|
+
if model_ref is None:
|
|
1219
|
+
continue
|
|
1220
|
+
state.role_models[role] = [model_ref]
|
|
1221
|
+
state.role_selection_order.append(f"role-model:{role}:1")
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def _wizard_state_from_json(payload: dict[str, Any]) -> WizardState:
|
|
1225
|
+
if not isinstance(payload, dict):
|
|
1226
|
+
raise WizardError("wizard state must be a JSON object")
|
|
1227
|
+
data = dict(payload)
|
|
1228
|
+
version = data.pop("executionIdentityVersion", None)
|
|
1229
|
+
has_v2_fields = any(field_name in data for field_name in _V2_STATE_FIELDS)
|
|
1230
|
+
has_v1_fields = any(
|
|
1231
|
+
field_name in data for field_name in _V1_PROVIDER_STATE_JSON_FIELDS
|
|
1232
|
+
)
|
|
1233
|
+
if version == 2:
|
|
1234
|
+
if has_v1_fields or not all(field_name in data for field_name in _V2_STATE_FIELDS):
|
|
1235
|
+
raise WizardError("mixed wizard state versions are not allowed")
|
|
1236
|
+
data["role_counts"] = data.pop("roleCounts")
|
|
1237
|
+
data["role_models"] = data.pop("roleModels")
|
|
1238
|
+
data["role_selection_order"] = data.pop("roleSelectionOrder")
|
|
1239
|
+
data["execution_identity_version"] = 2
|
|
1240
|
+
_validate_v2_state_fields(data)
|
|
1241
|
+
elif version in (None, 1):
|
|
1242
|
+
if has_v2_fields:
|
|
1243
|
+
raise WizardError("mixed wizard state versions are not allowed")
|
|
1244
|
+
data.pop("execution_identity_version", None)
|
|
1245
|
+
else:
|
|
1246
|
+
raise WizardError(f"unsupported wizard state version: {version!r}")
|
|
1247
|
+
allowed = WizardState.__dataclass_fields__.keys()
|
|
1248
|
+
state = WizardState(**{key: value for key, value in data.items() if key in allowed})
|
|
1249
|
+
if version != 2:
|
|
1250
|
+
state.execution_identity_version = 2
|
|
1251
|
+
_convert_v1_provider_selections(state, payload)
|
|
1252
|
+
# v1 은 리더 확인 칸이 없었다. 변환 재개 시 다시 묻지 않는다.
|
|
1253
|
+
if (
|
|
1254
|
+
state.host_entry_mode == "current-session"
|
|
1255
|
+
and _LEADER_SESSION_STEP not in state.answered
|
|
1256
|
+
):
|
|
1257
|
+
state.answered.append(_LEADER_SESSION_STEP)
|
|
1258
|
+
if (
|
|
1259
|
+
state.host_entry_mode == "current-session"
|
|
1260
|
+
and _LEADER_SESSION_STEP not in state.role_selection_order
|
|
1261
|
+
):
|
|
1262
|
+
state.role_selection_order.insert(0, _LEADER_SESSION_STEP)
|
|
1263
|
+
return state
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def _validate_v2_state_fields(data: dict[str, Any]) -> None:
|
|
1267
|
+
role_counts = data.get("role_counts")
|
|
1268
|
+
role_models = data.get("role_models")
|
|
1269
|
+
selection_order = data.get("role_selection_order")
|
|
1270
|
+
if not isinstance(role_counts, dict) or not all(
|
|
1271
|
+
isinstance(role, str)
|
|
1272
|
+
and isinstance(count, int)
|
|
1273
|
+
and not isinstance(count, bool)
|
|
1274
|
+
for role, count in role_counts.items()
|
|
1275
|
+
):
|
|
1276
|
+
raise WizardError("wizard state roleCounts must map roles to integers")
|
|
1277
|
+
if not isinstance(role_models, dict) or not all(
|
|
1278
|
+
isinstance(role, str)
|
|
1279
|
+
and isinstance(models, list)
|
|
1280
|
+
and all(isinstance(model_ref, str) for model_ref in models)
|
|
1281
|
+
for role, models in role_models.items()
|
|
1282
|
+
):
|
|
1283
|
+
raise WizardError("wizard state roleModels must map roles to modelRef arrays")
|
|
1284
|
+
if not isinstance(selection_order, list) or not all(
|
|
1285
|
+
isinstance(step_id, str) for step_id in selection_order
|
|
1286
|
+
):
|
|
1287
|
+
raise WizardError("wizard state roleSelectionOrder must be a string array")
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def _role_count_prompt_id(role: str) -> str:
|
|
1291
|
+
return f"role-count:{role}"
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def _role_add_prompt_id(role: str) -> str:
|
|
1295
|
+
return f"role-add:{role}"
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
def _role_model_prompt_id(role: str, ordinal: int) -> str:
|
|
1299
|
+
return f"role-model:{role}:{ordinal}"
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
_LEADER_SESSION_STEP = "leader-session"
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def _is_role_selection_step(step_id: str) -> bool:
|
|
1306
|
+
return step_id == _LEADER_SESSION_STEP or step_id.startswith(
|
|
1307
|
+
("role-count:", "role-model:", "role-add:")
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def _selected_role_count(
|
|
1312
|
+
state: WizardState,
|
|
1313
|
+
requirement: RoleRequirement,
|
|
1314
|
+
) -> int:
|
|
1315
|
+
if requirement.min_count == requirement.max_count:
|
|
1316
|
+
return requirement.min_count
|
|
1317
|
+
if requirement.role in state.role_counts:
|
|
1318
|
+
return state.role_counts[requirement.role]
|
|
1319
|
+
# min=0 선택 역할(예: critic)은 사용자가 열기 전까지 기본 0
|
|
1320
|
+
if requirement.min_count == 0:
|
|
1321
|
+
return 0
|
|
1322
|
+
return requirement.recommended_count
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
def _selectable_static_requirements(
|
|
1326
|
+
state: WizardState,
|
|
1327
|
+
profile: RoleProfile,
|
|
1328
|
+
) -> tuple[RoleRequirement, ...]:
|
|
1329
|
+
requirements: list[RoleRequirement] = []
|
|
1330
|
+
if state.host_entry_mode == "spawn-process":
|
|
1331
|
+
requirements.append(RoleRequirement("leader", 1, 1, 1, "lead"))
|
|
1332
|
+
requirements.extend(
|
|
1333
|
+
requirement
|
|
1334
|
+
for requirement in profile.roles
|
|
1335
|
+
if not requirement.dynamic
|
|
1336
|
+
and _selected_role_count(state, requirement) > 0
|
|
1337
|
+
)
|
|
1338
|
+
return tuple(requirements)
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def _leader_session_prompt(state: WizardState) -> Prompt:
|
|
1342
|
+
"""current-session 리더 칸: 모델/effort 표시만 하고 role_models 에 쓰지 않는다."""
|
|
1343
|
+
attestation = _host_session_context(state).current_model
|
|
1344
|
+
model_ref = (
|
|
1345
|
+
attestation.normalized_model_ref
|
|
1346
|
+
or attestation.observed_model
|
|
1347
|
+
or "current-session"
|
|
1348
|
+
)
|
|
1349
|
+
steps = _load_wizard_root(state.workspace_root)["steps"]
|
|
1350
|
+
raw = steps.get("leader_session") or {}
|
|
1351
|
+
effort_suffix = ""
|
|
1352
|
+
if attestation.effort:
|
|
1353
|
+
effort_template = raw.get("effort_suffix", " · effort {effort}")
|
|
1354
|
+
effort_suffix = effort_template.format(effort=attestation.effort)
|
|
1355
|
+
prompt = _p(
|
|
1356
|
+
state.workspace_root,
|
|
1357
|
+
"leader_session",
|
|
1358
|
+
model_ref=model_ref,
|
|
1359
|
+
effort_suffix=effort_suffix,
|
|
1360
|
+
)
|
|
1361
|
+
continue_label = prompt["options"].get("continue", "계속")
|
|
1362
|
+
return Prompt(
|
|
1363
|
+
step=_LEADER_SESSION_STEP,
|
|
1364
|
+
kind="pick",
|
|
1365
|
+
label=prompt["label"],
|
|
1366
|
+
options=[_opt("continue", continue_label)],
|
|
1367
|
+
echo_template=prompt["echo_template"],
|
|
1368
|
+
)
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def _count_prompt(
|
|
1372
|
+
state: WizardState,
|
|
1373
|
+
requirement: RoleRequirement,
|
|
1374
|
+
) -> Prompt:
|
|
1375
|
+
prompt = _p(
|
|
1376
|
+
state.workspace_root,
|
|
1377
|
+
"role_count",
|
|
1378
|
+
role=requirement.role,
|
|
1379
|
+
minimum=str(requirement.min_count),
|
|
1380
|
+
maximum=str(requirement.max_count),
|
|
1381
|
+
default=str(requirement.recommended_count),
|
|
1382
|
+
)
|
|
1383
|
+
return Prompt(
|
|
1384
|
+
step=_role_count_prompt_id(requirement.role),
|
|
1385
|
+
kind="pick",
|
|
1386
|
+
label=prompt["label"],
|
|
1387
|
+
options=[
|
|
1388
|
+
_opt(
|
|
1389
|
+
str(count),
|
|
1390
|
+
prompt["options"]["count"].format(
|
|
1391
|
+
count=count,
|
|
1392
|
+
default_suffix=(
|
|
1393
|
+
prompt["options"].get("default_suffix", "")
|
|
1394
|
+
if count == requirement.recommended_count
|
|
1395
|
+
else ""
|
|
1396
|
+
),
|
|
1397
|
+
),
|
|
1398
|
+
)
|
|
1399
|
+
for count in range(requirement.min_count, requirement.max_count + 1)
|
|
1400
|
+
],
|
|
1401
|
+
echo_template=prompt["echo_template"],
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
def _role_add_prompt(
|
|
1406
|
+
state: WizardState,
|
|
1407
|
+
requirement: RoleRequirement,
|
|
1408
|
+
) -> Prompt:
|
|
1409
|
+
"""min=0 선택 역할: 기본은 추가 안 함, 추가 시 1..max 수량을 이 스텝에서 고른다."""
|
|
1410
|
+
prompt = _p(
|
|
1411
|
+
state.workspace_root,
|
|
1412
|
+
"role_add",
|
|
1413
|
+
role=requirement.role,
|
|
1414
|
+
maximum=str(requirement.max_count),
|
|
1415
|
+
)
|
|
1416
|
+
options = [
|
|
1417
|
+
_opt(
|
|
1418
|
+
"0",
|
|
1419
|
+
prompt["options"]["skip"].format(
|
|
1420
|
+
default_suffix=prompt["options"].get("default_suffix", ""),
|
|
1421
|
+
),
|
|
1422
|
+
),
|
|
1423
|
+
]
|
|
1424
|
+
for count in range(1, requirement.max_count + 1):
|
|
1425
|
+
options.append(
|
|
1426
|
+
_opt(
|
|
1427
|
+
str(count),
|
|
1428
|
+
prompt["options"]["add"].format(count=count),
|
|
1429
|
+
)
|
|
1430
|
+
)
|
|
1431
|
+
return Prompt(
|
|
1432
|
+
step=_role_add_prompt_id(requirement.role),
|
|
1433
|
+
kind="pick",
|
|
1434
|
+
label=prompt["label"],
|
|
1435
|
+
options=options,
|
|
1436
|
+
echo_template=prompt["echo_template"],
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def _role_default_candidates(
|
|
1441
|
+
state: WizardState,
|
|
1442
|
+
role: str,
|
|
1443
|
+
pool: ModelPool,
|
|
1444
|
+
scopes: ModelDefaultScopes | None = None,
|
|
1445
|
+
) -> tuple[str, ...]:
|
|
1446
|
+
try:
|
|
1447
|
+
selected_scopes = scopes or _model_default_scopes(Path(state.project_root))
|
|
1448
|
+
scoped = default_candidates(role, selected_scopes)
|
|
1449
|
+
except (PrepareError, ValueError) as exc:
|
|
1450
|
+
raise WizardError(str(exc)) from exc
|
|
1451
|
+
if scoped:
|
|
1452
|
+
for model_ref in scoped:
|
|
1453
|
+
availability = pool.availability(
|
|
1454
|
+
model_ref,
|
|
1455
|
+
role,
|
|
1456
|
+
state.host_runtime,
|
|
1457
|
+
"new-session",
|
|
1458
|
+
)
|
|
1459
|
+
if not availability.available:
|
|
1460
|
+
raise WizardError(
|
|
1461
|
+
f"configured default model {model_ref!r} is unavailable for "
|
|
1462
|
+
f"role {role!r}: {availability.reason}"
|
|
1463
|
+
)
|
|
1464
|
+
return tuple(scoped)
|
|
1465
|
+
return tuple(
|
|
1466
|
+
str(model.model_ref) for model in pool.default_candidates(role)
|
|
1467
|
+
)
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def _role_model_options(
|
|
1471
|
+
state: WizardState,
|
|
1472
|
+
profile: RoleProfile,
|
|
1473
|
+
role: str,
|
|
1474
|
+
ordinal: int,
|
|
1475
|
+
context: AssignmentContext,
|
|
1476
|
+
scopes: ModelDefaultScopes,
|
|
1477
|
+
) -> list[Option]:
|
|
1478
|
+
pool = context.pool
|
|
1479
|
+
defaults = _role_default_candidates(state, role, pool, scopes)
|
|
1480
|
+
if defaults:
|
|
1481
|
+
offset = (ordinal - 1) % len(defaults)
|
|
1482
|
+
defaults = (*defaults[offset:], *defaults[:offset])
|
|
1483
|
+
candidate_refs = [
|
|
1484
|
+
*defaults,
|
|
1485
|
+
*(str(model.model_ref) for model in pool.list(role=role)),
|
|
1486
|
+
]
|
|
1487
|
+
ordered_refs = tuple(dict.fromkeys(candidate_refs))
|
|
1488
|
+
default_refs = frozenset(defaults)
|
|
1489
|
+
prompt = _p(
|
|
1490
|
+
state.workspace_root,
|
|
1491
|
+
"role_model",
|
|
1492
|
+
role=role,
|
|
1493
|
+
ordinal="1",
|
|
1494
|
+
count="1",
|
|
1495
|
+
)
|
|
1496
|
+
options: list[Option] = []
|
|
1497
|
+
for model_ref in ordered_refs:
|
|
1498
|
+
availability = pool.availability(
|
|
1499
|
+
model_ref,
|
|
1500
|
+
role,
|
|
1501
|
+
state.host_runtime,
|
|
1502
|
+
"new-session",
|
|
1503
|
+
)
|
|
1504
|
+
if not availability.available:
|
|
1505
|
+
continue
|
|
1506
|
+
candidate_models = {
|
|
1507
|
+
selected_role: tuple(models)
|
|
1508
|
+
for selected_role, models in state.role_models.items()
|
|
1509
|
+
}
|
|
1510
|
+
selected = list(candidate_models.get(role, ()))[: ordinal - 1]
|
|
1511
|
+
selected.append(model_ref)
|
|
1512
|
+
candidate_models[role] = tuple(selected)
|
|
1513
|
+
if not _role_selection_can_complete(
|
|
1514
|
+
state,
|
|
1515
|
+
profile,
|
|
1516
|
+
context,
|
|
1517
|
+
candidate_models,
|
|
1518
|
+
scopes,
|
|
1519
|
+
):
|
|
1520
|
+
continue
|
|
1521
|
+
model = pool.resolve(model_ref)
|
|
1522
|
+
options.append(_opt(
|
|
1523
|
+
model_ref,
|
|
1524
|
+
prompt["options"]["model"].format(
|
|
1525
|
+
model_ref=model_ref,
|
|
1526
|
+
display=model.display_name,
|
|
1527
|
+
default_suffix=(
|
|
1528
|
+
prompt["options"].get("default_suffix", "")
|
|
1529
|
+
if model_ref in default_refs
|
|
1530
|
+
else ""
|
|
1531
|
+
),
|
|
1532
|
+
),
|
|
1533
|
+
))
|
|
1534
|
+
if not options:
|
|
1535
|
+
_validate_role_selection_feasibility(state, profile, context, scopes)
|
|
1536
|
+
raise WizardError(f"role {role!r} has no executable model candidates")
|
|
1537
|
+
return options
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def _model_prompt(
|
|
1541
|
+
state: WizardState,
|
|
1542
|
+
profile: RoleProfile,
|
|
1543
|
+
requirement: RoleRequirement,
|
|
1544
|
+
ordinal: int,
|
|
1545
|
+
context: AssignmentContext,
|
|
1546
|
+
scopes: ModelDefaultScopes,
|
|
1547
|
+
) -> Prompt:
|
|
1548
|
+
prompt = _p(
|
|
1549
|
+
state.workspace_root,
|
|
1550
|
+
"role_model",
|
|
1551
|
+
role=requirement.role,
|
|
1552
|
+
ordinal=str(ordinal),
|
|
1553
|
+
count=str(_selected_role_count(state, requirement)),
|
|
1554
|
+
)
|
|
1555
|
+
return Prompt(
|
|
1556
|
+
step=_role_model_prompt_id(requirement.role, ordinal),
|
|
1557
|
+
kind="pick",
|
|
1558
|
+
label=prompt["label"],
|
|
1559
|
+
options=_role_model_options(
|
|
1560
|
+
state,
|
|
1561
|
+
profile,
|
|
1562
|
+
requirement.role,
|
|
1563
|
+
ordinal,
|
|
1564
|
+
context,
|
|
1565
|
+
scopes,
|
|
1566
|
+
),
|
|
1567
|
+
echo_template=prompt["echo_template"],
|
|
1568
|
+
)
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
def _rewind_invalid_role_models(
|
|
1572
|
+
state: WizardState,
|
|
1573
|
+
requirements: tuple[RoleRequirement, ...],
|
|
1574
|
+
requirement_index: int,
|
|
1575
|
+
model_index: int,
|
|
1576
|
+
) -> None:
|
|
1577
|
+
requirement = requirements[requirement_index]
|
|
1578
|
+
del state.role_models[requirement.role][model_index:]
|
|
1579
|
+
invalid_ids: set[str] = set()
|
|
1580
|
+
for later_index, later_requirement in enumerate(
|
|
1581
|
+
requirements[requirement_index:],
|
|
1582
|
+
start=requirement_index,
|
|
1583
|
+
):
|
|
1584
|
+
first_ordinal = model_index + 1 if later_index == requirement_index else 1
|
|
1585
|
+
later_count = _selected_role_count(state, later_requirement)
|
|
1586
|
+
invalid_ids.update(
|
|
1587
|
+
_role_model_prompt_id(later_requirement.role, ordinal)
|
|
1588
|
+
for ordinal in range(first_ordinal, later_count + 1)
|
|
1589
|
+
)
|
|
1590
|
+
if later_index != requirement_index:
|
|
1591
|
+
state.role_models.pop(later_requirement.role, None)
|
|
1592
|
+
state.role_selection_order = [
|
|
1593
|
+
step_id for step_id in state.role_selection_order
|
|
1594
|
+
if step_id not in invalid_ids
|
|
1595
|
+
]
|
|
1596
|
+
state.answered = [
|
|
1597
|
+
step_id for step_id in state.answered if step_id not in invalid_ids
|
|
1598
|
+
]
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def _discard_invalid_previous_models(
|
|
1602
|
+
state: WizardState,
|
|
1603
|
+
profile: RoleProfile,
|
|
1604
|
+
requirements: tuple[RoleRequirement, ...],
|
|
1605
|
+
context: AssignmentContext,
|
|
1606
|
+
scopes: ModelDefaultScopes,
|
|
1607
|
+
) -> None:
|
|
1608
|
+
selected_models = {
|
|
1609
|
+
role: tuple(models) for role, models in state.role_models.items()
|
|
1610
|
+
}
|
|
1611
|
+
if _role_selection_error(
|
|
1612
|
+
state,
|
|
1613
|
+
profile,
|
|
1614
|
+
context,
|
|
1615
|
+
selected_models,
|
|
1616
|
+
scopes,
|
|
1617
|
+
) is None:
|
|
1618
|
+
return
|
|
1619
|
+
selected_prefix: dict[str, tuple[str, ...]] = {}
|
|
1620
|
+
for requirement_index, requirement in enumerate(requirements):
|
|
1621
|
+
count = _selected_role_count(state, requirement)
|
|
1622
|
+
selected = state.role_models.get(requirement.role, [])
|
|
1623
|
+
if len(selected) > count:
|
|
1624
|
+
raise WizardError(
|
|
1625
|
+
f"role {requirement.role!r} has {len(selected)} models for "
|
|
1626
|
+
f"{count} instances"
|
|
1627
|
+
)
|
|
1628
|
+
for index, model_ref in enumerate(selected):
|
|
1629
|
+
candidate_prefix = dict(selected_prefix)
|
|
1630
|
+
candidate_prefix[requirement.role] = tuple(selected[: index + 1])
|
|
1631
|
+
if _role_selection_can_complete(
|
|
1632
|
+
state,
|
|
1633
|
+
profile,
|
|
1634
|
+
context,
|
|
1635
|
+
candidate_prefix,
|
|
1636
|
+
scopes,
|
|
1637
|
+
):
|
|
1638
|
+
continue
|
|
1639
|
+
_rewind_invalid_role_models(
|
|
1640
|
+
state,
|
|
1641
|
+
requirements,
|
|
1642
|
+
requirement_index,
|
|
1643
|
+
index,
|
|
1644
|
+
)
|
|
1645
|
+
return
|
|
1646
|
+
if selected:
|
|
1647
|
+
selected_prefix[requirement.role] = tuple(selected)
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
def _host_session_context(state: WizardState) -> HostSessionContext:
|
|
1651
|
+
native_provider = default_host_registry().resolve(
|
|
1652
|
+
state.host_runtime
|
|
1653
|
+
).descriptor.native_provider_id
|
|
1654
|
+
return HostSessionContext(
|
|
1655
|
+
host_id=state.host_runtime,
|
|
1656
|
+
entry_mode=state.host_entry_mode,
|
|
1657
|
+
available_functions=frozenset(state.available_functions),
|
|
1658
|
+
interaction_surface="wizard",
|
|
1659
|
+
current_model=CurrentSessionModelAttestation.unknown(native_provider),
|
|
1660
|
+
)
|
|
1661
|
+
|
|
1662
|
+
|
|
1663
|
+
def _role_selection_error(
|
|
1664
|
+
state: WizardState,
|
|
1665
|
+
profile: RoleProfile,
|
|
1666
|
+
context: AssignmentContext,
|
|
1667
|
+
role_models: dict[str, tuple[str, ...]],
|
|
1668
|
+
scopes: ModelDefaultScopes,
|
|
1669
|
+
) -> str | None:
|
|
1670
|
+
try:
|
|
1671
|
+
resolve_assignments(
|
|
1672
|
+
profile=profile,
|
|
1673
|
+
role_counts=dict(state.role_counts),
|
|
1674
|
+
role_models=role_models,
|
|
1675
|
+
scopes=scopes,
|
|
1676
|
+
pool=context.pool,
|
|
1677
|
+
host=_host_session_context(state),
|
|
1678
|
+
environment=context.environment,
|
|
1679
|
+
)
|
|
1680
|
+
except AssignmentResolutionError as exc:
|
|
1681
|
+
return _assignment_resolution_message(exc)
|
|
1682
|
+
except (PrepareError, ValueError, RuntimeError) as exc:
|
|
1683
|
+
return str(exc)
|
|
1684
|
+
return None
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
def _executable_role_models(
|
|
1688
|
+
state: WizardState,
|
|
1689
|
+
profile: RoleProfile,
|
|
1690
|
+
requirement: RoleRequirement,
|
|
1691
|
+
context: AssignmentContext,
|
|
1692
|
+
) -> tuple[str, ...]:
|
|
1693
|
+
host = _host_session_context(state)
|
|
1694
|
+
candidates: list[str] = []
|
|
1695
|
+
instance = RoleInstance(requirement.role, requirement.duty, 1)
|
|
1696
|
+
for model in context.pool.list(role=requirement.role):
|
|
1697
|
+
model_ref = str(model.model_ref)
|
|
1698
|
+
try:
|
|
1699
|
+
resolve_model_assignment(
|
|
1700
|
+
instance=instance,
|
|
1701
|
+
model_ref=model_ref,
|
|
1702
|
+
pool=context.pool,
|
|
1703
|
+
host=host,
|
|
1704
|
+
environment=context.environment,
|
|
1705
|
+
)
|
|
1706
|
+
except (AssignmentResolutionError, ValueError, RuntimeError):
|
|
1707
|
+
continue
|
|
1708
|
+
candidates.append(model_ref)
|
|
1709
|
+
return tuple(candidates)
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
def _completed_role_models(
|
|
1713
|
+
state: WizardState,
|
|
1714
|
+
profile: RoleProfile,
|
|
1715
|
+
context: AssignmentContext,
|
|
1716
|
+
role_models: dict[str, tuple[str, ...]],
|
|
1717
|
+
) -> dict[str, tuple[str, ...]] | None:
|
|
1718
|
+
completed = {role: list(models) for role, models in role_models.items()}
|
|
1719
|
+
for requirement in _selectable_static_requirements(state, profile):
|
|
1720
|
+
count = _selected_role_count(state, requirement)
|
|
1721
|
+
selected = completed.setdefault(requirement.role, [])
|
|
1722
|
+
if len(selected) > count:
|
|
1723
|
+
return None
|
|
1724
|
+
candidates = _executable_role_models(state, profile, requirement, context)
|
|
1725
|
+
if not candidates or any(model not in candidates for model in selected):
|
|
1726
|
+
return None
|
|
1727
|
+
# 같은 역할 패널의 model_ref 는 서로 달라야 한다.
|
|
1728
|
+
if len(set(selected)) < len(selected):
|
|
1729
|
+
return None
|
|
1730
|
+
used = set(selected)
|
|
1731
|
+
remaining = [model for model in candidates if model not in used]
|
|
1732
|
+
if len(remaining) < count - len(selected):
|
|
1733
|
+
return None
|
|
1734
|
+
while len(selected) < count:
|
|
1735
|
+
model_ref = next(
|
|
1736
|
+
model for model in candidates if model not in used
|
|
1737
|
+
)
|
|
1738
|
+
selected.append(model_ref)
|
|
1739
|
+
used.add(model_ref)
|
|
1740
|
+
return {role: tuple(models) for role, models in completed.items()}
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
def _role_selection_can_complete(
|
|
1744
|
+
state: WizardState,
|
|
1745
|
+
profile: RoleProfile,
|
|
1746
|
+
context: AssignmentContext,
|
|
1747
|
+
role_models: dict[str, tuple[str, ...]],
|
|
1748
|
+
scopes: ModelDefaultScopes,
|
|
1749
|
+
) -> bool:
|
|
1750
|
+
completed = _completed_role_models(state, profile, context, role_models)
|
|
1751
|
+
return completed is not None and _role_selection_error(
|
|
1752
|
+
state,
|
|
1753
|
+
profile,
|
|
1754
|
+
context,
|
|
1755
|
+
completed,
|
|
1756
|
+
scopes,
|
|
1757
|
+
) is None
|
|
1758
|
+
|
|
1759
|
+
|
|
1760
|
+
def _validate_role_selection_feasibility(
|
|
1761
|
+
state: WizardState,
|
|
1762
|
+
profile: RoleProfile,
|
|
1763
|
+
context: AssignmentContext | None = None,
|
|
1764
|
+
scopes: ModelDefaultScopes | None = None,
|
|
1765
|
+
) -> None:
|
|
1766
|
+
assignment_context = context or load_assignment_context(
|
|
1767
|
+
host_runtime=state.host_runtime,
|
|
1768
|
+
terminal_backend=detect_terminal_backend(),
|
|
1769
|
+
)
|
|
1770
|
+
selected_scopes = scopes or _model_default_scopes(Path(state.project_root))
|
|
1771
|
+
error = _role_selection_error(
|
|
1772
|
+
state,
|
|
1773
|
+
profile,
|
|
1774
|
+
assignment_context,
|
|
1775
|
+
{
|
|
1776
|
+
role: tuple(models) for role, models in state.role_models.items()
|
|
1777
|
+
},
|
|
1778
|
+
selected_scopes,
|
|
1779
|
+
)
|
|
1780
|
+
if error is not None:
|
|
1781
|
+
raise WizardError(error)
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
def next_role_prompt(state: WizardState) -> Prompt | None:
|
|
1785
|
+
"""Ask every adjustable count before one model question per role instance."""
|
|
1786
|
+
if not _role_selection_enabled(state) or not _identity_ready(state):
|
|
1787
|
+
return None
|
|
1788
|
+
state.use_defaults = False
|
|
1789
|
+
# current-session: 리더는 읽기 전용 확인 칸만 먼저 보여 준다.
|
|
1790
|
+
if (
|
|
1791
|
+
state.host_entry_mode == "current-session"
|
|
1792
|
+
and _LEADER_SESSION_STEP not in state.answered
|
|
1793
|
+
and _LEADER_SESSION_STEP not in state.role_selection_order
|
|
1794
|
+
):
|
|
1795
|
+
return _leader_session_prompt(state)
|
|
1796
|
+
profile = _load_role_profile_for_state(state)
|
|
1797
|
+
for requirement in profile.roles:
|
|
1798
|
+
# 필수 수량: min < max 이고 min > 0 일 때만.
|
|
1799
|
+
if (
|
|
1800
|
+
requirement.dynamic
|
|
1801
|
+
or requirement.min_count == requirement.max_count
|
|
1802
|
+
or requirement.min_count == 0
|
|
1803
|
+
):
|
|
1804
|
+
continue
|
|
1805
|
+
if requirement.role not in state.role_counts:
|
|
1806
|
+
return _count_prompt(state, requirement)
|
|
1807
|
+
count = state.role_counts[requirement.role]
|
|
1808
|
+
if count < requirement.min_count or count > requirement.max_count:
|
|
1809
|
+
raise WizardError(
|
|
1810
|
+
f"role {requirement.role!r} count must be in "
|
|
1811
|
+
f"{requirement.min_count}..{requirement.max_count}: {count}"
|
|
1812
|
+
)
|
|
1813
|
+
for requirement in profile.roles:
|
|
1814
|
+
# 선택 역할(min=0, max>0): 기본은 추가 안 함. role-add 로만 연다.
|
|
1815
|
+
if (
|
|
1816
|
+
requirement.dynamic
|
|
1817
|
+
or requirement.min_count != 0
|
|
1818
|
+
or requirement.max_count == 0
|
|
1819
|
+
):
|
|
1820
|
+
continue
|
|
1821
|
+
if requirement.role not in state.role_counts:
|
|
1822
|
+
return _role_add_prompt(state, requirement)
|
|
1823
|
+
count = state.role_counts[requirement.role]
|
|
1824
|
+
if count < 0 or count > requirement.max_count:
|
|
1825
|
+
raise WizardError(
|
|
1826
|
+
f"role {requirement.role!r} count must be in "
|
|
1827
|
+
f"0..{requirement.max_count}: {count}"
|
|
1828
|
+
)
|
|
1829
|
+
requirements = _selectable_static_requirements(state, profile)
|
|
1830
|
+
try:
|
|
1831
|
+
scopes = _model_default_scopes(Path(state.project_root))
|
|
1832
|
+
except (PrepareError, ValueError) as exc:
|
|
1833
|
+
raise WizardError(str(exc)) from exc
|
|
1834
|
+
context = load_assignment_context(
|
|
1835
|
+
host_runtime=state.host_runtime,
|
|
1836
|
+
terminal_backend=detect_terminal_backend(),
|
|
1837
|
+
)
|
|
1838
|
+
_discard_invalid_previous_models(
|
|
1839
|
+
state,
|
|
1840
|
+
profile,
|
|
1841
|
+
requirements,
|
|
1842
|
+
context,
|
|
1843
|
+
scopes,
|
|
1844
|
+
)
|
|
1845
|
+
for requirement in requirements:
|
|
1846
|
+
count = _selected_role_count(state, requirement)
|
|
1847
|
+
selected = state.role_models.get(requirement.role, [])
|
|
1848
|
+
if len(selected) < count:
|
|
1849
|
+
return _model_prompt(
|
|
1850
|
+
state,
|
|
1851
|
+
profile,
|
|
1852
|
+
requirement,
|
|
1853
|
+
len(selected) + 1,
|
|
1854
|
+
context,
|
|
1855
|
+
scopes,
|
|
1856
|
+
)
|
|
1857
|
+
_validate_role_selection_feasibility(state, profile, context, scopes)
|
|
1858
|
+
return None
|
|
1859
|
+
|
|
1860
|
+
|
|
1861
|
+
def _validate_submitted_role_model(
|
|
1862
|
+
state: WizardState,
|
|
1863
|
+
profile: RoleProfile,
|
|
1864
|
+
) -> None:
|
|
1865
|
+
try:
|
|
1866
|
+
scopes = _model_default_scopes(Path(state.project_root))
|
|
1867
|
+
except (PrepareError, ValueError) as exc:
|
|
1868
|
+
raise WizardError(str(exc)) from exc
|
|
1869
|
+
context = load_assignment_context(
|
|
1870
|
+
host_runtime=state.host_runtime,
|
|
1871
|
+
terminal_backend=detect_terminal_backend(),
|
|
1872
|
+
)
|
|
1873
|
+
role_models = {
|
|
1874
|
+
role: tuple(models) for role, models in state.role_models.items()
|
|
1875
|
+
}
|
|
1876
|
+
requirements = _selectable_static_requirements(state, profile)
|
|
1877
|
+
complete = all(
|
|
1878
|
+
len(role_models.get(requirement.role, ()))
|
|
1879
|
+
== _selected_role_count(state, requirement)
|
|
1880
|
+
for requirement in requirements
|
|
1881
|
+
)
|
|
1882
|
+
if complete:
|
|
1883
|
+
_validate_role_selection_feasibility(state, profile, context, scopes)
|
|
1884
|
+
return
|
|
1885
|
+
if not _role_selection_can_complete(
|
|
1886
|
+
state,
|
|
1887
|
+
profile,
|
|
1888
|
+
context,
|
|
1889
|
+
role_models,
|
|
1890
|
+
scopes,
|
|
1891
|
+
):
|
|
1892
|
+
raise WizardError("selected role model leaves no complete role assignment")
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
def _submit_role_prompt(state: WizardState, prompt: Prompt, value: str) -> str:
|
|
1896
|
+
if prompt.step == _LEADER_SESSION_STEP:
|
|
1897
|
+
if value != "continue":
|
|
1898
|
+
raise WizardError("leader session step accepts only 'continue'")
|
|
1899
|
+
# role_models["leader"] 에 쓰지 않는다 — 현재 세션 모델을 그대로 쓴다.
|
|
1900
|
+
return "leader-session: continue"
|
|
1901
|
+
if prompt.step.startswith("role-count:"):
|
|
1902
|
+
role = prompt.step.split(":", 1)[1]
|
|
1903
|
+
profile = _load_role_profile_for_state(state)
|
|
1904
|
+
requirement = next(
|
|
1905
|
+
(row for row in profile.roles if row.role == role),
|
|
1906
|
+
None,
|
|
1907
|
+
)
|
|
1908
|
+
if (
|
|
1909
|
+
requirement is None
|
|
1910
|
+
or requirement.dynamic
|
|
1911
|
+
or requirement.min_count == requirement.max_count
|
|
1912
|
+
or requirement.min_count == 0
|
|
1913
|
+
):
|
|
1914
|
+
raise WizardError(f"role {role!r} does not accept a count selection")
|
|
1915
|
+
try:
|
|
1916
|
+
count = int(value)
|
|
1917
|
+
except ValueError as exc:
|
|
1918
|
+
raise WizardError(f"role {role!r} count must be an integer") from exc
|
|
1919
|
+
if count < requirement.min_count or count > requirement.max_count:
|
|
1920
|
+
raise WizardError(
|
|
1921
|
+
f"role {role!r} count must be in "
|
|
1922
|
+
f"{requirement.min_count}..{requirement.max_count}: {count}"
|
|
1923
|
+
)
|
|
1924
|
+
state.role_counts[role] = count
|
|
1925
|
+
state.role_models.pop(role, None)
|
|
1926
|
+
return f"role-count: {role}={count}"
|
|
1927
|
+
if prompt.step.startswith("role-add:"):
|
|
1928
|
+
role = prompt.step.split(":", 1)[1]
|
|
1929
|
+
profile = _load_role_profile_for_state(state)
|
|
1930
|
+
requirement = next(
|
|
1931
|
+
(row for row in profile.roles if row.role == role),
|
|
1932
|
+
None,
|
|
1933
|
+
)
|
|
1934
|
+
if (
|
|
1935
|
+
requirement is None
|
|
1936
|
+
or requirement.dynamic
|
|
1937
|
+
or requirement.min_count != 0
|
|
1938
|
+
or requirement.max_count == 0
|
|
1939
|
+
):
|
|
1940
|
+
raise WizardError(f"role {role!r} does not accept an optional add")
|
|
1941
|
+
allowed = {option.value for option in prompt.options}
|
|
1942
|
+
if value not in allowed:
|
|
1943
|
+
raise WizardError(
|
|
1944
|
+
f"role {role!r} add selection must be one of "
|
|
1945
|
+
f"{sorted(allowed, key=int)}: {value}"
|
|
1946
|
+
)
|
|
1947
|
+
try:
|
|
1948
|
+
count = int(value)
|
|
1949
|
+
except ValueError as exc:
|
|
1950
|
+
raise WizardError(f"role {role!r} count must be an integer") from exc
|
|
1951
|
+
if count < 0 or count > requirement.max_count:
|
|
1952
|
+
raise WizardError(
|
|
1953
|
+
f"role {role!r} count must be in "
|
|
1954
|
+
f"0..{requirement.max_count}: {count}"
|
|
1955
|
+
)
|
|
1956
|
+
state.role_counts[role] = count
|
|
1957
|
+
state.role_models.pop(role, None)
|
|
1958
|
+
return f"role-add: {role}={count}"
|
|
1959
|
+
_, role, ordinal_raw = prompt.step.split(":", 2)
|
|
1960
|
+
ordinal = int(ordinal_raw)
|
|
1961
|
+
allowed = {option.value for option in prompt.options}
|
|
1962
|
+
if value not in allowed:
|
|
1963
|
+
raise WizardError(
|
|
1964
|
+
f"model {value!r} is not a compatible candidate for role {role!r}"
|
|
1965
|
+
)
|
|
1966
|
+
selected = state.role_models.setdefault(role, [])
|
|
1967
|
+
if ordinal != len(selected) + 1:
|
|
1968
|
+
raise WizardError(
|
|
1969
|
+
f"role {role!r} model selection is out of order at ordinal {ordinal}"
|
|
1970
|
+
)
|
|
1971
|
+
selected.append(value)
|
|
1972
|
+
try:
|
|
1973
|
+
profile = _load_role_profile_for_state(state)
|
|
1974
|
+
_validate_submitted_role_model(state, profile)
|
|
1975
|
+
except WizardError:
|
|
1976
|
+
selected.pop()
|
|
1977
|
+
raise
|
|
1978
|
+
return f"role-model: {role}#{ordinal}={value}"
|
|
1979
|
+
|
|
1980
|
+
|
|
962
1981
|
def _load_profile_workers(workspace_root: Path, task_type: str) -> list[str]:
|
|
963
1982
|
return resolve_profile_workers(_profile_path(workspace_root, task_type))
|
|
964
1983
|
|
|
@@ -1270,6 +2289,30 @@ def _contract_outcome_suffix(entry: dict) -> str:
|
|
|
1270
2289
|
return ""
|
|
1271
2290
|
|
|
1272
2291
|
|
|
2292
|
+
def _next_phase_cell(raw: Any) -> str:
|
|
2293
|
+
"""task picker 한 줄에 실을 다음 phase 표기.
|
|
2294
|
+
|
|
2295
|
+
`phase` 만 싣고 `status` 를 버리면 "아직 여기 있다" 가 "다음으로 갈 수 있다"
|
|
2296
|
+
로 읽힌다. prepare 는 끝나지 않은 run 의 포인터를 `ready` → `pending` 으로
|
|
2297
|
+
내리면서 `phase` 는 남겨두므로(`render._derive_next_recommended_phase`),
|
|
2298
|
+
준비만 된 implementation 태스크의 포인터가
|
|
2299
|
+
`{"phase": "final-verification", "status": "pending"}` 이다. status 를 안
|
|
2300
|
+
보면 그 줄이 `next: final-verification` 으로 찍혀 착수 가능한 것처럼 읽힌다.
|
|
2301
|
+
|
|
2302
|
+
그래서 `ready` 가 아닌 status 는 괄호로 함께 싣는다. `okstra-inspect` 의
|
|
2303
|
+
status 표는 같은 정보를 `*` 마커로 싣지만(`facets/status.md`), 그쪽은 표
|
|
2304
|
+
아래 범례 한 줄을 붙일 수 있는 자리다. picker 옵션은 라벨 한 줄이 전부라
|
|
2305
|
+
범례를 둘 데가 없으므로 status 이름을 그대로 적는다. phase 를 `--` 로
|
|
2306
|
+
지우지 않는 이유는 그 이름이 "어디로 가는 태스크인가" 라는 정보를 여전히
|
|
2307
|
+
나르기 때문이다 — 지우는 것은 착수 가능성만이 아니라 목적지까지 지운다.
|
|
2308
|
+
"""
|
|
2309
|
+
pointer = next_phase.promote(raw)
|
|
2310
|
+
cell = pointer["phase"] or "--"
|
|
2311
|
+
if pointer["status"] == next_phase.STATUS_READY:
|
|
2312
|
+
return cell
|
|
2313
|
+
return f"{cell} ({pointer['status']})"
|
|
2314
|
+
|
|
2315
|
+
|
|
1273
2316
|
def _build_task_pick(state: WizardState) -> Prompt:
|
|
1274
2317
|
t = _p(state.workspace_root, "task_pick")
|
|
1275
2318
|
project_root = Path(state.project_root)
|
|
@@ -1285,7 +2328,7 @@ def _build_task_pick(state: WizardState) -> Prompt:
|
|
|
1285
2328
|
# catalog entries are flat (render_task_catalog_discovery) — there is
|
|
1286
2329
|
# no nested "workflow" object here, unlike task-manifest.json.
|
|
1287
2330
|
phase = entry.get("currentPhase") or ttype
|
|
1288
|
-
nxt = entry.get("nextRecommendedPhase")
|
|
2331
|
+
nxt = _next_phase_cell(entry.get("nextRecommendedPhase"))
|
|
1289
2332
|
suffix = latest_suffix if key == latest_key else ""
|
|
1290
2333
|
label = f"{key} · {phase} · next: {nxt}{_contract_outcome_suffix(entry)}{suffix}"
|
|
1291
2334
|
options.append(_opt(value=key, label=label))
|
|
@@ -1325,8 +2368,7 @@ def _submit_task_pick(state: WizardState, value: str) -> Optional[str]:
|
|
|
1325
2368
|
# task_type seeded from manifest's nextRecommendedPhase as the recommended default
|
|
1326
2369
|
root = find_task_root(Path(state.project_root), value)
|
|
1327
2370
|
manifest = (read_task_manifest(root) or {}) if root else {}
|
|
1328
|
-
|
|
1329
|
-
state.task_type = workflow.get("nextRecommendedPhase") or ""
|
|
2371
|
+
state.task_type = next_phase.autofill_task_type(manifest)
|
|
1330
2372
|
return f"task: {value}"
|
|
1331
2373
|
|
|
1332
2374
|
|
|
@@ -1770,8 +2812,18 @@ def _build_task_type(state: WizardState) -> Prompt:
|
|
|
1770
2812
|
|
|
1771
2813
|
workflow = _existing_task_workflow(state)
|
|
1772
2814
|
revision_requested = _latest_revision_requested_analysis_type(state)
|
|
2815
|
+
# 포인터에서 phase 를 꺼내는 것은 `status` 가 `ready` 일 때뿐이다. prepare 는
|
|
2816
|
+
# 실행이 끝나지 않은 run 의 포인터를 일부러 `pending` 으로 내려두는데
|
|
2817
|
+
# (`render._derive_next_recommended_phase`), 여기서 status 를 안 보고 `phase`
|
|
2818
|
+
# 만 꺼내면 그 방어를 그대로 우회한다 — 기록된 사례가 `implementation` 이
|
|
2819
|
+
# `prepared` 상태인데 `final-verification` 이 추천으로 뜬 것이다. 판정은
|
|
2820
|
+
# 셸 진입점이 쓰는 것과 같은 함수(`next_phase.autofill_task_type`)로 한다.
|
|
2821
|
+
#
|
|
2822
|
+
# `ready` 가 아니면 추천은 비고, 아래 `currentPhase` 재실행 옵션이 남는다.
|
|
2823
|
+
# 실패한 run 의 포인터가 `{"phase": "", "status": "blocked"}` 라는 점에서
|
|
2824
|
+
# 그것이 맞는 제안이다 — 그 옵션은 포인터가 아니라 `currentPhase` 에서 온다.
|
|
1773
2825
|
recommended = (revision_requested or state.task_type
|
|
1774
|
-
or
|
|
2826
|
+
or next_phase.autofill_task_type({"workflow": workflow}))
|
|
1775
2827
|
if not recommended and not workflow:
|
|
1776
2828
|
recommended = TASK_TYPE_VALUES[0]
|
|
1777
2829
|
add(recommended, recommended_suffix)
|
|
@@ -3534,6 +4586,14 @@ def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
|
|
|
3534
4586
|
report_writer_provider = state.report_writer_provider or "claude"
|
|
3535
4587
|
state.report_writer_model = _safe_model(
|
|
3536
4588
|
report_writer_provider, inputs.get("reportWriterModel"))
|
|
4589
|
+
if _role_selection_enabled(state):
|
|
4590
|
+
conversion_payload = dict(inputs)
|
|
4591
|
+
conversion_payload.setdefault("grokModel", state.grok_model)
|
|
4592
|
+
conversion_payload.setdefault("kimiModel", state.kimi_model)
|
|
4593
|
+
state.role_counts.clear()
|
|
4594
|
+
state.role_models.clear()
|
|
4595
|
+
state.role_selection_order.clear()
|
|
4596
|
+
_convert_v1_provider_selections(state, conversion_payload)
|
|
3537
4597
|
directive = inputs.get("directive")
|
|
3538
4598
|
state.directive = directive if isinstance(directive, str) else ""
|
|
3539
4599
|
related = inputs.get("relatedTasks")
|
|
@@ -3579,6 +4639,7 @@ def _role_model_lines(state: WizardState) -> str:
|
|
|
3579
4639
|
.resolve(state.host_runtime)
|
|
3580
4640
|
.descriptor.native_provider_id
|
|
3581
4641
|
or "",
|
|
4642
|
+
report_writer_provider=state.report_writer_provider or "claude",
|
|
3582
4643
|
)
|
|
3583
4644
|
roster = _resolved_roster(state)
|
|
3584
4645
|
impl = state.task_type == "implementation"
|
|
@@ -4116,7 +5177,15 @@ def _build_edit_target(state: WizardState) -> Prompt:
|
|
|
4116
5177
|
t = _p(state.workspace_root, "edit_target")
|
|
4117
5178
|
# offer every step that has been answered.
|
|
4118
5179
|
options: list[Option] = []
|
|
4119
|
-
|
|
5180
|
+
selection_ids = [
|
|
5181
|
+
*state.answered,
|
|
5182
|
+
*(
|
|
5183
|
+
step_id
|
|
5184
|
+
for step_id in state.role_selection_order
|
|
5185
|
+
if step_id not in state.answered
|
|
5186
|
+
),
|
|
5187
|
+
]
|
|
5188
|
+
for sid in selection_ids:
|
|
4120
5189
|
if sid in (S_CONFIRM, S_EDIT_TARGET):
|
|
4121
5190
|
continue
|
|
4122
5191
|
options.append(_opt(sid, sid))
|
|
@@ -4128,11 +5197,59 @@ def _build_edit_target(state: WizardState) -> Prompt:
|
|
|
4128
5197
|
)
|
|
4129
5198
|
|
|
4130
5199
|
|
|
5200
|
+
def _reset_role_selection_from(state: WizardState, target_step: str) -> None:
|
|
5201
|
+
try:
|
|
5202
|
+
target_index = state.role_selection_order.index(target_step)
|
|
5203
|
+
except ValueError as exc:
|
|
5204
|
+
raise WizardError(f"unknown role selection step: {target_step!r}") from exc
|
|
5205
|
+
kept_ids = state.role_selection_order[:target_index]
|
|
5206
|
+
kept_counts = {
|
|
5207
|
+
step_id.split(":", 1)[1]
|
|
5208
|
+
for step_id in kept_ids
|
|
5209
|
+
if step_id.startswith(("role-count:", "role-add:"))
|
|
5210
|
+
}
|
|
5211
|
+
kept_models: dict[str, int] = {}
|
|
5212
|
+
for step_id in kept_ids:
|
|
5213
|
+
if not step_id.startswith("role-model:"):
|
|
5214
|
+
continue
|
|
5215
|
+
_, role, _ = step_id.split(":", 2)
|
|
5216
|
+
kept_models[role] = kept_models.get(role, 0) + 1
|
|
5217
|
+
state.role_counts = {
|
|
5218
|
+
role: count
|
|
5219
|
+
for role, count in state.role_counts.items()
|
|
5220
|
+
if role in kept_counts
|
|
5221
|
+
}
|
|
5222
|
+
state.role_models = {
|
|
5223
|
+
role: models[:kept_models[role]]
|
|
5224
|
+
for role, models in state.role_models.items()
|
|
5225
|
+
if kept_models.get(role, 0) > 0
|
|
5226
|
+
}
|
|
5227
|
+
removed_ids = set(state.role_selection_order[target_index:])
|
|
5228
|
+
state.role_selection_order = kept_ids
|
|
5229
|
+
state.answered = [
|
|
5230
|
+
step_id for step_id in state.answered if step_id not in removed_ids
|
|
5231
|
+
]
|
|
5232
|
+
|
|
5233
|
+
|
|
5234
|
+
def _clear_role_selection(state: WizardState) -> None:
|
|
5235
|
+
state.role_counts.clear()
|
|
5236
|
+
state.role_models.clear()
|
|
5237
|
+
state.role_selection_order.clear()
|
|
5238
|
+
state.answered = [
|
|
5239
|
+
step_id
|
|
5240
|
+
for step_id in state.answered
|
|
5241
|
+
if not _is_role_selection_step(step_id)
|
|
5242
|
+
]
|
|
5243
|
+
|
|
5244
|
+
|
|
4131
5245
|
def _submit_edit_target(state: WizardState, value: str) -> Optional[str]:
|
|
4132
|
-
if
|
|
5246
|
+
if _is_role_selection_step(value):
|
|
5247
|
+
_reset_role_selection_from(state, value)
|
|
5248
|
+
elif any(s.id == value for s in STEPS):
|
|
5249
|
+
_reset_from(state, value)
|
|
5250
|
+
else:
|
|
4133
5251
|
raise WizardError(f"unknown step: {value!r}")
|
|
4134
5252
|
state.edit_target = value
|
|
4135
|
-
_reset_from(state, value)
|
|
4136
5253
|
state.confirmed = None
|
|
4137
5254
|
state.edit_target = ""
|
|
4138
5255
|
return f"edit-target: {value} (rewinding)"
|
|
@@ -4379,23 +5496,28 @@ STEPS: list[Step] = [
|
|
|
4379
5496
|
owns=("handoff_mode", "handoff_stages", "approved_plan_path")),
|
|
4380
5497
|
Step(S_EXECUTOR,
|
|
4381
5498
|
applies=lambda s: (s.task_type == "implementation"
|
|
5499
|
+
and not _role_selection_enabled(s)
|
|
4382
5500
|
and bool(s.approved_plan_path)
|
|
4383
5501
|
and not s.executor),
|
|
4384
5502
|
build=_build_executor, submit=_submit_executor,
|
|
4385
5503
|
owns=("executor",)),
|
|
4386
5504
|
Step(S_CRITIC_PICK,
|
|
4387
|
-
applies=lambda s: (
|
|
5505
|
+
applies=lambda s: (not _role_selection_enabled(s)
|
|
5506
|
+
and s.task_type in ("requirements-discovery", "error-analysis", "implementation-planning", "final-verification")
|
|
4388
5507
|
and not s.critic
|
|
4389
5508
|
and not s.critic_pending_text
|
|
4390
5509
|
and S_CRITIC_PICK not in s.answered),
|
|
4391
5510
|
build=_build_critic_pick, submit=_submit_critic_pick,
|
|
4392
5511
|
owns=("critic", "critic_pending_text")),
|
|
4393
5512
|
Step(S_CRITIC_TEXT,
|
|
4394
|
-
applies=lambda s: (
|
|
5513
|
+
applies=lambda s: (not _role_selection_enabled(s)
|
|
5514
|
+
and s.critic_pending_text
|
|
5515
|
+
and S_CRITIC_TEXT not in s.answered),
|
|
4395
5516
|
build=_build_critic_text, submit=_submit_critic_text,
|
|
4396
5517
|
owns=("critic", "critic_pending_text")),
|
|
4397
5518
|
Step(S_DEFAULTS_OR_CUSTOM,
|
|
4398
5519
|
applies=lambda s: (_identity_ready(s)
|
|
5520
|
+
and not _role_selection_enabled(s)
|
|
4399
5521
|
and s.use_defaults is None),
|
|
4400
5522
|
build=_build_defaults_or_custom, submit=_submit_defaults_or_custom,
|
|
4401
5523
|
owns=("use_defaults",)),
|
|
@@ -4408,6 +5530,7 @@ STEPS: list[Step] = [
|
|
|
4408
5530
|
# still skipped because it runs lead + executor only.
|
|
4409
5531
|
Step(S_WORKERS_OVERRIDE,
|
|
4410
5532
|
applies=lambda s: (s.task_type != "implementation"
|
|
5533
|
+
and not _role_selection_enabled(s)
|
|
4411
5534
|
and any(
|
|
4412
5535
|
w != "report-writer"
|
|
4413
5536
|
for w in (s.profile_workers
|
|
@@ -4419,23 +5542,27 @@ STEPS: list[Step] = [
|
|
|
4419
5542
|
# "직접 선택" 을 고른 run 에서만 등장한다 — 기본 로스터에서 워커를 빼는
|
|
4420
5543
|
# 축소가 가능한 유일한 화면.
|
|
4421
5544
|
Step(S_WORKERS_CUSTOM,
|
|
4422
|
-
applies=lambda s: (s
|
|
5545
|
+
applies=lambda s: (not _role_selection_enabled(s)
|
|
5546
|
+
and s.workers_custom_pending
|
|
4423
5547
|
and S_WORKERS_CUSTOM not in s.answered),
|
|
4424
5548
|
build=_build_workers_custom, submit=_submit_workers_custom,
|
|
4425
5549
|
owns=("workers_override", "workers_custom_pending")),
|
|
4426
5550
|
Step(S_LEAD_MODEL,
|
|
4427
5551
|
applies=lambda s: (s.use_defaults is False
|
|
5552
|
+
and not _role_selection_enabled(s)
|
|
4428
5553
|
and S_LEAD_MODEL not in s.answered),
|
|
4429
5554
|
build=_build_lead_model, submit=_submit_lead_model,
|
|
4430
5555
|
owns=("lead_provider", "lead_model")),
|
|
4431
5556
|
Step(S_EXECUTOR_MODEL,
|
|
4432
5557
|
applies=lambda s: (s.use_defaults is False
|
|
5558
|
+
and not _role_selection_enabled(s)
|
|
4433
5559
|
and s.task_type == "implementation"
|
|
4434
5560
|
and S_EXECUTOR_MODEL not in s.answered),
|
|
4435
5561
|
build=_build_executor_model, submit=_submit_executor_model,
|
|
4436
5562
|
owns=("claude_model", "codex_model", "antigravity_model")),
|
|
4437
5563
|
Step(S_CLAUDE_MODEL,
|
|
4438
5564
|
applies=lambda s: (s.use_defaults is False
|
|
5565
|
+
and not _role_selection_enabled(s)
|
|
4439
5566
|
and s.task_type != "implementation"
|
|
4440
5567
|
and "claude" in _resolved_roster(s)
|
|
4441
5568
|
and S_CLAUDE_MODEL not in s.answered),
|
|
@@ -4443,6 +5570,7 @@ STEPS: list[Step] = [
|
|
|
4443
5570
|
owns=("claude_model",)),
|
|
4444
5571
|
Step(S_CODEX_MODEL,
|
|
4445
5572
|
applies=lambda s: (s.use_defaults is False
|
|
5573
|
+
and not _role_selection_enabled(s)
|
|
4446
5574
|
and s.task_type != "implementation"
|
|
4447
5575
|
and "codex" in _resolved_roster(s)
|
|
4448
5576
|
and S_CODEX_MODEL not in s.answered),
|
|
@@ -4450,6 +5578,7 @@ STEPS: list[Step] = [
|
|
|
4450
5578
|
owns=("codex_model",)),
|
|
4451
5579
|
Step(S_ANTIGRAVITY_MODEL,
|
|
4452
5580
|
applies=lambda s: (s.use_defaults is False
|
|
5581
|
+
and not _role_selection_enabled(s)
|
|
4453
5582
|
and s.task_type != "implementation"
|
|
4454
5583
|
and "antigravity" in _resolved_roster(s)
|
|
4455
5584
|
and S_ANTIGRAVITY_MODEL not in s.answered),
|
|
@@ -4457,6 +5586,7 @@ STEPS: list[Step] = [
|
|
|
4457
5586
|
owns=("antigravity_model",)),
|
|
4458
5587
|
Step(S_GROK_MODEL,
|
|
4459
5588
|
applies=lambda s: (s.use_defaults is False
|
|
5589
|
+
and not _role_selection_enabled(s)
|
|
4460
5590
|
and s.task_type != "implementation"
|
|
4461
5591
|
and "grok" in _resolved_roster(s)
|
|
4462
5592
|
and S_GROK_MODEL not in s.answered),
|
|
@@ -4464,6 +5594,7 @@ STEPS: list[Step] = [
|
|
|
4464
5594
|
owns=("grok_model",)),
|
|
4465
5595
|
Step(S_KIMI_MODEL,
|
|
4466
5596
|
applies=lambda s: (s.use_defaults is False
|
|
5597
|
+
and not _role_selection_enabled(s)
|
|
4467
5598
|
and s.task_type != "implementation"
|
|
4468
5599
|
and "kimi" in _resolved_roster(s)
|
|
4469
5600
|
and S_KIMI_MODEL not in s.answered),
|
|
@@ -4471,6 +5602,7 @@ STEPS: list[Step] = [
|
|
|
4471
5602
|
owns=("kimi_model",)),
|
|
4472
5603
|
Step(S_REPORT_WRITER_MODEL,
|
|
4473
5604
|
applies=lambda s: (s.use_defaults is False
|
|
5605
|
+
and not _role_selection_enabled(s)
|
|
4474
5606
|
and (s.task_type == "implementation"
|
|
4475
5607
|
or "report-writer" in _resolved_roster(s))
|
|
4476
5608
|
and S_REPORT_WRITER_MODEL not in s.answered),
|
|
@@ -4590,7 +5722,7 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
4590
5722
|
if s.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
4591
5723
|
if not s.approved_plan_path:
|
|
4592
5724
|
return False
|
|
4593
|
-
if s.task_type == "implementation":
|
|
5725
|
+
if s.task_type == "implementation" and not _role_selection_enabled(s):
|
|
4594
5726
|
if not s.executor:
|
|
4595
5727
|
return False
|
|
4596
5728
|
if (s.task_type == "release-handoff"
|
|
@@ -4600,6 +5732,24 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
4600
5732
|
|
|
4601
5733
|
|
|
4602
5734
|
def _ready_for_confirm(s: WizardState) -> bool:
|
|
5735
|
+
if _role_selection_enabled(s):
|
|
5736
|
+
try:
|
|
5737
|
+
if next_role_prompt(s) is not None:
|
|
5738
|
+
return False
|
|
5739
|
+
except WizardError:
|
|
5740
|
+
return False
|
|
5741
|
+
followup_ids = [
|
|
5742
|
+
S_DIRECTIVE_PICK,
|
|
5743
|
+
S_DIRECTIVE,
|
|
5744
|
+
S_RELATED_TASKS_PICK,
|
|
5745
|
+
S_RELATED_TASKS,
|
|
5746
|
+
S_CLARIFICATION_PICK,
|
|
5747
|
+
S_CLARIFICATION,
|
|
5748
|
+
S_PR_TEMPLATE_PICK,
|
|
5749
|
+
S_PR_TEMPLATE,
|
|
5750
|
+
S_PR_TEMPLATE_SCOPE,
|
|
5751
|
+
]
|
|
5752
|
+
return not any(STEP_BY_ID[step_id].applies(s) for step_id in followup_ids)
|
|
4603
5753
|
if s.use_defaults is None:
|
|
4604
5754
|
return False
|
|
4605
5755
|
# Worker roster is required regardless of the defaults / customize
|
|
@@ -4631,6 +5781,12 @@ def _reset_from(state: WizardState, target_step: str) -> None:
|
|
|
4631
5781
|
idx = next((i for i, s in enumerate(STEPS) if s.id == target_step), -1)
|
|
4632
5782
|
if idx < 0:
|
|
4633
5783
|
return
|
|
5784
|
+
role_boundary = next(
|
|
5785
|
+
i for i, step in enumerate(STEPS)
|
|
5786
|
+
if step.id == S_DEFAULTS_OR_CUSTOM
|
|
5787
|
+
)
|
|
5788
|
+
if idx < role_boundary:
|
|
5789
|
+
_clear_role_selection(state)
|
|
4634
5790
|
# A later step may own a field an earlier answered step also owns —
|
|
4635
5791
|
# `handoff_stage_pick` owns `approved_plan_path` because it resolves the
|
|
4636
5792
|
# plan on its own. Clearing it while rewinding to a step in front of it
|
|
@@ -4687,6 +5843,8 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
4687
5843
|
"selected_stages": "",
|
|
4688
5844
|
"handoff_mode": "", "handoff_stages": "",
|
|
4689
5845
|
"executor": "", "critic": "", "critic_pending_text": False,
|
|
5846
|
+
"execution_identity_version": 1,
|
|
5847
|
+
"role_counts": {}, "role_models": {}, "role_selection_order": [],
|
|
4690
5848
|
"reuse_previous": None,
|
|
4691
5849
|
"use_defaults": None, "workers_override": "",
|
|
4692
5850
|
"workers_custom_pending": False,
|
|
@@ -4711,6 +5869,8 @@ def _reset_field(state: WizardState, fname: str) -> None:
|
|
|
4711
5869
|
# copy mutable defaults
|
|
4712
5870
|
if isinstance(default, list):
|
|
4713
5871
|
setattr(state, fname, [])
|
|
5872
|
+
elif isinstance(default, dict):
|
|
5873
|
+
setattr(state, fname, {})
|
|
4714
5874
|
else:
|
|
4715
5875
|
setattr(state, fname, default)
|
|
4716
5876
|
|
|
@@ -4725,6 +5885,7 @@ def init_state(
|
|
|
4725
5885
|
) -> WizardState:
|
|
4726
5886
|
"""Bootstrap a new wizard state."""
|
|
4727
5887
|
return WizardState(
|
|
5888
|
+
execution_identity_version=2,
|
|
4728
5889
|
workspace_root=workspace_root,
|
|
4729
5890
|
project_root=project_root,
|
|
4730
5891
|
project_id=project_id,
|
|
@@ -4764,6 +5925,10 @@ def next_prompt(state: WizardState) -> Prompt:
|
|
|
4764
5925
|
if state.confirmed:
|
|
4765
5926
|
return Prompt(step=S_DONE, kind="done")
|
|
4766
5927
|
for step in STEPS:
|
|
5928
|
+
if step.id == S_DEFAULTS_OR_CUSTOM:
|
|
5929
|
+
role_prompt = next_role_prompt(state)
|
|
5930
|
+
if role_prompt is not None:
|
|
5931
|
+
return role_prompt
|
|
4767
5932
|
if step.id in state.answered and not step.repeatable:
|
|
4768
5933
|
continue
|
|
4769
5934
|
if step.applies(state):
|
|
@@ -4800,6 +5965,13 @@ def _sim_advance(state: WizardState, prompt: Prompt) -> None:
|
|
|
4800
5965
|
"""기본답으로 한 화면 전진한다. progress 를 재계산하는 submit()/
|
|
4801
5966
|
_submit_group() 은 호출하지 않고 step.submit 만 직접 호출해 재귀를 막는다."""
|
|
4802
5967
|
try:
|
|
5968
|
+
if _is_role_selection_step(prompt.step):
|
|
5969
|
+
_submit_role_prompt(state, prompt, _sim_answer(prompt))
|
|
5970
|
+
if prompt.step not in state.answered:
|
|
5971
|
+
state.answered.append(prompt.step)
|
|
5972
|
+
if prompt.step not in state.role_selection_order:
|
|
5973
|
+
state.role_selection_order.append(prompt.step)
|
|
5974
|
+
return
|
|
4803
5975
|
if prompt.kind == "pick_group":
|
|
4804
5976
|
for q in prompt.questions:
|
|
4805
5977
|
STEP_BY_ID[q.step].submit(state, _sim_answer(q))
|
|
@@ -4980,6 +6152,14 @@ def submit(state: WizardState, value: str) -> dict[str, Any]:
|
|
|
4980
6152
|
value = _normalize_interaction_answer(state, prompt, plan, value)
|
|
4981
6153
|
if prompt.kind == "pick_group":
|
|
4982
6154
|
return _submit_group(state, prompt, value)
|
|
6155
|
+
if _is_role_selection_step(prompt.step):
|
|
6156
|
+
echo = _submit_role_prompt(state, prompt, value or "")
|
|
6157
|
+
if prompt.step not in state.answered:
|
|
6158
|
+
state.answered.append(prompt.step)
|
|
6159
|
+
if prompt.step not in state.role_selection_order:
|
|
6160
|
+
state.role_selection_order.append(prompt.step)
|
|
6161
|
+
nxt = next_prompt(state)
|
|
6162
|
+
return {"echo": echo, "next": prompt_payload(state, nxt)}
|
|
4983
6163
|
step = STEP_BY_ID[prompt.step]
|
|
4984
6164
|
echo = step.submit(state, value or "")
|
|
4985
6165
|
if prompt.step not in state.answered:
|
|
@@ -5005,15 +6185,45 @@ def _stage_intent(state: WizardState) -> WizardStageIntent:
|
|
|
5005
6185
|
raise WizardError(str(exc)) from exc
|
|
5006
6186
|
|
|
5007
6187
|
|
|
5008
|
-
def
|
|
6188
|
+
def render_role_args(state: WizardState) -> list[str]:
|
|
6189
|
+
"""Return ordered canonical role count/model CLI tokens."""
|
|
6190
|
+
role_argv: list[str] = []
|
|
6191
|
+
if _role_selection_enabled(state):
|
|
6192
|
+
profile = _load_role_profile_for_state(state)
|
|
6193
|
+
requirements = _selectable_static_requirements(state, profile)
|
|
6194
|
+
for requirement in profile.roles:
|
|
6195
|
+
if (
|
|
6196
|
+
requirement.dynamic
|
|
6197
|
+
or requirement.min_count == requirement.max_count
|
|
6198
|
+
):
|
|
6199
|
+
continue
|
|
6200
|
+
if requirement.role not in state.role_counts:
|
|
6201
|
+
continue
|
|
6202
|
+
count = state.role_counts[requirement.role]
|
|
6203
|
+
if count <= 0:
|
|
6204
|
+
continue
|
|
6205
|
+
role_argv.extend([
|
|
6206
|
+
"--role-count",
|
|
6207
|
+
f"{requirement.role}={count}",
|
|
6208
|
+
])
|
|
6209
|
+
ordered_roles = [requirement.role for requirement in requirements]
|
|
6210
|
+
else:
|
|
6211
|
+
ordered_roles = []
|
|
6212
|
+
for role in state.role_models:
|
|
6213
|
+
if role not in ordered_roles:
|
|
6214
|
+
ordered_roles.append(role)
|
|
6215
|
+
for role in ordered_roles:
|
|
6216
|
+
for model_ref in state.role_models.get(role, []):
|
|
6217
|
+
role_argv.extend(["--role-model", f"{role}={model_ref}"])
|
|
6218
|
+
return role_argv
|
|
6219
|
+
|
|
6220
|
+
|
|
6221
|
+
def render_args(state: WizardState) -> dict[str, Any]:
|
|
5009
6222
|
"""Convert finalized state into ``okstra render-bundle`` argument map."""
|
|
5010
6223
|
if state.aborted:
|
|
5011
6224
|
raise WizardError(
|
|
5012
6225
|
"wizard was aborted by the user — render-args is unavailable"
|
|
5013
6226
|
)
|
|
5014
|
-
workers = state.workers_override.strip()
|
|
5015
|
-
if state.task_type == "implementation":
|
|
5016
|
-
workers = "" # profile-default roster is mandatory for impl
|
|
5017
6227
|
base_ref = (
|
|
5018
6228
|
""
|
|
5019
6229
|
if state.reuse_worktree or state.task_type == "final-verification"
|
|
@@ -5030,15 +6240,7 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
5030
6240
|
for path in (state.feature_evidence_path, state.project_evidence_path)
|
|
5031
6241
|
if path
|
|
5032
6242
|
]
|
|
5033
|
-
|
|
5034
|
-
f"{provider}={model}"
|
|
5035
|
-
for provider, model in (
|
|
5036
|
-
("grok", state.grok_model),
|
|
5037
|
-
("kimi", state.kimi_model),
|
|
5038
|
-
)
|
|
5039
|
-
if model
|
|
5040
|
-
)
|
|
5041
|
-
return {
|
|
6243
|
+
rendered: dict[str, Any] = {
|
|
5042
6244
|
"project-root": state.project_root,
|
|
5043
6245
|
"project-id": state.project_id,
|
|
5044
6246
|
"task-group": state.task_group,
|
|
@@ -5047,22 +6249,11 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
5047
6249
|
"task-brief": state.brief_path,
|
|
5048
6250
|
"analysis-target": state.analysis_target,
|
|
5049
6251
|
"evidence-inputs": ",".join(evidence_paths),
|
|
5050
|
-
"executor": state.executor,
|
|
5051
|
-
"critic": state.critic,
|
|
5052
6252
|
"approved-plan": state.approved_plan_path,
|
|
5053
6253
|
"stage": stage_intent.stage,
|
|
5054
6254
|
"stages": state.handoff_stages,
|
|
5055
6255
|
"base-ref": base_ref,
|
|
5056
|
-
"workers": workers,
|
|
5057
6256
|
"directive": state.directive,
|
|
5058
|
-
"lead-provider": state.lead_provider,
|
|
5059
|
-
"lead-model": state.lead_model,
|
|
5060
|
-
"claude-model": state.claude_model,
|
|
5061
|
-
"codex-model": state.codex_model,
|
|
5062
|
-
"antigravity-model": state.antigravity_model,
|
|
5063
|
-
"worker-model": worker_models,
|
|
5064
|
-
"report-writer-provider": state.report_writer_provider,
|
|
5065
|
-
"report-writer-model": state.report_writer_model,
|
|
5066
6257
|
"related-tasks": state.related_tasks_raw,
|
|
5067
6258
|
"clarification-response": state.clarification_response_path,
|
|
5068
6259
|
"selected-direction": state.selected_direction_path,
|
|
@@ -5072,7 +6263,36 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
5072
6263
|
),
|
|
5073
6264
|
"pr-template-path": pr_template,
|
|
5074
6265
|
"fix-cycle": state.fix_cycle,
|
|
6266
|
+
"host-session-context-json": serialize_host_session_context(
|
|
6267
|
+
_host_session_context(state)
|
|
6268
|
+
),
|
|
5075
6269
|
}
|
|
6270
|
+
role_argv = render_role_args(state)
|
|
6271
|
+
rendered["role-count"] = [
|
|
6272
|
+
role_argv[index + 1]
|
|
6273
|
+
for index, token in enumerate(role_argv)
|
|
6274
|
+
if token == "--role-count"
|
|
6275
|
+
]
|
|
6276
|
+
rendered["role-model"] = [
|
|
6277
|
+
role_argv[index + 1]
|
|
6278
|
+
for index, token in enumerate(role_argv)
|
|
6279
|
+
if token == "--role-model"
|
|
6280
|
+
]
|
|
6281
|
+
return rendered
|
|
6282
|
+
|
|
6283
|
+
|
|
6284
|
+
def _render_argv(rendered: dict[str, Any], *, host_runtime: str) -> list[str]:
|
|
6285
|
+
"""Flatten render arguments into the canonical render-bundle argv."""
|
|
6286
|
+
argv = ["--lead-runtime", host_runtime]
|
|
6287
|
+
for name, raw_value in rendered.items():
|
|
6288
|
+
values = raw_value if isinstance(raw_value, list) else [raw_value]
|
|
6289
|
+
for value in values:
|
|
6290
|
+
if not isinstance(value, str):
|
|
6291
|
+
raise WizardError(
|
|
6292
|
+
f"wizard render arg --{name} must be a string"
|
|
6293
|
+
)
|
|
6294
|
+
argv.extend([f"--{name}", value])
|
|
6295
|
+
return argv
|
|
5076
6296
|
|
|
5077
6297
|
|
|
5078
6298
|
def _clarification_sidecar_line(state: WizardState) -> Optional[str]:
|
|
@@ -5172,37 +6392,77 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
5172
6392
|
worktree_line = _worktree_preview_line(state)
|
|
5173
6393
|
if worktree_line is not None:
|
|
5174
6394
|
lines.append(worktree_line)
|
|
5175
|
-
|
|
6395
|
+
role_selection = _role_selection_enabled(state)
|
|
6396
|
+
if state.task_type == "implementation" and not role_selection:
|
|
5176
6397
|
lines.append(f" executor : {state.executor or '(default)'}")
|
|
5177
6398
|
lines.append(
|
|
5178
6399
|
_msg(state.workspace_root, "confirmation", "workers_implementation_default")
|
|
5179
6400
|
)
|
|
5180
|
-
|
|
6401
|
+
elif not role_selection:
|
|
5181
6402
|
roster = state.workers_override or ",".join(state.profile_workers) or "(profile default)"
|
|
5182
6403
|
lines.append(f" workers : {roster}")
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
6404
|
+
if role_selection:
|
|
6405
|
+
profile = _load_role_profile_for_state(state)
|
|
6406
|
+
if state.host_entry_mode == "current-session":
|
|
6407
|
+
lines.append(_msg(
|
|
6408
|
+
state.workspace_root,
|
|
6409
|
+
"confirmation",
|
|
6410
|
+
"static_role",
|
|
6411
|
+
role="leader",
|
|
6412
|
+
ordinal="1",
|
|
6413
|
+
model="current-session",
|
|
6414
|
+
))
|
|
6415
|
+
for requirement in _selectable_static_requirements(state, profile):
|
|
6416
|
+
count = _selected_role_count(state, requirement)
|
|
6417
|
+
selected = state.role_models.get(requirement.role, [])
|
|
6418
|
+
for ordinal in range(1, count + 1):
|
|
6419
|
+
model_ref = (
|
|
6420
|
+
selected[ordinal - 1]
|
|
6421
|
+
if ordinal <= len(selected)
|
|
6422
|
+
else "(selection required)"
|
|
6423
|
+
)
|
|
6424
|
+
lines.append(_msg(
|
|
6425
|
+
state.workspace_root,
|
|
6426
|
+
"confirmation",
|
|
6427
|
+
"static_role",
|
|
6428
|
+
role=requirement.role,
|
|
6429
|
+
ordinal=str(ordinal),
|
|
6430
|
+
model=model_ref,
|
|
6431
|
+
))
|
|
6432
|
+
for requirement in profile.roles:
|
|
6433
|
+
if not requirement.dynamic:
|
|
6434
|
+
continue
|
|
6435
|
+
lines.append(_msg(
|
|
6436
|
+
state.workspace_root,
|
|
6437
|
+
"confirmation",
|
|
6438
|
+
"dynamic_role",
|
|
6439
|
+
role=requirement.role,
|
|
6440
|
+
))
|
|
6441
|
+
else:
|
|
5198
6442
|
lines.append(
|
|
5199
|
-
f"
|
|
5200
|
-
f"{state.
|
|
6443
|
+
f" lead : {state.lead_provider or 'claude'} / "
|
|
6444
|
+
f"{state.lead_model or 'default'}"
|
|
5201
6445
|
)
|
|
6446
|
+
if state.claude_model:
|
|
6447
|
+
lines.append(f" claude-model : {state.claude_model}")
|
|
6448
|
+
if state.codex_model:
|
|
6449
|
+
lines.append(f" codex-model : {state.codex_model}")
|
|
6450
|
+
if state.antigravity_model:
|
|
6451
|
+
lines.append(f" antigravity-model : {state.antigravity_model}")
|
|
6452
|
+
if state.grok_model:
|
|
6453
|
+
lines.append(f" grok-model : {state.grok_model}")
|
|
6454
|
+
if state.kimi_model:
|
|
6455
|
+
lines.append(f" kimi-model : {state.kimi_model}")
|
|
6456
|
+
if state.report_writer_model or state.report_writer_provider:
|
|
6457
|
+
lines.append(
|
|
6458
|
+
f" report-writer : {state.report_writer_provider or 'claude'} / "
|
|
6459
|
+
f"{state.report_writer_model or 'default'}"
|
|
6460
|
+
)
|
|
5202
6461
|
lines.append(f" directive : {state.directive or '(none)'}")
|
|
5203
6462
|
if state.related_tasks_raw:
|
|
5204
6463
|
lines.append(f" related-tasks : {state.related_tasks_raw}")
|
|
5205
|
-
if
|
|
6464
|
+
if (not role_selection
|
|
6465
|
+
and state.task_type in ("requirements-discovery", "error-analysis", "implementation-planning", "final-verification")):
|
|
5206
6466
|
lines.append(f" critic : {state.critic or '(off)'}")
|
|
5207
6467
|
if state.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
5208
6468
|
lines.append(f" approved-plan : {state.approved_plan_path}")
|
|
@@ -5269,8 +6529,10 @@ def wizard_outcome(state: WizardState) -> dict[str, Any]:
|
|
|
5269
6529
|
raise WizardError("wizard was aborted by the user — outcome is unavailable")
|
|
5270
6530
|
if state.confirmed is not True:
|
|
5271
6531
|
raise WizardError("wizard is not complete — outcome is unavailable")
|
|
6532
|
+
rendered = render_args(state)
|
|
5272
6533
|
return {
|
|
5273
|
-
"renderArgs":
|
|
6534
|
+
"renderArgs": rendered,
|
|
6535
|
+
"renderArgv": _render_argv(rendered, host_runtime=state.host_runtime),
|
|
5274
6536
|
"orchestration": {"chainStages": _stage_intent(state).chain_stages},
|
|
5275
6537
|
"persistActions": _wizard_persist_actions(state),
|
|
5276
6538
|
"confirmationText": confirmation_block(state),
|
|
@@ -5279,16 +6541,136 @@ def wizard_outcome(state: WizardState) -> dict[str, Any]:
|
|
|
5279
6541
|
|
|
5280
6542
|
# ---- File I/O helpers (used by CLI) -------------------------------------
|
|
5281
6543
|
|
|
6544
|
+
_LEGACY_SOURCE_FIELD = "legacySource"
|
|
6545
|
+
|
|
6546
|
+
|
|
6547
|
+
def _legacy_v2_companion_path(path: Path) -> Path:
|
|
6548
|
+
return path.with_name(f"{path.name}.v2.json")
|
|
6549
|
+
|
|
6550
|
+
|
|
6551
|
+
def _content_addressed_v2_companion_path(
|
|
6552
|
+
path: Path,
|
|
6553
|
+
source_digest: str,
|
|
6554
|
+
suffix: int | None = None,
|
|
6555
|
+
) -> Path:
|
|
6556
|
+
numbered = f".{suffix}" if suffix is not None else ""
|
|
6557
|
+
return path.with_name(f"{path.name}.v2.{source_digest}{numbered}.json")
|
|
6558
|
+
|
|
6559
|
+
|
|
6560
|
+
def _content_addressed_v2_companion_paths(
|
|
6561
|
+
path: Path,
|
|
6562
|
+
source_digest: str,
|
|
6563
|
+
) -> tuple[Path, ...]:
|
|
6564
|
+
canonical = _content_addressed_v2_companion_path(path, source_digest)
|
|
6565
|
+
numbered_prefix = f"{path.name}.v2.{source_digest}."
|
|
6566
|
+
numbered = tuple(sorted(
|
|
6567
|
+
candidate
|
|
6568
|
+
for candidate in path.parent.iterdir()
|
|
6569
|
+
if candidate.is_file()
|
|
6570
|
+
and candidate.name.startswith(numbered_prefix)
|
|
6571
|
+
and candidate.name.endswith(".json")
|
|
6572
|
+
))
|
|
6573
|
+
return (canonical, *numbered)
|
|
6574
|
+
|
|
6575
|
+
|
|
6576
|
+
def _available_v2_companion_path(path: Path, source_digest: str) -> Path:
|
|
6577
|
+
candidate = _content_addressed_v2_companion_path(path, source_digest)
|
|
6578
|
+
suffix = 1
|
|
6579
|
+
while candidate.exists():
|
|
6580
|
+
candidate = _content_addressed_v2_companion_path(
|
|
6581
|
+
path,
|
|
6582
|
+
source_digest,
|
|
6583
|
+
suffix,
|
|
6584
|
+
)
|
|
6585
|
+
suffix += 1
|
|
6586
|
+
return candidate
|
|
6587
|
+
|
|
6588
|
+
|
|
6589
|
+
def _legacy_source_metadata(path: Path, source: bytes) -> dict[str, str]:
|
|
6590
|
+
return {
|
|
6591
|
+
"path": str(path.resolve()),
|
|
6592
|
+
"sha256": hashlib.sha256(source).hexdigest(),
|
|
6593
|
+
}
|
|
6594
|
+
|
|
6595
|
+
|
|
6596
|
+
def _matching_v2_companion(
|
|
6597
|
+
path: Path,
|
|
6598
|
+
source_metadata: dict[str, str],
|
|
6599
|
+
) -> dict[str, Any] | None:
|
|
6600
|
+
try:
|
|
6601
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
6602
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
6603
|
+
return None
|
|
6604
|
+
if not isinstance(payload, dict):
|
|
6605
|
+
return None
|
|
6606
|
+
if payload.get("executionIdentityVersion") != 2:
|
|
6607
|
+
return None
|
|
6608
|
+
if payload.get(_LEGACY_SOURCE_FIELD) != source_metadata:
|
|
6609
|
+
return None
|
|
6610
|
+
return payload
|
|
6611
|
+
|
|
6612
|
+
|
|
6613
|
+
def _load_matching_v2_state(
|
|
6614
|
+
paths: tuple[Path, ...],
|
|
6615
|
+
source_metadata: dict[str, str],
|
|
6616
|
+
) -> tuple[WizardState | None, Path | None]:
|
|
6617
|
+
for companion_path in paths:
|
|
6618
|
+
payload = _matching_v2_companion(companion_path, source_metadata)
|
|
6619
|
+
if payload is None:
|
|
6620
|
+
continue
|
|
6621
|
+
try:
|
|
6622
|
+
return WizardState.from_json(payload), companion_path
|
|
6623
|
+
except WizardError:
|
|
6624
|
+
continue
|
|
6625
|
+
return None, None
|
|
6626
|
+
|
|
6627
|
+
|
|
5282
6628
|
def load_state_file(path: Path) -> WizardState:
|
|
5283
|
-
|
|
5284
|
-
|
|
6629
|
+
source_path = Path(path)
|
|
6630
|
+
source = source_path.read_bytes()
|
|
6631
|
+
data = json.loads(source)
|
|
6632
|
+
source_version = data.get("executionIdentityVersion", 1)
|
|
6633
|
+
source_metadata = _legacy_source_metadata(source_path, source)
|
|
6634
|
+
source_digest = source_metadata["sha256"]
|
|
6635
|
+
resumed_state = None
|
|
6636
|
+
companion_path = None
|
|
6637
|
+
if source_version in (None, 1):
|
|
6638
|
+
content_paths = _content_addressed_v2_companion_paths(
|
|
6639
|
+
source_path,
|
|
6640
|
+
source_digest,
|
|
6641
|
+
)
|
|
6642
|
+
resumed_state, companion_path = _load_matching_v2_state(
|
|
6643
|
+
content_paths,
|
|
6644
|
+
source_metadata,
|
|
6645
|
+
)
|
|
6646
|
+
if resumed_state is None:
|
|
6647
|
+
resumed_state, _ = _load_matching_v2_state(
|
|
6648
|
+
(_legacy_v2_companion_path(source_path),),
|
|
6649
|
+
source_metadata,
|
|
6650
|
+
)
|
|
6651
|
+
state = resumed_state or WizardState.from_json(data)
|
|
6652
|
+
state._source_execution_identity_version = source_version
|
|
6653
|
+
if source_version in (None, 1):
|
|
6654
|
+
state._legacy_source_metadata = source_metadata
|
|
6655
|
+
state._v2_companion_path = companion_path or _available_v2_companion_path(
|
|
6656
|
+
source_path,
|
|
6657
|
+
source_digest,
|
|
6658
|
+
)
|
|
6659
|
+
return state
|
|
6660
|
+
|
|
6661
|
+
|
|
6662
|
+
def _save_resumed_state_file(path: Path, state: WizardState) -> None:
|
|
6663
|
+
"""Persist native v2 state while keeping legacy resume sources read-only."""
|
|
6664
|
+
if getattr(state, "_source_execution_identity_version", 2) in (None, 1):
|
|
6665
|
+
payload = state.to_json()
|
|
6666
|
+
payload[_LEGACY_SOURCE_FIELD] = state._legacy_source_metadata
|
|
6667
|
+
write_json_atomic(state._v2_companion_path, payload)
|
|
6668
|
+
return
|
|
6669
|
+
save_state_file(path, state)
|
|
5285
6670
|
|
|
5286
6671
|
|
|
5287
6672
|
def save_state_file(path: Path, state: WizardState) -> None:
|
|
5288
|
-
Path(path).
|
|
5289
|
-
json.dumps(state.to_json(), indent=2, ensure_ascii=False),
|
|
5290
|
-
encoding="utf-8",
|
|
5291
|
-
)
|
|
6673
|
+
write_json_atomic(Path(path), state.to_json())
|
|
5292
6674
|
|
|
5293
6675
|
|
|
5294
6676
|
# ---- CLI entrypoint -----------------------------------------------------
|
|
@@ -5366,6 +6748,7 @@ def _cli(argv: list[str]) -> int:
|
|
|
5366
6748
|
|
|
5367
6749
|
if args.cmd == "step":
|
|
5368
6750
|
state = load_state_file(state_path)
|
|
6751
|
+
persisted_state = copy.deepcopy(state)
|
|
5369
6752
|
if args.no_submit and args.answer is not None:
|
|
5370
6753
|
print(json.dumps(
|
|
5371
6754
|
{"ok": False, "error": "--no-submit and --answer are mutually exclusive"},
|
|
@@ -5390,7 +6773,7 @@ def _cli(argv: list[str]) -> int:
|
|
|
5390
6773
|
else:
|
|
5391
6774
|
result = submit(state, args.answer)
|
|
5392
6775
|
except WizardError as exc:
|
|
5393
|
-
|
|
6776
|
+
_save_resumed_state_file(state_path, persisted_state)
|
|
5394
6777
|
try:
|
|
5395
6778
|
current = prompt_payload(state, next_prompt(state))
|
|
5396
6779
|
except WizardError:
|
|
@@ -5402,7 +6785,7 @@ def _cli(argv: list[str]) -> int:
|
|
|
5402
6785
|
print(json.dumps({"ok": False, "error": str(exc), "current": current},
|
|
5403
6786
|
ensure_ascii=False, indent=2))
|
|
5404
6787
|
return 0
|
|
5405
|
-
|
|
6788
|
+
_save_resumed_state_file(state_path, state)
|
|
5406
6789
|
print(json.dumps({"ok": True, **result}, ensure_ascii=False, indent=2))
|
|
5407
6790
|
return 0
|
|
5408
6791
|
|