okstra 0.174.0 → 0.175.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -9
- package/bin/okstra +8 -4
- package/dist/cli-registry.d.mts +16 -0
- package/dist/cli-registry.mjs +560 -0
- package/dist/cli-registry.mjs.map +1 -0
- package/dist/commands/execute/agent-prompt.d.mts +1 -0
- package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
- package/dist/commands/execute/agent-prompt.mjs.map +1 -0
- package/dist/commands/execute/codex-dispatch.d.mts +3 -0
- package/dist/commands/execute/codex-dispatch.mjs +6 -0
- package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
- package/dist/commands/execute/codex-run.d.mts +3 -0
- package/{src → dist}/commands/execute/codex-run.mjs +30 -37
- package/dist/commands/execute/codex-run.mjs.map +1 -0
- package/dist/commands/execute/convergence.d.mts +1 -0
- package/{src → dist}/commands/execute/convergence.mjs +15 -16
- package/dist/commands/execute/convergence.mjs.map +1 -0
- package/dist/commands/execute/error-log.d.mts +1 -0
- package/{src → dist}/commands/execute/error-log.mjs +2 -3
- package/dist/commands/execute/error-log.mjs.map +1 -0
- package/dist/commands/execute/git-reconcile.d.mts +1 -0
- package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
- package/dist/commands/execute/git-reconcile.mjs.map +1 -0
- package/dist/commands/execute/handoff.d.mts +1 -0
- package/{src → dist}/commands/execute/handoff.mjs +10 -11
- package/dist/commands/execute/handoff.mjs.map +1 -0
- package/dist/commands/execute/incremental-carry.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
- package/dist/commands/execute/incremental-carry.mjs.map +1 -0
- package/dist/commands/execute/incremental-scope.d.mts +1 -0
- package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
- package/dist/commands/execute/incremental-scope.mjs.map +1 -0
- package/dist/commands/execute/integrate-stages.d.mts +1 -0
- package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
- package/dist/commands/execute/integrate-stages.mjs.map +1 -0
- package/dist/commands/execute/pane-title.d.mts +1 -0
- package/dist/commands/execute/pane-title.mjs +20 -0
- package/dist/commands/execute/pane-title.mjs.map +1 -0
- package/dist/commands/execute/plan-items.d.mts +1 -0
- package/dist/commands/execute/plan-items.mjs +9 -0
- package/dist/commands/execute/plan-items.mjs.map +1 -0
- package/dist/commands/execute/plan-validate.d.mts +1 -0
- package/dist/commands/execute/plan-validate.mjs +68 -0
- package/dist/commands/execute/plan-validate.mjs.map +1 -0
- package/dist/commands/execute/plan-verify.d.mts +1 -0
- package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
- package/dist/commands/execute/plan-verify.mjs.map +1 -0
- package/dist/commands/execute/render-bundle.d.mts +3 -0
- package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
- package/dist/commands/execute/render-bundle.mjs.map +1 -0
- package/dist/commands/execute/run.d.mts +24 -0
- package/dist/commands/execute/run.mjs +196 -0
- package/dist/commands/execute/run.mjs.map +1 -0
- package/dist/commands/execute/spawn-followups.d.mts +1 -0
- package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
- package/dist/commands/execute/spawn-followups.mjs.map +1 -0
- package/dist/commands/execute/team.d.mts +3 -0
- package/{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 +4 -0
- package/docs/architecture.md +7 -1
- package/docs/cli.md +26 -6
- package/docs/contributor-change-matrix.md +2 -2
- package/docs/project-structure-overview.md +16 -7
- package/docs/task-process/README.md +2 -0
- package/docs/task-process/implementation-planning.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/package.json +11 -5
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +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/usage.sh +4 -4
- package/runtime/bin/okstra-error-log.py +27 -0
- package/runtime/bin/okstra-provider-exec.py +217 -5
- package/runtime/bin/okstra.sh +10 -0
- package/runtime/prompts/lead/convergence.md +27 -18
- package/runtime/prompts/lead/okstra-lead-contract.md +3 -1
- package/runtime/prompts/lead/plan-body-verification.md +10 -3
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/change-impact-analysis.md +16 -0
- package/runtime/prompts/profiles/error-analysis.md +20 -0
- package/runtime/prompts/profiles/feature-analysis.md +16 -0
- package/runtime/prompts/profiles/final-verification.md +16 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +16 -0
- package/runtime/prompts/profiles/implementation-planning.md +20 -0
- package/runtime/prompts/profiles/implementation.md +15 -0
- package/runtime/prompts/profiles/improvement-discovery.md +16 -0
- package/runtime/prompts/profiles/project-analysis.md +16 -0
- package/runtime/prompts/profiles/release-handoff.md +4 -0
- package/runtime/prompts/profiles/requirements-discovery.md +20 -0
- package/runtime/prompts/wizard/prompts.ko.json +18 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/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/agent_activity.py +5 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
- package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
- package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
- package/runtime/python/okstra_ctl/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 +1092 -63
- 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 +32 -1
- package/runtime/python/okstra_ctl/domain/provider.py +212 -6
- package/runtime/python/okstra_ctl/domain/role.py +125 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +13 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +34 -1
- package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
- package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
- package/runtime/python/okstra_ctl/error_log_write.py +9 -0
- package/runtime/python/okstra_ctl/error_report.py +21 -1
- package/runtime/python/okstra_ctl/execution_identity.py +529 -0
- package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
- package/runtime/python/okstra_ctl/index.py +6 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
- package/runtime/python/okstra_ctl/legacy_model_selection.py +490 -0
- package/runtime/python/okstra_ctl/model_cli.py +278 -0
- package/runtime/python/okstra_ctl/model_defaults.py +46 -0
- package/runtime/python/okstra_ctl/model_discovery.py +26 -7
- package/runtime/python/okstra_ctl/model_pool.py +501 -0
- package/runtime/python/okstra_ctl/models.py +47 -27
- package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
- package/runtime/python/okstra_ctl/pane_title.py +151 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
- package/runtime/python/okstra_ctl/render.py +78 -10
- package/runtime/python/okstra_ctl/render_final_report.py +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 +217 -0
- package/runtime/python/okstra_ctl/run.py +1207 -258
- package/runtime/python/okstra_ctl/run_index_row.py +7 -1
- 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 +1253 -62
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
- 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/write_policy.py +571 -0
- package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
- package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
- package/runtime/schemas/final-report-v2.0.schema.json +25 -0
- package/runtime/skills/okstra-run/SKILL.md +9 -9
- package/runtime/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/worker-prompt-preamble.md +6 -2
- package/runtime/validators/lib/fixtures.sh +49 -16
- package/runtime/validators/validate-run.py +429 -35
- 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
|
@@ -84,6 +84,16 @@ def _write_json(path: Path, payload: dict) -> None:
|
|
|
84
84
|
_write_text(path, json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def _execution_identity(ctx: Mapping[str, object]) -> dict:
|
|
88
|
+
raw = ctx.get("EXECUTION_IDENTITY_JSON")
|
|
89
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
90
|
+
return {}
|
|
91
|
+
payload = json.loads(raw)
|
|
92
|
+
if not isinstance(payload, dict):
|
|
93
|
+
raise TokenRenderError("EXECUTION_IDENTITY_JSON must contain an object")
|
|
94
|
+
return payload
|
|
95
|
+
|
|
96
|
+
|
|
87
97
|
def _lead_runtime(ctx: dict) -> str:
|
|
88
98
|
return ctx.get("LEAD_RUNTIME", "") or "claude-code"
|
|
89
99
|
|
|
@@ -377,10 +387,14 @@ def _agent_contract(ctx: dict) -> dict:
|
|
|
377
387
|
return parsed
|
|
378
388
|
|
|
379
389
|
|
|
380
|
-
def _executor_contract(ctx: dict) -> dict:
|
|
390
|
+
def _executor_contract(ctx: dict) -> dict | None:
|
|
381
391
|
runner = ctx.get("EXECUTOR_RUNNER", "")
|
|
382
392
|
dispatch_mode = ctx.get("EXECUTOR_DISPATCH_MODE", "")
|
|
383
393
|
host_model_value = ctx.get("EXECUTOR_HOST_MODEL_VALUE") or None
|
|
394
|
+
if not runner and not ctx.get("EXECUTOR_PROVIDER"):
|
|
395
|
+
if ctx.get("TASK_TYPE") == "implementation":
|
|
396
|
+
raise ValueError("implementation executor assignment is required")
|
|
397
|
+
return None
|
|
384
398
|
expected_mode = {
|
|
385
399
|
"native-session": "host-native",
|
|
386
400
|
"cli-wrapper": "worker-dispatch",
|
|
@@ -1326,8 +1340,14 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
1326
1340
|
latest_team_state_relative = pointers["latestTeamStatePath"]
|
|
1327
1341
|
latest_resume_command_relative = pointers["latestResumeCommandPath"]
|
|
1328
1342
|
convergence_block = _build_convergence_block(ctx)
|
|
1343
|
+
execution_identity = _execution_identity(ctx)
|
|
1344
|
+
existing_v2 = (
|
|
1345
|
+
existing.get("schemaVersion") == "2.0"
|
|
1346
|
+
and existing.get("executionIdentityVersion") == 2
|
|
1347
|
+
)
|
|
1348
|
+
writes_v2 = execution_identity.get("executionIdentityVersion") == 2
|
|
1329
1349
|
payload = {
|
|
1330
|
-
"schemaVersion": "1.0",
|
|
1350
|
+
"schemaVersion": "2.0" if existing_v2 or writes_v2 else "1.0",
|
|
1331
1351
|
"projectId": ctx.get("PROJECT_ID", ""),
|
|
1332
1352
|
"taskGroup": ctx.get("TASK_GROUP", ""),
|
|
1333
1353
|
"taskId": ctx.get("TASK_ID", ""),
|
|
@@ -1487,6 +1507,12 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
1487
1507
|
"sessionId": ctx.get("CLAUDE_SESSION_ID", ""),
|
|
1488
1508
|
"resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
|
|
1489
1509
|
}
|
|
1510
|
+
if existing_v2 or writes_v2:
|
|
1511
|
+
existing_index = existing.get("roleExecutionIndex")
|
|
1512
|
+
payload["executionIdentityVersion"] = 2
|
|
1513
|
+
payload["roleExecutionIndex"] = (
|
|
1514
|
+
list(existing_index) if isinstance(existing_index, list) else []
|
|
1515
|
+
)
|
|
1490
1516
|
_write_json(path, payload)
|
|
1491
1517
|
|
|
1492
1518
|
|
|
@@ -1608,8 +1634,9 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1608
1634
|
for s in str(ctx.get("CONCURRENT_RUN_STAGES", "") or "").split(",")
|
|
1609
1635
|
if s.strip().isdigit()
|
|
1610
1636
|
]
|
|
1637
|
+
execution_identity = _execution_identity(ctx)
|
|
1611
1638
|
payload = {
|
|
1612
|
-
"schemaVersion": "1.0",
|
|
1639
|
+
"schemaVersion": execution_identity.get("schemaVersion", "1.0"),
|
|
1613
1640
|
"okstraVersion": ctx.get("OKSTRA_VERSION", ""),
|
|
1614
1641
|
"projectId": ctx.get("PROJECT_ID", ""),
|
|
1615
1642
|
"taskGroup": ctx.get("TASK_GROUP", ""),
|
|
@@ -1783,6 +1810,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1783
1810
|
)
|
|
1784
1811
|
if _lead_runtime(ctx) == "claude-code":
|
|
1785
1812
|
payload["claudeSessionId"] = ctx.get("CLAUDE_SESSION_ID", "")
|
|
1813
|
+
payload.update(execution_identity)
|
|
1786
1814
|
_guard_invocation_manifest_rewrite(existing_run_manifest, payload)
|
|
1787
1815
|
_write_json(Path(run_manifest_path), payload)
|
|
1788
1816
|
|
|
@@ -1795,6 +1823,11 @@ _MODEL_ASSIGNMENT_KEYS = {
|
|
|
1795
1823
|
"hostRuntime",
|
|
1796
1824
|
"hostModelValue",
|
|
1797
1825
|
}
|
|
1826
|
+
_V2_INVOCATION_IDENTITY_KEYS = {
|
|
1827
|
+
"participantRef",
|
|
1828
|
+
"roleExecutionRef",
|
|
1829
|
+
"dutyId",
|
|
1830
|
+
}
|
|
1798
1831
|
_AGENT_CONTRACT_KEYS = {
|
|
1799
1832
|
"schemaVersion",
|
|
1800
1833
|
"dutyRootPath",
|
|
@@ -1860,11 +1893,8 @@ def _validate_invocation_manifest(payload: Mapping[str, object]) -> None:
|
|
|
1860
1893
|
or not assignments
|
|
1861
1894
|
):
|
|
1862
1895
|
raise ValueError("incomplete invocation manifest")
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
for item in assignments.values()
|
|
1866
|
-
):
|
|
1867
|
-
raise ValueError("incomplete invocation manifest assignments")
|
|
1896
|
+
_invocation_manifest_identity_version(payload)
|
|
1897
|
+
_validate_invocation_assignment_shapes(assignments)
|
|
1868
1898
|
_validate_compatibility_projections(payload, assignments)
|
|
1869
1899
|
for key in (
|
|
1870
1900
|
item
|
|
@@ -1901,13 +1931,17 @@ def _validate_compatibility_projections(
|
|
|
1901
1931
|
workers = payload.get("workerAssignments")
|
|
1902
1932
|
if not isinstance(lead, Mapping) or not isinstance(workers, list):
|
|
1903
1933
|
raise ValueError("incomplete invocation manifest compatibility projection")
|
|
1904
|
-
if _model_projection(lead) !=
|
|
1934
|
+
if _model_projection(lead) != _model_projection_from_assignment(
|
|
1935
|
+
assignments.get("lead")
|
|
1936
|
+
):
|
|
1905
1937
|
raise ValueError("immutable invocation manifest lead projection drift")
|
|
1906
1938
|
for worker in workers:
|
|
1907
1939
|
if not isinstance(worker, Mapping):
|
|
1908
1940
|
raise ValueError("incomplete invocation manifest worker projection")
|
|
1909
1941
|
reference = f"initial/{worker.get('workerId', '')}"
|
|
1910
|
-
if _model_projection(worker) !=
|
|
1942
|
+
if _model_projection(worker) != _model_projection_from_assignment(
|
|
1943
|
+
assignments.get(reference)
|
|
1944
|
+
):
|
|
1911
1945
|
raise ValueError("immutable invocation manifest worker projection drift")
|
|
1912
1946
|
|
|
1913
1947
|
|
|
@@ -1915,6 +1949,40 @@ def _model_projection(value: Mapping[str, object]) -> dict[str, object]:
|
|
|
1915
1949
|
return {key: value.get(key) for key in _MODEL_ASSIGNMENT_KEYS}
|
|
1916
1950
|
|
|
1917
1951
|
|
|
1952
|
+
def _model_projection_from_assignment(value: object) -> dict[str, object] | None:
|
|
1953
|
+
if not isinstance(value, Mapping) or not _MODEL_ASSIGNMENT_KEYS.issubset(value):
|
|
1954
|
+
return None
|
|
1955
|
+
return _model_projection(value)
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
def _invocation_manifest_identity_version(payload: Mapping[str, object]) -> int:
|
|
1959
|
+
schema = payload.get("schemaVersion")
|
|
1960
|
+
identity = payload.get("executionIdentityVersion")
|
|
1961
|
+
if schema == "2.0" and identity == 2:
|
|
1962
|
+
return 2
|
|
1963
|
+
if schema == "2.0" or identity is not None:
|
|
1964
|
+
raise ValueError("invocation manifest mixes v1 and v2 execution identity")
|
|
1965
|
+
if schema not in (None, 1, "1", "1.0"):
|
|
1966
|
+
raise ValueError("unsupported invocation manifest execution identity version")
|
|
1967
|
+
return 1
|
|
1968
|
+
|
|
1969
|
+
|
|
1970
|
+
def _validate_invocation_assignment_shapes(
|
|
1971
|
+
assignments: Mapping[str, object],
|
|
1972
|
+
) -> None:
|
|
1973
|
+
allowed = {frozenset(_MODEL_ASSIGNMENT_KEYS)}
|
|
1974
|
+
for item in assignments.values():
|
|
1975
|
+
if not isinstance(item, Mapping):
|
|
1976
|
+
raise ValueError("incomplete invocation manifest assignments")
|
|
1977
|
+
keys = set(item)
|
|
1978
|
+
if keys & _V2_INVOCATION_IDENTITY_KEYS:
|
|
1979
|
+
raise ValueError(
|
|
1980
|
+
"invocation assignment duplicates v2 execution identity"
|
|
1981
|
+
)
|
|
1982
|
+
if frozenset(keys) not in allowed:
|
|
1983
|
+
raise ValueError("incomplete invocation manifest assignments")
|
|
1984
|
+
|
|
1985
|
+
|
|
1918
1986
|
def render_timeline(timeline_path: str, ctx: dict) -> None:
|
|
1919
1987
|
task_manifest_path = Path(ctx.get("TASK_MANIFEST_PATH", ""))
|
|
1920
1988
|
task_manifest = {}
|
|
@@ -566,6 +566,8 @@ def _ai_markdown_context(data: dict, schema: dict | None) -> dict:
|
|
|
566
566
|
context["md_rest"] = sections.rest
|
|
567
567
|
context["md_has"] = sections.has
|
|
568
568
|
context["md_claim"] = sections.mark_rendered
|
|
569
|
+
context["executionRolesTable"] = render_execution_roles_markdown(data)
|
|
570
|
+
context["executionIdentityVersion"] = data.get("executionIdentityVersion")
|
|
569
571
|
return context
|
|
570
572
|
|
|
571
573
|
|
|
@@ -623,6 +625,23 @@ _OPTIONAL_ANALYSIS_DEFAULTS = (
|
|
|
623
625
|
)
|
|
624
626
|
|
|
625
627
|
|
|
628
|
+
def render_execution_roles_markdown(data: dict) -> str:
|
|
629
|
+
roles = data.get("executionRoles") or []
|
|
630
|
+
if data.get("executionIdentityVersion") != 2:
|
|
631
|
+
return "legacy execution roles"
|
|
632
|
+
lines = ["| Role | Label | Status |", "|---|---|---|"]
|
|
633
|
+
for row in roles:
|
|
634
|
+
lines.append(
|
|
635
|
+
f"| {row.get('role', '')} | {row.get('executionLabel', '')} | "
|
|
636
|
+
f"{row.get('status', '')} |"
|
|
637
|
+
)
|
|
638
|
+
return "\n".join(lines)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def render_execution_roles_html(data: dict) -> str:
|
|
642
|
+
return render_execution_roles_markdown(data)
|
|
643
|
+
|
|
644
|
+
|
|
626
645
|
def _with_optional_defaults(data: dict) -> dict:
|
|
627
646
|
"""Render context with schema-optional fields filled in.
|
|
628
647
|
|
|
@@ -170,6 +170,45 @@ def html_template_for(task_type: str) -> str:
|
|
|
170
170
|
raise ValueError(f"no task-specific html template for: {task_type}") from exc
|
|
171
171
|
|
|
172
172
|
|
|
173
|
+
def execution_roles_from_manifest(manifest: dict) -> list[dict]:
|
|
174
|
+
"""Copy the exact RoleExecution set. Do not invent rows."""
|
|
175
|
+
if int(manifest.get("executionIdentityVersion") or 0) != 2:
|
|
176
|
+
return []
|
|
177
|
+
roles = []
|
|
178
|
+
for row in manifest.get("roleExecutions") or []:
|
|
179
|
+
if not isinstance(row, dict):
|
|
180
|
+
continue
|
|
181
|
+
roles.append({
|
|
182
|
+
"roleExecutionRef": row.get("roleExecutionRef"),
|
|
183
|
+
"role": row.get("role"),
|
|
184
|
+
"provider": row.get("provider"),
|
|
185
|
+
"modelRef": row.get("modelRef"),
|
|
186
|
+
"ordinal": row.get("ordinal"),
|
|
187
|
+
"executionLabel": row.get("executionLabel"),
|
|
188
|
+
"status": row.get("status"),
|
|
189
|
+
})
|
|
190
|
+
return roles
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def apply_execution_roles(data: dict, manifest: dict) -> dict:
|
|
194
|
+
"""Copy the manifest RoleExecution set onto a final-report payload."""
|
|
195
|
+
updated = dict(data)
|
|
196
|
+
if int(manifest.get("executionIdentityVersion") or 0) != 2:
|
|
197
|
+
return updated
|
|
198
|
+
updated["executionIdentityVersion"] = 2
|
|
199
|
+
updated["executionRoles"] = execution_roles_from_manifest(manifest)
|
|
200
|
+
return updated
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def assert_execution_roles_match_manifest(report: dict, manifest: dict) -> None:
|
|
204
|
+
report_refs = {row.get("roleExecutionRef") for row in report.get("executionRoles") or []}
|
|
205
|
+
manifest_refs = {
|
|
206
|
+
row.get("roleExecutionRef") for row in manifest.get("roleExecutions") or []
|
|
207
|
+
}
|
|
208
|
+
if report_refs != manifest_refs:
|
|
209
|
+
raise ValueError("report executionRoles must equal manifest roleExecutions")
|
|
210
|
+
|
|
211
|
+
|
|
173
212
|
def markdown_template_for(task_type: str) -> str:
|
|
174
213
|
try:
|
|
175
214
|
return TASK_TYPE_MARKDOWN_TEMPLATE[task_type]
|
|
@@ -38,6 +38,7 @@ from pathlib import Path
|
|
|
38
38
|
from typing import Any, Callable, Mapping, Sequence
|
|
39
39
|
|
|
40
40
|
from .agent_activity import ActivityProjectionError, project_agent_activity
|
|
41
|
+
from .report_contract import apply_execution_roles
|
|
41
42
|
from .dispatch_state import DispatchError, link_agent_dispatch_result
|
|
42
43
|
from .final_report_paths import final_report_data_path, final_report_markdown_path
|
|
43
44
|
from .paths import task_dir, task_manifest_file
|
|
@@ -388,6 +389,7 @@ def run_finalize(
|
|
|
388
389
|
steps: list[dict[str, Any]] = []
|
|
389
390
|
deferred = ""
|
|
390
391
|
try:
|
|
392
|
+
write_execution_roles(ctx)
|
|
391
393
|
commands = build_commands(ctx)
|
|
392
394
|
except FinalizeError as exc:
|
|
393
395
|
return {"ok": False, "reason": str(exc), "steps": steps}
|
|
@@ -465,6 +467,20 @@ def run_finalize(
|
|
|
465
467
|
return {"ok": True, "reason": "", "steps": steps}
|
|
466
468
|
|
|
467
469
|
|
|
470
|
+
def write_execution_roles(ctx: FinalizeContext) -> None:
|
|
471
|
+
"""Write the exact manifest role set onto the final-report data.json."""
|
|
472
|
+
if not ctx.data_path.is_file() or not ctx.manifest_path.is_file():
|
|
473
|
+
return
|
|
474
|
+
manifest = _load_manifest(ctx.manifest_path)
|
|
475
|
+
data = _load_manifest(ctx.data_path)
|
|
476
|
+
updated = apply_execution_roles(dict(data), dict(manifest))
|
|
477
|
+
if updated == data:
|
|
478
|
+
return
|
|
479
|
+
tmp = ctx.data_path.with_suffix(ctx.data_path.suffix + ".tmp")
|
|
480
|
+
tmp.write_text(json.dumps(updated, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
481
|
+
tmp.replace(ctx.data_path)
|
|
482
|
+
|
|
483
|
+
|
|
468
484
|
def _link_lead_result_for_validation(ctx: FinalizeContext) -> None:
|
|
469
485
|
"""Link the lead-authored synthesis artifact before Phase 7 validation."""
|
|
470
486
|
manifest = _load_manifest(ctx.manifest_path)
|
|
@@ -155,6 +155,7 @@ def render_v2_html_view(
|
|
|
155
155
|
# Every task type ends with the same run-cost section, so it is bound
|
|
156
156
|
# here rather than in ten view models that would each rebuild it.
|
|
157
157
|
"runUsage": run_usage(data),
|
|
158
|
+
"executionRoles": data.get("executionRoles") or [],
|
|
158
159
|
"css": (root / "html/assets/base.css").read_text(encoding="utf-8"),
|
|
159
160
|
"js": response_js + "\n" + base_js,
|
|
160
161
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Parse canonical role requirements declared in phase profiles."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .domain.role import RoleCatalogError, normalize_role, role_for_duty
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RoleProfileError(ValueError):
|
|
11
|
+
"""Raised when a profile's structured role declaration is invalid."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RoleRequirement:
|
|
16
|
+
role: str
|
|
17
|
+
count: int
|
|
18
|
+
duty: str
|
|
19
|
+
optional_count: int = 0
|
|
20
|
+
min_distinct_providers: int | None = None
|
|
21
|
+
dynamic: bool = False
|
|
22
|
+
|
|
23
|
+
def signature(self) -> tuple[str, int, int, str, int | None, bool]:
|
|
24
|
+
return (
|
|
25
|
+
self.role,
|
|
26
|
+
self.count,
|
|
27
|
+
self.optional_count,
|
|
28
|
+
self.duty,
|
|
29
|
+
self.min_distinct_providers,
|
|
30
|
+
self.dynamic,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class RoleProfile:
|
|
36
|
+
roles: tuple[RoleRequirement, ...]
|
|
37
|
+
|
|
38
|
+
def role_signature(self) -> tuple[tuple[str, int, int, str, int | None, bool], ...]:
|
|
39
|
+
return tuple(requirement.signature() for requirement in self.roles)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_REQUIRED_FIELDS = frozenset({"role", "count", "duty"})
|
|
43
|
+
_ALLOWED_FIELDS = _REQUIRED_FIELDS | {
|
|
44
|
+
"optionalCount",
|
|
45
|
+
"minDistinctProviders",
|
|
46
|
+
"dynamic",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_role_profile(path: Path) -> RoleProfile:
|
|
51
|
+
"""Load and validate the one fenced YAML ``roles:`` declaration in a profile."""
|
|
52
|
+
profile_path = Path(path)
|
|
53
|
+
try:
|
|
54
|
+
text = profile_path.read_text(encoding="utf-8")
|
|
55
|
+
except OSError as exc:
|
|
56
|
+
raise RoleProfileError(f"cannot read role profile: {profile_path}") from exc
|
|
57
|
+
raw_roles, errors = _parse_roles_block(text)
|
|
58
|
+
requirements = tuple(
|
|
59
|
+
_validate_requirement(raw, index, errors)
|
|
60
|
+
for index, raw in enumerate(raw_roles, 1)
|
|
61
|
+
)
|
|
62
|
+
_validate_static_role_duplicates(requirements, errors)
|
|
63
|
+
if errors:
|
|
64
|
+
raise RoleProfileError("invalid role profile:\n- " + "\n- ".join(errors))
|
|
65
|
+
return RoleProfile(requirements)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parse_roles_block(text: str) -> tuple[list[dict[str, str]], list[str]]:
|
|
69
|
+
blocks = _yaml_blocks_with_roles(text)
|
|
70
|
+
if len(blocks) != 1:
|
|
71
|
+
return [], ["profile must declare exactly one fenced YAML roles block"]
|
|
72
|
+
lines = blocks[0]
|
|
73
|
+
if lines == ["roles: []"]:
|
|
74
|
+
return [], []
|
|
75
|
+
if not lines or lines[0] != "roles:":
|
|
76
|
+
return [], ["roles block must start with roles:"]
|
|
77
|
+
roles: list[dict[str, str]] = []
|
|
78
|
+
errors: list[str] = []
|
|
79
|
+
current: dict[str, str] | None = None
|
|
80
|
+
for line_number, line in enumerate(lines[1:], 2):
|
|
81
|
+
if not line.strip():
|
|
82
|
+
continue
|
|
83
|
+
if line.startswith(" - "):
|
|
84
|
+
if current is not None:
|
|
85
|
+
roles.append(current)
|
|
86
|
+
current = {}
|
|
87
|
+
_add_yaml_field(current, line[4:], line_number, errors)
|
|
88
|
+
continue
|
|
89
|
+
if line.startswith(" ") and current is not None:
|
|
90
|
+
_add_yaml_field(current, line[4:], line_number, errors)
|
|
91
|
+
continue
|
|
92
|
+
errors.append(f"roles block line {line_number} is not a role field")
|
|
93
|
+
if current is not None:
|
|
94
|
+
roles.append(current)
|
|
95
|
+
elif not errors:
|
|
96
|
+
errors.append("roles block must contain a role or use roles: []")
|
|
97
|
+
return roles, errors
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _yaml_blocks_with_roles(text: str) -> list[list[str]]:
|
|
101
|
+
blocks: list[list[str]] = []
|
|
102
|
+
current: list[str] | None = None
|
|
103
|
+
for line in text.splitlines():
|
|
104
|
+
if line.strip() == "```yaml":
|
|
105
|
+
current = []
|
|
106
|
+
continue
|
|
107
|
+
if line.strip() == "```" and current is not None:
|
|
108
|
+
if any(item.strip().startswith("roles:") for item in current):
|
|
109
|
+
blocks.append(current)
|
|
110
|
+
current = None
|
|
111
|
+
continue
|
|
112
|
+
if current is not None:
|
|
113
|
+
current.append(line)
|
|
114
|
+
return blocks
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _add_yaml_field(
|
|
118
|
+
fields: dict[str, str], line: str, line_number: int, errors: list[str],
|
|
119
|
+
) -> None:
|
|
120
|
+
if ":" not in line:
|
|
121
|
+
errors.append(f"roles block line {line_number} must contain key: value")
|
|
122
|
+
return
|
|
123
|
+
key, value = (part.strip() for part in line.split(":", 1))
|
|
124
|
+
if not key or not value:
|
|
125
|
+
errors.append(f"roles block line {line_number} must contain key: value")
|
|
126
|
+
return
|
|
127
|
+
if key in fields:
|
|
128
|
+
errors.append(f"roles block line {line_number} repeats {key}")
|
|
129
|
+
return
|
|
130
|
+
fields[key] = value
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _validate_requirement(
|
|
134
|
+
raw: dict[str, str], index: int, errors: list[str],
|
|
135
|
+
) -> RoleRequirement:
|
|
136
|
+
prefix = f"role requirement {index}"
|
|
137
|
+
for field in sorted(_REQUIRED_FIELDS - raw.keys()):
|
|
138
|
+
errors.append(f"{prefix} is missing {field}")
|
|
139
|
+
for field in sorted(raw.keys() - _ALLOWED_FIELDS):
|
|
140
|
+
errors.append(f"{prefix} has unknown field {field}")
|
|
141
|
+
|
|
142
|
+
role = _validate_role(raw.get("role", ""), prefix, errors)
|
|
143
|
+
count = _validate_non_negative_integer(raw.get("count", ""), "count", prefix, errors)
|
|
144
|
+
duty = _validate_duty(raw.get("duty", ""), role, prefix, errors)
|
|
145
|
+
optional_count = _validate_non_negative_integer(
|
|
146
|
+
raw.get("optionalCount", "0"), "optionalCount", prefix, errors,
|
|
147
|
+
)
|
|
148
|
+
diversity = _validate_diversity(raw.get("minDistinctProviders"), prefix, errors)
|
|
149
|
+
dynamic = _validate_dynamic(raw.get("dynamic", "false"), prefix, errors)
|
|
150
|
+
if diversity is not None and diversity > count + optional_count:
|
|
151
|
+
errors.append(f"{prefix} minDistinctProviders must not exceed count + optionalCount")
|
|
152
|
+
if dynamic and (count != 0 or optional_count != 0):
|
|
153
|
+
errors.append(f"{prefix} dynamic roles must have count and optionalCount of zero")
|
|
154
|
+
if dynamic and diversity is not None:
|
|
155
|
+
errors.append(f"{prefix} dynamic roles must not declare minDistinctProviders")
|
|
156
|
+
return RoleRequirement(role, count, duty, optional_count, diversity, dynamic)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _validate_role(raw: str, prefix: str, errors: list[str]) -> str:
|
|
160
|
+
try:
|
|
161
|
+
return normalize_role(raw)
|
|
162
|
+
except RoleCatalogError as exc:
|
|
163
|
+
errors.append(f"{prefix} {exc}")
|
|
164
|
+
return raw
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _validate_duty(raw: str, role: str, prefix: str, errors: list[str]) -> str:
|
|
168
|
+
try:
|
|
169
|
+
duty_role = role_for_duty(raw)
|
|
170
|
+
except RoleCatalogError as exc:
|
|
171
|
+
errors.append(f"{prefix} {exc}")
|
|
172
|
+
return raw
|
|
173
|
+
if duty_role != role:
|
|
174
|
+
errors.append(f"{prefix} duty {raw} belongs to role {duty_role}, not {role}")
|
|
175
|
+
return raw
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _validate_non_negative_integer(raw: str, field: str, prefix: str, errors: list[str]) -> int:
|
|
179
|
+
try:
|
|
180
|
+
value = int(raw)
|
|
181
|
+
except ValueError:
|
|
182
|
+
errors.append(f"{prefix} {field} must be a non-negative integer")
|
|
183
|
+
return 0
|
|
184
|
+
if value < 0 or str(value) != raw:
|
|
185
|
+
errors.append(f"{prefix} {field} must be a non-negative integer")
|
|
186
|
+
return 0
|
|
187
|
+
return value
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _validate_diversity(raw: str | None, prefix: str, errors: list[str]) -> int | None:
|
|
191
|
+
if raw is None:
|
|
192
|
+
return None
|
|
193
|
+
value = _validate_non_negative_integer(raw, "minDistinctProviders", prefix, errors)
|
|
194
|
+
if value == 0:
|
|
195
|
+
errors.append(f"{prefix} minDistinctProviders must be at least one")
|
|
196
|
+
return value
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _validate_dynamic(raw: str, prefix: str, errors: list[str]) -> bool:
|
|
200
|
+
if raw == "true":
|
|
201
|
+
return True
|
|
202
|
+
if raw == "false":
|
|
203
|
+
return False
|
|
204
|
+
errors.append(f"{prefix} dynamic must be true or false")
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _validate_static_role_duplicates(
|
|
209
|
+
requirements: tuple[RoleRequirement, ...], errors: list[str],
|
|
210
|
+
) -> None:
|
|
211
|
+
static_roles: set[str] = set()
|
|
212
|
+
for requirement in requirements:
|
|
213
|
+
if requirement.dynamic:
|
|
214
|
+
continue
|
|
215
|
+
if requirement.role in static_roles:
|
|
216
|
+
errors.append(f"duplicate static role: {requirement.role}")
|
|
217
|
+
static_roles.add(requirement.role)
|