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
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
"""Deterministic role-instance model assignment and host binding."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
from typing import Literal, Mapping, Sequence
|
|
7
|
+
|
|
8
|
+
from .application.resolve_assignment import (
|
|
9
|
+
resolve_assignment_runner,
|
|
10
|
+
resolve_lead_provider_id,
|
|
11
|
+
resolve_lead_provider_from_specs,
|
|
12
|
+
)
|
|
13
|
+
from .domain.host import HostDescriptor, HostSessionContext
|
|
14
|
+
from .domain.provider import (
|
|
15
|
+
ExecutionNormalization,
|
|
16
|
+
HostModelBinding,
|
|
17
|
+
LeadProviderAssignment,
|
|
18
|
+
)
|
|
19
|
+
from .domain.role import normalize_role
|
|
20
|
+
from .domain.worker_exec import WorkerWriteCapability
|
|
21
|
+
from .model_defaults import (
|
|
22
|
+
ModelDefaultError,
|
|
23
|
+
ModelDefaultScopes,
|
|
24
|
+
default_candidates,
|
|
25
|
+
default_scope,
|
|
26
|
+
)
|
|
27
|
+
from .model_pool import ModelPool, ResolvedModel
|
|
28
|
+
from .ports.host_model import (
|
|
29
|
+
HostModelBindingError,
|
|
30
|
+
HostModelBindingPort,
|
|
31
|
+
HostModelBindingRequest,
|
|
32
|
+
)
|
|
33
|
+
from .role_requirements import RoleProfile, RoleRequirement
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
TerminalBackend = Literal[
|
|
37
|
+
"cli-wrapper",
|
|
38
|
+
"cmux-pane",
|
|
39
|
+
"mixed",
|
|
40
|
+
]
|
|
41
|
+
_TERMINAL_BACKENDS = frozenset({
|
|
42
|
+
"cli-wrapper",
|
|
43
|
+
"cmux-pane",
|
|
44
|
+
"mixed",
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class AssignmentEnvironment:
|
|
50
|
+
"""Immutable host and dispatch facts consumed by assignment resolution."""
|
|
51
|
+
|
|
52
|
+
host_descriptor: HostDescriptor
|
|
53
|
+
host_model_port: HostModelBindingPort
|
|
54
|
+
native_worker_write_capability: WorkerWriteCapability
|
|
55
|
+
terminal_backend: TerminalBackend
|
|
56
|
+
execution_resolutions: Mapping[
|
|
57
|
+
tuple[str, str],
|
|
58
|
+
ExecutionNormalization,
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
if self.terminal_backend not in _TERMINAL_BACKENDS:
|
|
63
|
+
allowed = ", ".join(sorted(_TERMINAL_BACKENDS))
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"unknown terminal backend {self.terminal_backend!r}. "
|
|
66
|
+
f"Allowed values: {allowed}"
|
|
67
|
+
)
|
|
68
|
+
resolutions: dict[tuple[str, str], ExecutionNormalization] = {}
|
|
69
|
+
for key, resolution in self.execution_resolutions.items():
|
|
70
|
+
if (
|
|
71
|
+
not isinstance(key, tuple)
|
|
72
|
+
or len(key) != 2
|
|
73
|
+
or not all(isinstance(value, str) and value for value in key)
|
|
74
|
+
or not isinstance(resolution, ExecutionNormalization)
|
|
75
|
+
):
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"execution resolutions require (model_ref, role) keys and "
|
|
78
|
+
"ExecutionNormalization values"
|
|
79
|
+
)
|
|
80
|
+
resolutions[key] = resolution
|
|
81
|
+
object.__setattr__(
|
|
82
|
+
self,
|
|
83
|
+
"execution_resolutions",
|
|
84
|
+
MappingProxyType(resolutions),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class AssignmentContext:
|
|
90
|
+
pool: ModelPool
|
|
91
|
+
environment: AssignmentEnvironment
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class RoleCountSelection:
|
|
96
|
+
role: str
|
|
97
|
+
count: int
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class RoleModelSelection:
|
|
102
|
+
role: str
|
|
103
|
+
model_refs: tuple[str, ...]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class RoleInstance:
|
|
108
|
+
role: str
|
|
109
|
+
duty_id: str
|
|
110
|
+
ordinal: int
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class CandidateAssessment:
|
|
115
|
+
role: str
|
|
116
|
+
ordinal: int
|
|
117
|
+
model_ref: str
|
|
118
|
+
scope: str
|
|
119
|
+
available: bool
|
|
120
|
+
reason: str = ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class UnsatisfiedConstraint:
|
|
125
|
+
role: str
|
|
126
|
+
constraint: str
|
|
127
|
+
required: int
|
|
128
|
+
available: int
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class ResolvedAssignment:
|
|
133
|
+
instance: RoleInstance
|
|
134
|
+
model_ref: str | None
|
|
135
|
+
provider_id: str
|
|
136
|
+
model_id: str
|
|
137
|
+
display_name: str
|
|
138
|
+
host_runtime: str
|
|
139
|
+
entry_mode: Literal["current-session", "new-session"]
|
|
140
|
+
binding: HostModelBinding | None
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def role(self) -> str:
|
|
144
|
+
return self.instance.role
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def duty_id(self) -> str:
|
|
148
|
+
return self.instance.duty_id
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def ordinal(self) -> int:
|
|
152
|
+
return self.instance.ordinal
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def display(self) -> str:
|
|
156
|
+
return self.display_name
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def execution(self) -> str:
|
|
160
|
+
return self.binding.resolved_execution_value if self.binding else ""
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def execution_label(self) -> str:
|
|
164
|
+
return f"{self.role}-{self.provider_id}-{self.model_id}-{self.ordinal}"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True)
|
|
168
|
+
class AssignmentPlan:
|
|
169
|
+
assignments: tuple[ResolvedAssignment, ...]
|
|
170
|
+
candidates: tuple[CandidateAssessment, ...] = ()
|
|
171
|
+
|
|
172
|
+
def by_role(self, role: str) -> tuple[ResolvedAssignment, ...]:
|
|
173
|
+
return tuple(row for row in self.assignments if row.role == role)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class AssignmentResolutionError(ValueError):
|
|
177
|
+
"""Raised with all evaluated candidates and failed constraints."""
|
|
178
|
+
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
message: str,
|
|
182
|
+
*,
|
|
183
|
+
candidates: Sequence[CandidateAssessment] = (),
|
|
184
|
+
unsatisfied_constraints: Sequence[UnsatisfiedConstraint] = (),
|
|
185
|
+
) -> None:
|
|
186
|
+
self.candidates = tuple(candidates)
|
|
187
|
+
self.unsatisfied_constraints = tuple(unsatisfied_constraints)
|
|
188
|
+
super().__init__(message)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _validate_environment(
|
|
192
|
+
host: HostSessionContext,
|
|
193
|
+
environment: AssignmentEnvironment,
|
|
194
|
+
) -> None:
|
|
195
|
+
if environment.host_descriptor.id != host.host_id:
|
|
196
|
+
raise AssignmentResolutionError(
|
|
197
|
+
"assignment environment host does not match the session context: "
|
|
198
|
+
f"{environment.host_descriptor.id!r} != {host.host_id!r}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def resolve_assignments(
|
|
203
|
+
*,
|
|
204
|
+
profile: RoleProfile,
|
|
205
|
+
role_counts: Mapping[str, int],
|
|
206
|
+
role_models: Mapping[str, tuple[str, ...]],
|
|
207
|
+
scopes: ModelDefaultScopes,
|
|
208
|
+
pool: ModelPool,
|
|
209
|
+
host: HostSessionContext,
|
|
210
|
+
environment: AssignmentEnvironment,
|
|
211
|
+
) -> AssignmentPlan:
|
|
212
|
+
"""Resolve the first complete assignment in deterministic DFS order."""
|
|
213
|
+
_validate_environment(host, environment)
|
|
214
|
+
requirements = _static_requirements(profile)
|
|
215
|
+
instances = _role_instances(requirements, role_counts)
|
|
216
|
+
_validate_model_selections(instances, role_models)
|
|
217
|
+
current_leader = _current_leader_assignment(
|
|
218
|
+
instances,
|
|
219
|
+
role_models,
|
|
220
|
+
pool,
|
|
221
|
+
host,
|
|
222
|
+
environment,
|
|
223
|
+
)
|
|
224
|
+
options, assessments = _assignment_options(
|
|
225
|
+
instances,
|
|
226
|
+
role_models,
|
|
227
|
+
scopes,
|
|
228
|
+
pool,
|
|
229
|
+
host,
|
|
230
|
+
profile,
|
|
231
|
+
requirements,
|
|
232
|
+
current_leader,
|
|
233
|
+
environment,
|
|
234
|
+
)
|
|
235
|
+
resolved = _first_complete_assignment(options, requirements)
|
|
236
|
+
if resolved is None:
|
|
237
|
+
constraints = _unmet_constraints(options, requirements)
|
|
238
|
+
raise AssignmentResolutionError(
|
|
239
|
+
"no complete role assignment satisfies the profile constraints",
|
|
240
|
+
candidates=assessments,
|
|
241
|
+
unsatisfied_constraints=constraints,
|
|
242
|
+
)
|
|
243
|
+
return AssignmentPlan(tuple(resolved), tuple(assessments))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def resolve_model_assignment(
|
|
247
|
+
*,
|
|
248
|
+
instance: RoleInstance,
|
|
249
|
+
model_ref: str,
|
|
250
|
+
pool: ModelPool,
|
|
251
|
+
host: HostSessionContext,
|
|
252
|
+
environment: AssignmentEnvironment,
|
|
253
|
+
entry_mode: Literal["current-session", "new-session"] = "new-session",
|
|
254
|
+
) -> ResolvedAssignment:
|
|
255
|
+
"""Resolve one canonical model through the selected host execution path."""
|
|
256
|
+
_validate_environment(host, environment)
|
|
257
|
+
model = pool.resolve(model_ref)
|
|
258
|
+
availability = pool.availability(
|
|
259
|
+
model_ref,
|
|
260
|
+
instance.role,
|
|
261
|
+
host.host_id,
|
|
262
|
+
entry_mode,
|
|
263
|
+
)
|
|
264
|
+
if not availability.available:
|
|
265
|
+
raise AssignmentResolutionError(
|
|
266
|
+
f"model {model_ref!r} is unavailable: {availability.reason}"
|
|
267
|
+
)
|
|
268
|
+
binding = _first_supported_binding(
|
|
269
|
+
instance,
|
|
270
|
+
model,
|
|
271
|
+
pool,
|
|
272
|
+
environment,
|
|
273
|
+
)
|
|
274
|
+
return _resolved_assignment(instance, model, host, entry_mode, binding)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def resolve_dispatch_assignment(
|
|
278
|
+
*,
|
|
279
|
+
context: AssignmentContext,
|
|
280
|
+
host_runtime: str,
|
|
281
|
+
role: str,
|
|
282
|
+
duty_id: str,
|
|
283
|
+
provider: str,
|
|
284
|
+
model: str = "",
|
|
285
|
+
) -> ResolvedAssignment:
|
|
286
|
+
"""Resolve one legacy provider/model input through the shared core."""
|
|
287
|
+
canonical_role = normalize_role(role)
|
|
288
|
+
pool = context.pool
|
|
289
|
+
selected = resolve_model_selection(pool, provider, canonical_role, model)
|
|
290
|
+
host = HostSessionContext(
|
|
291
|
+
host_id=host_runtime,
|
|
292
|
+
entry_mode="spawn-process",
|
|
293
|
+
available_functions=frozenset(),
|
|
294
|
+
interaction_surface="application-dispatch",
|
|
295
|
+
)
|
|
296
|
+
return resolve_model_assignment(
|
|
297
|
+
instance=RoleInstance(canonical_role, duty_id, 1),
|
|
298
|
+
model_ref=str(selected.model_ref),
|
|
299
|
+
pool=pool,
|
|
300
|
+
host=host,
|
|
301
|
+
environment=context.environment,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def resolve_context_lead_provider(
|
|
306
|
+
*,
|
|
307
|
+
context: AssignmentContext,
|
|
308
|
+
requested_provider: str,
|
|
309
|
+
) -> LeadProviderAssignment:
|
|
310
|
+
"""Resolve a lead provider from the same immutable assignment facts."""
|
|
311
|
+
host = context.environment.host_descriptor
|
|
312
|
+
provider_id = resolve_lead_provider_id(
|
|
313
|
+
host=host,
|
|
314
|
+
requested_provider=requested_provider,
|
|
315
|
+
)
|
|
316
|
+
provider = context.pool.resolve_provider(provider_id)
|
|
317
|
+
return resolve_lead_provider_from_specs(
|
|
318
|
+
host=host,
|
|
319
|
+
requested_provider=requested_provider,
|
|
320
|
+
provider=provider,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def resolve_model_selection(
|
|
325
|
+
pool: ModelPool,
|
|
326
|
+
provider: str,
|
|
327
|
+
role: str,
|
|
328
|
+
raw_model: str,
|
|
329
|
+
) -> ResolvedModel:
|
|
330
|
+
"""Resolve one explicit or provider-default catalog model without binding."""
|
|
331
|
+
if raw_model:
|
|
332
|
+
return pool.resolve_alias(provider, raw_model)
|
|
333
|
+
selected = pool.default_candidate(provider, role)
|
|
334
|
+
if selected is None:
|
|
335
|
+
raise AssignmentResolutionError(
|
|
336
|
+
f"provider {provider!r} has no default model for role {role!r}"
|
|
337
|
+
)
|
|
338
|
+
return selected
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _static_requirements(profile: RoleProfile) -> tuple[RoleRequirement, ...]:
|
|
342
|
+
leader = RoleRequirement("leader", 1, 1, 1, "lead")
|
|
343
|
+
return (leader, *(row for row in profile.roles if not row.dynamic))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _role_instances(
|
|
347
|
+
requirements: Sequence[RoleRequirement],
|
|
348
|
+
role_counts: Mapping[str, int],
|
|
349
|
+
) -> tuple[RoleInstance, ...]:
|
|
350
|
+
allowed = {row.role for row in requirements}
|
|
351
|
+
unknown = tuple(sorted(set(role_counts) - allowed))
|
|
352
|
+
if unknown:
|
|
353
|
+
raise AssignmentResolutionError(
|
|
354
|
+
f"role counts contain roles outside the profile: {', '.join(unknown)}"
|
|
355
|
+
)
|
|
356
|
+
instances: list[RoleInstance] = []
|
|
357
|
+
for requirement in requirements:
|
|
358
|
+
fixed = requirement.min_count == requirement.max_count
|
|
359
|
+
if requirement.role in role_counts and fixed:
|
|
360
|
+
raise AssignmentResolutionError(
|
|
361
|
+
f"role {requirement.role!r} has a fixed quantity and cannot "
|
|
362
|
+
"receive an explicit count"
|
|
363
|
+
)
|
|
364
|
+
count = role_counts.get(requirement.role, requirement.recommended_count)
|
|
365
|
+
if count < requirement.min_count or count > requirement.max_count:
|
|
366
|
+
raise AssignmentResolutionError(
|
|
367
|
+
f"role {requirement.role!r} count must be in "
|
|
368
|
+
f"{requirement.min_count}..{requirement.max_count}: {count}"
|
|
369
|
+
)
|
|
370
|
+
instances.extend(
|
|
371
|
+
RoleInstance(requirement.role, requirement.duty, ordinal)
|
|
372
|
+
for ordinal in range(1, count + 1)
|
|
373
|
+
)
|
|
374
|
+
return tuple(instances)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _validate_model_selections(
|
|
378
|
+
instances: Sequence[RoleInstance],
|
|
379
|
+
role_models: Mapping[str, tuple[str, ...]],
|
|
380
|
+
) -> None:
|
|
381
|
+
counts = {
|
|
382
|
+
role: sum(instance.role == role for instance in instances)
|
|
383
|
+
for role in {instance.role for instance in instances}
|
|
384
|
+
}
|
|
385
|
+
unknown = tuple(sorted(set(role_models) - counts.keys()))
|
|
386
|
+
if unknown:
|
|
387
|
+
raise AssignmentResolutionError(
|
|
388
|
+
f"role models contain roles outside the profile: {', '.join(unknown)}"
|
|
389
|
+
)
|
|
390
|
+
for role, models in role_models.items():
|
|
391
|
+
if len(models) > counts[role]:
|
|
392
|
+
raise AssignmentResolutionError(
|
|
393
|
+
f"role {role!r} has {len(models)} models for {counts[role]} instances"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _current_leader_assignment(
|
|
398
|
+
instances: Sequence[RoleInstance],
|
|
399
|
+
role_models: Mapping[str, tuple[str, ...]],
|
|
400
|
+
pool: ModelPool,
|
|
401
|
+
host: HostSessionContext,
|
|
402
|
+
environment: AssignmentEnvironment,
|
|
403
|
+
) -> ResolvedAssignment | None:
|
|
404
|
+
if host.entry_mode != "current-session":
|
|
405
|
+
return None
|
|
406
|
+
leader = next(row for row in instances if row.role == "leader")
|
|
407
|
+
attestation = host.current_model
|
|
408
|
+
model = _validated_current_model(pool, host)
|
|
409
|
+
explicit = role_models.get("leader", ())
|
|
410
|
+
if explicit and (model is None or explicit[0] != str(model.model_ref)):
|
|
411
|
+
return None
|
|
412
|
+
if model is None:
|
|
413
|
+
provider_id = (
|
|
414
|
+
attestation.provider_id
|
|
415
|
+
or environment.host_descriptor.native_provider_id
|
|
416
|
+
)
|
|
417
|
+
return ResolvedAssignment(
|
|
418
|
+
leader,
|
|
419
|
+
None,
|
|
420
|
+
provider_id,
|
|
421
|
+
"unknown",
|
|
422
|
+
"unknown",
|
|
423
|
+
host.host_id,
|
|
424
|
+
"current-session",
|
|
425
|
+
None,
|
|
426
|
+
)
|
|
427
|
+
binding = HostModelBinding(
|
|
428
|
+
runner="current-session",
|
|
429
|
+
catalog_execution_value=model.execution_value,
|
|
430
|
+
resolved_execution_value=model.execution_value,
|
|
431
|
+
host_model_value=attestation.observed_model,
|
|
432
|
+
binding_fidelity=attestation.level,
|
|
433
|
+
worker_write_capability=None,
|
|
434
|
+
)
|
|
435
|
+
return _resolved_assignment(leader, model, host, "current-session", binding)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _validated_current_model(
|
|
439
|
+
pool: ModelPool,
|
|
440
|
+
host: HostSessionContext,
|
|
441
|
+
) -> ResolvedModel | None:
|
|
442
|
+
attestation = host.current_model
|
|
443
|
+
if attestation.level == "unknown":
|
|
444
|
+
return None
|
|
445
|
+
model_ref = attestation.normalized_model_ref or ""
|
|
446
|
+
try:
|
|
447
|
+
model = pool.resolve(model_ref)
|
|
448
|
+
except ValueError as exc:
|
|
449
|
+
action = "update the provider adapter catalog or start a registered leader model"
|
|
450
|
+
raise AssignmentResolutionError(
|
|
451
|
+
"unregistered current-session model attestation: "
|
|
452
|
+
f"provider={attestation.provider_id!r}, "
|
|
453
|
+
f"model={attestation.observed_model!r}, source={attestation.source!r}; "
|
|
454
|
+
f"{action}"
|
|
455
|
+
) from exc
|
|
456
|
+
if model.provider_id != attestation.provider_id:
|
|
457
|
+
raise AssignmentResolutionError(
|
|
458
|
+
"current-session model attestation provider does not match its model ref"
|
|
459
|
+
)
|
|
460
|
+
if attestation.level == "exact" and model.version_kind != "pinned":
|
|
461
|
+
raise AssignmentResolutionError(
|
|
462
|
+
"exact current-session attestation must reference a pinned model"
|
|
463
|
+
)
|
|
464
|
+
if attestation.level == "channel" and model.version_kind != "channel":
|
|
465
|
+
raise AssignmentResolutionError(
|
|
466
|
+
"channel current-session attestation must reference a channel model"
|
|
467
|
+
)
|
|
468
|
+
availability = pool.availability(
|
|
469
|
+
str(model.model_ref),
|
|
470
|
+
"leader",
|
|
471
|
+
host.host_id,
|
|
472
|
+
"current-session",
|
|
473
|
+
)
|
|
474
|
+
if not availability.available:
|
|
475
|
+
raise AssignmentResolutionError(
|
|
476
|
+
"current-session model is unavailable for leader: "
|
|
477
|
+
f"{availability.reason}"
|
|
478
|
+
)
|
|
479
|
+
return model
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _assignment_options(
|
|
483
|
+
instances: Sequence[RoleInstance],
|
|
484
|
+
role_models: Mapping[str, tuple[str, ...]],
|
|
485
|
+
scopes: ModelDefaultScopes,
|
|
486
|
+
pool: ModelPool,
|
|
487
|
+
host: HostSessionContext,
|
|
488
|
+
profile: RoleProfile,
|
|
489
|
+
requirements: Sequence[RoleRequirement],
|
|
490
|
+
current_leader: ResolvedAssignment | None,
|
|
491
|
+
environment: AssignmentEnvironment,
|
|
492
|
+
) -> tuple[list[tuple[ResolvedAssignment, ...]], list[CandidateAssessment]]:
|
|
493
|
+
options: list[tuple[ResolvedAssignment, ...]] = []
|
|
494
|
+
assessments: list[CandidateAssessment] = []
|
|
495
|
+
for instance in instances:
|
|
496
|
+
if current_leader is not None and instance.role == "leader":
|
|
497
|
+
options.append((current_leader,))
|
|
498
|
+
continue
|
|
499
|
+
rows, evaluated = _instance_options(
|
|
500
|
+
instance,
|
|
501
|
+
role_models,
|
|
502
|
+
scopes,
|
|
503
|
+
pool,
|
|
504
|
+
host,
|
|
505
|
+
profile,
|
|
506
|
+
environment,
|
|
507
|
+
)
|
|
508
|
+
options.append(rows)
|
|
509
|
+
assessments.extend(evaluated)
|
|
510
|
+
if any(not rows for rows in options):
|
|
511
|
+
raise AssignmentResolutionError(
|
|
512
|
+
"one or more role instances have no executable model candidates",
|
|
513
|
+
candidates=assessments,
|
|
514
|
+
unsatisfied_constraints=_unmet_constraints(options, requirements),
|
|
515
|
+
)
|
|
516
|
+
return options, assessments
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _instance_options(
|
|
520
|
+
instance: RoleInstance,
|
|
521
|
+
role_models: Mapping[str, tuple[str, ...]],
|
|
522
|
+
scopes: ModelDefaultScopes,
|
|
523
|
+
pool: ModelPool,
|
|
524
|
+
host: HostSessionContext,
|
|
525
|
+
profile: RoleProfile,
|
|
526
|
+
environment: AssignmentEnvironment,
|
|
527
|
+
) -> tuple[tuple[ResolvedAssignment, ...], list[CandidateAssessment]]:
|
|
528
|
+
explicit = role_models.get(instance.role, ())
|
|
529
|
+
if instance.ordinal <= len(explicit):
|
|
530
|
+
candidates, scope = (explicit[instance.ordinal - 1],), "explicit"
|
|
531
|
+
else:
|
|
532
|
+
candidates, scope = _rotated_defaults(instance, scopes, pool)
|
|
533
|
+
resolved: list[ResolvedAssignment] = []
|
|
534
|
+
assessments: list[CandidateAssessment] = []
|
|
535
|
+
for model_ref in candidates:
|
|
536
|
+
try:
|
|
537
|
+
assignment = resolve_model_assignment(
|
|
538
|
+
instance=instance,
|
|
539
|
+
model_ref=model_ref,
|
|
540
|
+
pool=pool,
|
|
541
|
+
host=host,
|
|
542
|
+
environment=environment,
|
|
543
|
+
)
|
|
544
|
+
resolved.append(assignment)
|
|
545
|
+
assessments.append(_assessment(instance, model_ref, scope, True, ""))
|
|
546
|
+
except (AssignmentResolutionError, ValueError, RuntimeError) as exc:
|
|
547
|
+
assessments.append(
|
|
548
|
+
_assessment(instance, model_ref, scope, False, str(exc))
|
|
549
|
+
)
|
|
550
|
+
if scope != "bundled" and any(not row.available for row in assessments):
|
|
551
|
+
return (), assessments
|
|
552
|
+
return tuple(resolved), assessments
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _rotated_defaults(
|
|
556
|
+
instance: RoleInstance,
|
|
557
|
+
scopes: ModelDefaultScopes,
|
|
558
|
+
pool: ModelPool,
|
|
559
|
+
) -> tuple[tuple[str, ...], str]:
|
|
560
|
+
try:
|
|
561
|
+
candidates = default_candidates(instance.role, scopes)
|
|
562
|
+
except ModelDefaultError as exc:
|
|
563
|
+
raise AssignmentResolutionError(str(exc)) from exc
|
|
564
|
+
scope = default_scope(instance.role, scopes)
|
|
565
|
+
if not candidates:
|
|
566
|
+
candidates = tuple(
|
|
567
|
+
str(model.model_ref) for model in pool.default_candidates(instance.role)
|
|
568
|
+
)
|
|
569
|
+
scope = "bundled"
|
|
570
|
+
if not candidates:
|
|
571
|
+
raise AssignmentResolutionError(
|
|
572
|
+
f"role {instance.role!r} has no default model candidates"
|
|
573
|
+
)
|
|
574
|
+
offset = (instance.ordinal - 1) % len(candidates)
|
|
575
|
+
return (*candidates[offset:], *candidates[:offset]), scope or "bundled"
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _assessment(
|
|
579
|
+
instance: RoleInstance,
|
|
580
|
+
model_ref: str,
|
|
581
|
+
scope: str,
|
|
582
|
+
available: bool,
|
|
583
|
+
reason: str,
|
|
584
|
+
) -> CandidateAssessment:
|
|
585
|
+
return CandidateAssessment(
|
|
586
|
+
instance.role,
|
|
587
|
+
instance.ordinal,
|
|
588
|
+
model_ref,
|
|
589
|
+
scope,
|
|
590
|
+
available,
|
|
591
|
+
reason,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _first_supported_binding(
|
|
596
|
+
instance: RoleInstance,
|
|
597
|
+
model: ResolvedModel,
|
|
598
|
+
pool: ModelPool,
|
|
599
|
+
environment: AssignmentEnvironment,
|
|
600
|
+
) -> HostModelBinding:
|
|
601
|
+
provider = pool.resolve_provider(model.provider_id)
|
|
602
|
+
errors: list[str] = []
|
|
603
|
+
for runner in _runner_candidates(instance, environment, provider):
|
|
604
|
+
try:
|
|
605
|
+
resolve_assignment_runner(
|
|
606
|
+
host=environment.host_descriptor,
|
|
607
|
+
provider=provider,
|
|
608
|
+
role=instance.role,
|
|
609
|
+
requested_runner=runner,
|
|
610
|
+
)
|
|
611
|
+
binding = _binding_for_runner(
|
|
612
|
+
instance,
|
|
613
|
+
model,
|
|
614
|
+
provider,
|
|
615
|
+
environment,
|
|
616
|
+
runner,
|
|
617
|
+
)
|
|
618
|
+
_validate_binding_fidelity(model, binding)
|
|
619
|
+
return binding
|
|
620
|
+
except (ValueError, RuntimeError, AttributeError) as exc:
|
|
621
|
+
errors.append(f"{runner}: {exc}")
|
|
622
|
+
raise AssignmentResolutionError(
|
|
623
|
+
f"model {model.model_ref!r} has no executable binding: {'; '.join(errors)}"
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _runner_candidates(
|
|
628
|
+
instance: RoleInstance,
|
|
629
|
+
environment: AssignmentEnvironment,
|
|
630
|
+
provider,
|
|
631
|
+
) -> tuple[str, ...]:
|
|
632
|
+
if (
|
|
633
|
+
instance.role != "leader"
|
|
634
|
+
and environment.terminal_backend == "cmux-pane"
|
|
635
|
+
):
|
|
636
|
+
return ("cli-wrapper",) if provider.wrapper else ()
|
|
637
|
+
runners: list[str] = []
|
|
638
|
+
if provider.provider == environment.host_descriptor.native_provider_id:
|
|
639
|
+
runners.append("native-session")
|
|
640
|
+
if provider.wrapper:
|
|
641
|
+
runners.append("cli-wrapper")
|
|
642
|
+
return tuple(runners or ("cli-wrapper",))
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _binding_for_runner(
|
|
646
|
+
instance: RoleInstance,
|
|
647
|
+
model: ResolvedModel,
|
|
648
|
+
provider,
|
|
649
|
+
environment: AssignmentEnvironment,
|
|
650
|
+
runner: str,
|
|
651
|
+
) -> HostModelBinding:
|
|
652
|
+
execution = model.execution_value
|
|
653
|
+
if runner == "cli-wrapper":
|
|
654
|
+
resolution = environment.execution_resolutions.get(
|
|
655
|
+
(str(model.model_ref), instance.role)
|
|
656
|
+
)
|
|
657
|
+
if resolution is None and provider.execution_normalization_required:
|
|
658
|
+
raise AssignmentResolutionError(
|
|
659
|
+
f"missing execution snapshot for model {model.model_ref!r} "
|
|
660
|
+
f"and role {instance.role!r}"
|
|
661
|
+
)
|
|
662
|
+
if resolution is not None:
|
|
663
|
+
if resolution.error:
|
|
664
|
+
raise AssignmentResolutionError(resolution.error)
|
|
665
|
+
execution = resolution.resolved_execution_value
|
|
666
|
+
capability = _worker_write_capability(
|
|
667
|
+
instance,
|
|
668
|
+
provider,
|
|
669
|
+
environment,
|
|
670
|
+
runner,
|
|
671
|
+
)
|
|
672
|
+
request = HostModelBindingRequest(
|
|
673
|
+
host_runtime=environment.host_descriptor.id,
|
|
674
|
+
provider=model.provider_id,
|
|
675
|
+
model_ref=str(model.model_ref),
|
|
676
|
+
version_kind=model.version_kind,
|
|
677
|
+
model_execution_value=model.execution_value,
|
|
678
|
+
resolved_execution_value=execution,
|
|
679
|
+
runner=runner,
|
|
680
|
+
worker_write_capability=capability,
|
|
681
|
+
)
|
|
682
|
+
try:
|
|
683
|
+
return environment.host_model_port.resolve(request)
|
|
684
|
+
except HostModelBindingError as exc:
|
|
685
|
+
raise AssignmentResolutionError(str(exc)) from exc
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _worker_write_capability(instance, provider, environment, runner):
|
|
689
|
+
if instance.role == "leader":
|
|
690
|
+
return None
|
|
691
|
+
if runner == "native-session":
|
|
692
|
+
return environment.native_worker_write_capability
|
|
693
|
+
if provider.cli_worker_write_capability is None:
|
|
694
|
+
raise AssignmentResolutionError(
|
|
695
|
+
f"provider {provider.provider!r} has no worker execution strategy"
|
|
696
|
+
)
|
|
697
|
+
return provider.cli_worker_write_capability
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _validate_binding_fidelity(
|
|
701
|
+
model: ResolvedModel,
|
|
702
|
+
binding: HostModelBinding,
|
|
703
|
+
) -> None:
|
|
704
|
+
allowed = {"exact"} if model.version_kind == "pinned" else {"exact", "channel"}
|
|
705
|
+
if binding.binding_fidelity not in allowed:
|
|
706
|
+
raise AssignmentResolutionError(
|
|
707
|
+
f"model {model.model_ref!r} cannot bind with fidelity "
|
|
708
|
+
f"{binding.binding_fidelity!r}"
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _resolved_assignment(instance, model, host, entry_mode, binding):
|
|
713
|
+
return ResolvedAssignment(
|
|
714
|
+
instance=instance,
|
|
715
|
+
model_ref=str(model.model_ref),
|
|
716
|
+
provider_id=model.provider_id,
|
|
717
|
+
model_id=model.model_ref.model_id,
|
|
718
|
+
display_name=model.display_name,
|
|
719
|
+
host_runtime=host.host_id,
|
|
720
|
+
entry_mode=entry_mode,
|
|
721
|
+
binding=binding,
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _first_complete_assignment(
|
|
726
|
+
options: Sequence[Sequence[ResolvedAssignment]],
|
|
727
|
+
requirements: Sequence[RoleRequirement],
|
|
728
|
+
) -> list[ResolvedAssignment] | None:
|
|
729
|
+
chosen: list[ResolvedAssignment] = []
|
|
730
|
+
|
|
731
|
+
def visit(index: int) -> list[ResolvedAssignment] | None:
|
|
732
|
+
if index == len(options):
|
|
733
|
+
return list(chosen) if not _constraint_failures(chosen, requirements) else None
|
|
734
|
+
for assignment in options[index]:
|
|
735
|
+
chosen.append(assignment)
|
|
736
|
+
if (complete := visit(index + 1)) is not None:
|
|
737
|
+
return complete
|
|
738
|
+
chosen.pop()
|
|
739
|
+
return None
|
|
740
|
+
|
|
741
|
+
return visit(0)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _constraint_failures(
|
|
745
|
+
assignments: Sequence[ResolvedAssignment],
|
|
746
|
+
requirements: Sequence[RoleRequirement],
|
|
747
|
+
) -> tuple[UnsatisfiedConstraint, ...]:
|
|
748
|
+
failures: list[UnsatisfiedConstraint] = []
|
|
749
|
+
for requirement in requirements:
|
|
750
|
+
role_rows = [row for row in assignments if row.role == requirement.role]
|
|
751
|
+
if not role_rows:
|
|
752
|
+
continue
|
|
753
|
+
refs = {row.model_ref for row in role_rows}
|
|
754
|
+
if len(refs) < len(role_rows):
|
|
755
|
+
failures.append(UnsatisfiedConstraint(
|
|
756
|
+
requirement.role,
|
|
757
|
+
"uniqueModelRefs",
|
|
758
|
+
len(role_rows),
|
|
759
|
+
len(refs),
|
|
760
|
+
))
|
|
761
|
+
return tuple(failures)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _unmet_constraints(
|
|
765
|
+
options: Sequence[Sequence[ResolvedAssignment]],
|
|
766
|
+
requirements: Sequence[RoleRequirement],
|
|
767
|
+
) -> tuple[UnsatisfiedConstraint, ...]:
|
|
768
|
+
failures: list[UnsatisfiedConstraint] = []
|
|
769
|
+
for requirement in requirements:
|
|
770
|
+
role_groups = [
|
|
771
|
+
candidates
|
|
772
|
+
for candidates in options
|
|
773
|
+
if candidates and candidates[0].role == requirement.role
|
|
774
|
+
]
|
|
775
|
+
if not role_groups:
|
|
776
|
+
continue
|
|
777
|
+
worker_count = len(role_groups)
|
|
778
|
+
unique_refs = {
|
|
779
|
+
row.model_ref
|
|
780
|
+
for candidates in role_groups
|
|
781
|
+
for row in candidates
|
|
782
|
+
}
|
|
783
|
+
if len(unique_refs) < worker_count:
|
|
784
|
+
failures.append(UnsatisfiedConstraint(
|
|
785
|
+
requirement.role,
|
|
786
|
+
"uniqueModelRefs",
|
|
787
|
+
worker_count,
|
|
788
|
+
len(unique_refs),
|
|
789
|
+
))
|
|
790
|
+
return tuple(failures)
|