okstra 0.186.7 → 0.187.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 +1 -1
- package/dist/cli-registry.d.mts +88 -1
- package/dist/cli-registry.mjs +68 -111
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +0 -1
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +8 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.mjs +1 -14
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/config.mjs +38 -40
- package/dist/commands/lifecycle/config.mjs.map +1 -1
- package/dist/commands/lifecycle/doctor.d.mts +22 -7
- package/dist/commands/lifecycle/doctor.mjs +77 -49
- package/dist/commands/lifecycle/doctor.mjs.map +1 -1
- package/dist/commands/lifecycle/install.d.mts +12 -10
- package/dist/commands/lifecycle/install.mjs +104 -39
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/lifecycle/paths.mjs +8 -3
- package/dist/commands/lifecycle/paths.mjs.map +1 -1
- package/dist/commands/lifecycle/preflight.mjs +2 -1
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/lifecycle/setup.mjs +22 -36
- package/dist/commands/lifecycle/setup.mjs.map +1 -1
- package/dist/commands/lifecycle/uninstall.d.mts +4 -2
- package/dist/commands/lifecycle/uninstall.mjs +59 -15
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +7 -1
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/helper-scripts.d.mts +1 -1
- package/dist/lib/helper-scripts.mjs +10 -18
- package/dist/lib/helper-scripts.mjs.map +1 -1
- package/dist/lib/host-config.d.mts +72 -0
- package/dist/lib/host-config.mjs +404 -0
- package/dist/lib/host-config.mjs.map +1 -0
- package/dist/lib/host-registry-client.d.mts +2 -2
- package/dist/lib/host-registry-client.mjs +0 -3
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/dist/lib/install-assets.d.mts +1 -0
- package/dist/lib/install-assets.mjs +4 -0
- package/dist/lib/install-assets.mjs.map +1 -1
- package/dist/lib/proc.d.mts +2 -0
- package/dist/lib/proc.mjs +12 -0
- package/dist/lib/proc.mjs.map +1 -1
- package/dist/lib/python-command.d.mts +2 -0
- package/dist/lib/python-command.mjs +52 -0
- package/dist/lib/python-command.mjs.map +1 -0
- package/dist/lib/python-helper.d.mts +2 -5
- package/dist/lib/python-helper.mjs +3 -52
- package/dist/lib/python-helper.mjs.map +1 -1
- package/dist/lib/runtime-payload.d.mts +23 -0
- package/dist/lib/runtime-payload.mjs +57 -0
- package/dist/lib/runtime-payload.mjs.map +1 -0
- package/dist/lib/types.d.mts +24 -13
- package/docs/architecture/storage-model.md +21 -5
- package/docs/architecture.md +37 -36
- package/docs/cli.md +55 -76
- package/docs/coding-rules.md +295 -0
- package/docs/container.md +10 -36
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/skills/okstra-code-review.md +0 -1
- package/docs/for-ai/skills/okstra-container-build.md +8 -41
- package/docs/for-ai/skills/okstra-inspect.md +4 -4
- package/docs/for-ai/skills/okstra-manager.md +2 -2
- package/docs/for-ai/skills/okstra-rollup.md +0 -1
- package/docs/for-ai/skills/okstra-run.md +3 -3
- package/docs/for-ai/skills/okstra-user-response.md +0 -1
- package/docs/performance-improvement-plan-v2.md +1 -1
- package/docs/project-structure-overview.md +62 -62
- package/docs/task-process/README.md +3 -3
- package/docs/task-process/common-flow.md +1 -1
- package/docs/task-process/error-analysis.md +3 -3
- package/docs/task-process/implementation-planning.md +3 -1
- package/docs/task-process/release-handoff.md +1 -1
- package/package.json +3 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +6 -11
- package/runtime/agents/workers/report-writer-worker.md +1 -1
- package/runtime/bin/lib/okstra/cli.sh +4 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/interactive.sh +26 -173
- package/runtime/bin/lib/okstra/project-resolver.sh +23 -61
- package/runtime/bin/lib/okstra/usage.sh +3 -3
- package/runtime/bin/okstra-compact-reminder.sh +1 -5
- package/runtime/bin/okstra-error-log.py +19 -2
- package/runtime/bin/okstra-import-check.py +26 -0
- package/runtime/bin/okstra-inject-report-index.py +5 -4
- package/runtime/bin/okstra-provider-exec.py +22 -17
- package/runtime/bin/okstra-render-final-report.py +16 -6
- package/runtime/bin/okstra-render-report-views.py +25 -18
- package/runtime/bin/okstra-report-translate.py +30 -18
- package/runtime/bin/okstra-spawn-followups.py +4 -0
- package/runtime/bin/okstra-token-usage.py +3 -0
- package/runtime/bin/okstra.sh +4 -2
- package/runtime/bin/okstra_bootstrap.py +58 -0
- package/runtime/prompts/coding-preflight/overview.md +1 -1
- package/runtime/prompts/duties/planning-worker.md +1 -1
- package/runtime/prompts/launch.template.md +35 -9
- package/runtime/prompts/lead/convergence.md +96 -87
- package/runtime/prompts/lead/okstra-lead-contract.md +63 -12
- package/runtime/prompts/lead/plan-body-verification.md +65 -46
- package/runtime/prompts/lead/report-writer.md +34 -4
- package/runtime/prompts/lead/team-contract.md +4 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +2 -1
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +3 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +16 -2
- package/runtime/prompts/profiles/error-analysis.md +4 -3
- package/runtime/prompts/profiles/final-verification.md +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +22 -17
- package/runtime/prompts/profiles/implementation.md +3 -2
- package/runtime/prompts/profiles/requirements-discovery.md +2 -1
- package/runtime/prompts/wizard/prompts.ko.json +29 -8
- package/runtime/python/okstra_ctl/__init__.py +6 -27
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -5
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +21 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +3 -12
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +40 -22
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +14 -21
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +37 -20
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +86 -14
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +12 -12
- package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +4 -7
- package/runtime/python/okstra_ctl/agent/__init__.py +1 -0
- package/runtime/python/okstra_ctl/{agent_activity.py → agent/activity.py} +12 -1
- package/runtime/python/okstra_ctl/{agent_invocation.py → agent/invocation.py} +34 -5
- package/runtime/python/okstra_ctl/agent/prompt_cli/__init__.py +19 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/__main__.py +11 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +215 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +155 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/emit.py +66 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/inputs.py +138 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +408 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/results.py +197 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +107 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +47 -4
- package/runtime/python/okstra_ctl/approval_decisions.py +260 -8
- package/runtime/python/okstra_ctl/assignment_resolver.py +0 -12
- package/runtime/python/okstra_ctl/attempt_evidence.py +12 -24
- package/runtime/python/okstra_ctl/blocking_checks.py +251 -0
- package/runtime/python/okstra_ctl/brief_frontmatter.py +0 -7
- package/runtime/python/okstra_ctl/clarification_items/__init__.py +102 -0
- package/runtime/python/okstra_ctl/clarification_items/carry.py +224 -0
- package/runtime/python/okstra_ctl/clarification_items/dispositions.py +135 -0
- package/runtime/python/okstra_ctl/clarification_items/parsing.py +253 -0
- package/runtime/python/okstra_ctl/clarification_items/rows.py +115 -0
- package/runtime/python/okstra_ctl/clarification_items/scan.py +215 -0
- package/runtime/python/okstra_ctl/clarification_items/sidecars.py +211 -0
- package/runtime/python/okstra_ctl/cmux.py +38 -31
- package/runtime/python/okstra_ctl/code_review_target.py +163 -1
- package/runtime/python/okstra_ctl/conformance.py +23 -0
- package/runtime/python/okstra_ctl/container.py +38 -421
- package/runtime/python/okstra_ctl/context_cost.py +16 -2
- package/runtime/python/okstra_ctl/contract_graph_cli.py +7 -0
- package/runtime/python/okstra_ctl/convergence.py +101 -15
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +345 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +382 -70
- package/runtime/python/okstra_ctl/convergence_store.py +5 -29
- package/runtime/python/okstra_ctl/design_prep.py +108 -12
- package/runtime/python/okstra_ctl/design_snapshot.py +11 -1
- package/runtime/python/okstra_ctl/design_surfaces.py +29 -2
- package/runtime/python/okstra_ctl/dispatch_core.py +102 -25
- package/runtime/python/okstra_ctl/dispatch_state.py +268 -39
- package/runtime/python/okstra_ctl/doctor_cli.py +48 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +10 -5
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +21 -1
- package/runtime/python/okstra_ctl/domain/worker_stream.py +52 -21
- package/runtime/python/okstra_ctl/domain/write_policy.py +219 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +9 -2
- package/runtime/python/okstra_ctl/error_log_core.py +1 -1
- package/runtime/python/okstra_ctl/error_report.py +14 -0
- package/runtime/python/okstra_ctl/error_zip.py +18 -2
- package/runtime/python/okstra_ctl/execution_identity.py +100 -7
- package/runtime/python/okstra_ctl/execution_manifest.py +67 -20
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +90 -8
- package/runtime/python/okstra_ctl/final_report_paths.py +17 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +90 -16
- package/runtime/python/okstra_ctl/git_reconcile.py +22 -2
- package/runtime/python/okstra_ctl/handoff.py +24 -1
- package/runtime/python/okstra_ctl/ids.py +6 -16
- package/runtime/python/okstra_ctl/implementation_direction.py +142 -33
- package/runtime/python/okstra_ctl/implementation_options.py +25 -11
- package/runtime/python/okstra_ctl/implementation_outcome.py +5 -2
- package/runtime/python/okstra_ctl/improvement_lenses.py +0 -14
- package/runtime/python/okstra_ctl/incremental_carry.py +69 -8
- package/runtime/python/okstra_ctl/incremental_scope.py +173 -12
- package/runtime/python/okstra_ctl/index.py +2 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +48 -8
- package/runtime/python/okstra_ctl/interactive_cli.py +223 -0
- package/runtime/python/okstra_ctl/json_boundary.py +10 -0
- package/runtime/python/okstra_ctl/listing.py +0 -91
- package/runtime/python/okstra_ctl/locks.py +5 -19
- package/runtime/python/okstra_ctl/log_report.py +14 -0
- package/runtime/python/okstra_ctl/manager_cli.py +22 -3
- package/runtime/python/okstra_ctl/manager_launch.py +4 -8
- package/runtime/python/okstra_ctl/material.py +2 -2
- package/runtime/python/okstra_ctl/migrate.py +23 -1
- package/runtime/python/okstra_ctl/model_cli.py +19 -5
- package/runtime/python/okstra_ctl/model_discovery.py +46 -47
- package/runtime/python/okstra_ctl/model_io/__init__.py +1 -0
- package/runtime/python/okstra_ctl/model_io/lines.py +163 -0
- package/runtime/python/okstra_ctl/model_io/references.py +370 -0
- package/runtime/python/okstra_ctl/model_io/renderers.py +498 -0
- package/runtime/python/okstra_ctl/model_io_cli.py +30 -945
- package/runtime/python/okstra_ctl/model_pool.py +10 -6
- package/runtime/python/okstra_ctl/models.py +8 -8
- package/runtime/python/okstra_ctl/mutation_recovery.py +5 -63
- package/runtime/python/okstra_ctl/next_phase.py +121 -69
- package/runtime/python/okstra_ctl/pane_reclaim.py +23 -0
- package/runtime/python/okstra_ctl/pane_title.py +11 -8
- package/runtime/python/okstra_ctl/path_hints.py +36 -11
- package/runtime/python/okstra_ctl/paths.py +76 -8
- package/runtime/python/okstra_ctl/plan_items.py +132 -16
- package/runtime/python/okstra_ctl/plan_items_cli.py +486 -36
- package/runtime/python/okstra_ctl/plan_run_root.py +1 -1
- package/runtime/python/okstra_ctl/plan_validate_cli.py +51 -0
- package/runtime/python/okstra_ctl/plan_verify_cli.py +80 -0
- package/runtime/python/okstra_ctl/prepare_error.py +20 -0
- package/runtime/python/okstra_ctl/prior_planning.py +157 -0
- package/runtime/python/okstra_ctl/profile_show.py +18 -1
- package/runtime/python/okstra_ctl/project_setup_cli.py +140 -0
- package/runtime/python/okstra_ctl/recap.py +22 -1
- package/runtime/python/okstra_ctl/reconcile.py +84 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +18 -1
- package/runtime/python/okstra_ctl/render.py +98 -32
- package/runtime/python/okstra_ctl/render_final_report.py +3 -7
- package/runtime/python/okstra_ctl/report_assembly.py +297 -7
- package/runtime/python/okstra_ctl/report_contract.py +30 -7
- package/runtime/python/okstra_ctl/report_finalize.py +206 -15
- package/runtime/python/okstra_ctl/report_html/common.py +2 -2
- package/runtime/python/okstra_ctl/report_html/render.py +8 -12
- package/runtime/python/okstra_ctl/report_language.py +8 -4
- package/runtime/python/okstra_ctl/report_narrative.py +90 -3
- package/runtime/python/okstra_ctl/report_projections.py +8 -2
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +258 -1
- package/runtime/python/okstra_ctl/report_translation.py +2 -35
- package/runtime/python/okstra_ctl/report_views.py +4 -22
- package/runtime/python/okstra_ctl/resolve_task_key.py +13 -0
- package/runtime/python/okstra_ctl/rollup.py +13 -0
- package/runtime/python/okstra_ctl/run.py +639 -138
- package/runtime/python/okstra_ctl/run_audit.py +13 -0
- package/runtime/python/okstra_ctl/run_index_row.py +0 -12
- package/runtime/python/okstra_ctl/seeding.py +21 -11
- package/runtime/python/okstra_ctl/sequence.py +1 -1
- package/runtime/python/okstra_ctl/session.py +64 -14
- package/runtime/python/okstra_ctl/set_work_status.py +18 -0
- package/runtime/python/okstra_ctl/stage_integrate.py +15 -1
- package/runtime/python/okstra_ctl/stage_ledger.py +1 -1
- package/runtime/python/okstra_ctl/stage_map.py +57 -11
- package/runtime/python/okstra_ctl/stage_map_cli.py +83 -0
- package/runtime/python/okstra_ctl/stage_map_view.py +65 -0
- package/runtime/python/okstra_ctl/stage_targets.py +3 -12
- package/runtime/python/okstra_ctl/task_list_cli.py +138 -0
- package/runtime/python/okstra_ctl/task_show_cli.py +66 -0
- package/runtime/python/okstra_ctl/task_target.py +4 -16
- package/runtime/python/okstra_ctl/team.py +49 -2
- package/runtime/python/okstra_ctl/time_report.py +17 -2
- package/runtime/python/okstra_ctl/usage_report.py +11 -0
- package/runtime/python/okstra_ctl/user_response.py +55 -218
- package/runtime/python/okstra_ctl/user_response_values.py +242 -0
- package/runtime/python/okstra_ctl/validation_contract.py +3 -0
- package/runtime/python/okstra_ctl/verification_target.py +68 -0
- package/runtime/python/okstra_ctl/wizard.py +457 -92
- package/runtime/python/okstra_ctl/worker_artifacts.py +75 -16
- package/runtime/python/okstra_ctl/worker_audit_check.py +22 -0
- package/runtime/python/okstra_ctl/worker_dispatch.py +21 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +33 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +15 -2
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +98 -7
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +24 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +18 -3
- package/runtime/python/okstra_ctl/worker_request.py +2 -1
- package/runtime/python/okstra_ctl/worker_runner.py +16 -9
- package/runtime/python/okstra_ctl/worker_state.py +16 -1
- package/runtime/python/okstra_ctl/workflow.py +18 -1
- package/runtime/python/okstra_ctl/worktree/__init__.py +92 -0
- package/runtime/python/okstra_ctl/worktree/cleanliness.py +89 -0
- package/runtime/python/okstra_ctl/worktree/decisions.py +127 -0
- package/runtime/python/okstra_ctl/worktree/git_ops.py +165 -0
- package/runtime/python/okstra_ctl/worktree/linking.py +249 -0
- package/runtime/python/okstra_ctl/worktree/naming.py +91 -0
- package/runtime/python/okstra_ctl/worktree/provision.py +385 -0
- package/runtime/python/okstra_ctl/worktree/sync_config.py +181 -0
- package/runtime/python/okstra_ctl/worktree_cli.py +75 -0
- package/runtime/python/okstra_ctl/worktree_lookup_cli.py +39 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +7 -0
- package/runtime/python/okstra_ctl/worktree_status_cli.py +53 -0
- package/runtime/python/okstra_ctl/write_policy.py +90 -213
- package/runtime/python/okstra_project/__init__.py +0 -4
- package/runtime/python/okstra_project/dirs.py +2 -2
- package/runtime/python/okstra_project/phase_pointer.py +84 -0
- package/runtime/python/okstra_project/slug.py +24 -0
- package/runtime/python/okstra_project/state.py +22 -230
- package/runtime/python/okstra_token_usage/claude.py +9 -1
- package/runtime/python/okstra_token_usage/cli.py +3 -1
- package/runtime/python/okstra_token_usage/collect.py +71 -10
- package/runtime/python/okstra_token_usage/cursor.py +2 -0
- package/runtime/python/okstra_token_usage/grok.py +1 -5
- package/runtime/python/okstra_token_usage/paths.py +26 -0
- package/runtime/python/okstra_token_usage/pricing.py +19 -7
- package/runtime/schemas/convergence-critic-results-v1.0.schema.json +5 -0
- package/runtime/schemas/execution-manifest-v2.schema.json +10 -10
- package/runtime/schemas/final-report-v2.0.schema.json +9 -26
- package/runtime/schemas/final-report-v3.0.schema.json +173 -41
- package/runtime/schemas/report-narrative-v3.0.schema.json +3 -2
- package/runtime/skills/okstra-brief-gen/SKILL.md +35 -2
- package/runtime/skills/okstra-container-build/SKILL.md +16 -47
- package/runtime/skills/okstra-inspect/facets/history.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +7 -6
- package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +7 -5
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/templates/reports/brief.template.md +6 -2
- package/runtime/templates/reports/error-analysis-input.template.md +2 -0
- package/runtime/templates/reports/final-verification-input.template.md +2 -0
- package/runtime/templates/reports/implementation-input.template.md +7 -1
- package/runtime/templates/reports/implementation-planning-input.template.md +4 -0
- package/runtime/templates/reports/quick-input.template.md +2 -0
- package/runtime/templates/reports/release-handoff-input.template.md +6 -1
- package/runtime/templates/reports/report.js +20 -3
- package/runtime/templates/reports/schedule.template.md +2 -0
- package/runtime/templates/reports/settings.template.json +0 -10
- package/runtime/templates/reports/task-brief.template.md +3 -0
- package/runtime/templates/reports/user-response.template.md +7 -3
- package/runtime/validators/checks/fixtures-01.py +57 -0
- package/runtime/validators/checks/fixtures-02.py +565 -0
- package/runtime/validators/checks/runners-01.py +108 -0
- package/runtime/validators/checks/validate-assets-01.py +60 -0
- package/runtime/validators/checks/validate-prompt-metadata-01.py +261 -0
- package/runtime/validators/checks/validate-tasks-01.py +46 -0
- package/runtime/validators/checks/validate-tasks-02.py +85 -0
- package/runtime/validators/checks/validate-tasks-03.py +61 -0
- package/runtime/validators/checks/validate-tasks-04.py +118 -0
- package/runtime/validators/forbidden_actions.py +73 -3
- package/runtime/validators/lib/common.sh +5 -0
- package/runtime/validators/lib/fixtures.sh +7 -591
- package/runtime/validators/lib/paths.sh +13 -0
- package/runtime/validators/lib/runners.sh +6 -104
- package/runtime/validators/lib/summary.sh +1 -1
- package/runtime/validators/lib/validate-assets.sh +6 -56
- package/runtime/validators/lib/validate-prompt-metadata.sh +6 -257
- package/runtime/validators/lib/validate-tasks.sh +9 -294
- package/runtime/validators/validate-implementation-plan-stages.py +4 -4
- package/runtime/validators/validate-run.py +1369 -2654
- package/runtime/validators/validate-workflow.sh +56 -16
- package/runtime/validators/validate_improvement_report.py +2 -1
- package/runtime/validators/validate_session_conformance.py +295 -49
- package/dist/commands/execute/agent-prompt.d.mts +0 -1
- package/dist/commands/execute/agent-prompt.mjs +0 -24
- package/dist/commands/execute/agent-prompt.mjs.map +0 -1
- package/dist/commands/execute/codex-dispatch.d.mts +0 -3
- package/dist/commands/execute/codex-dispatch.mjs +0 -6
- package/dist/commands/execute/codex-dispatch.mjs.map +0 -1
- package/dist/commands/execute/codex-run.d.mts +0 -3
- package/dist/commands/execute/codex-run.mjs +0 -62
- package/dist/commands/execute/codex-run.mjs.map +0 -1
- package/dist/commands/execute/convergence.d.mts +0 -1
- package/dist/commands/execute/convergence.mjs +0 -38
- package/dist/commands/execute/convergence.mjs.map +0 -1
- package/dist/commands/execute/error-log.d.mts +0 -1
- package/dist/commands/execute/error-log.mjs +0 -18
- package/dist/commands/execute/error-log.mjs.map +0 -1
- package/dist/commands/execute/git-reconcile.d.mts +0 -1
- package/dist/commands/execute/git-reconcile.mjs +0 -30
- package/dist/commands/execute/git-reconcile.mjs.map +0 -1
- package/dist/commands/execute/handoff.d.mts +0 -1
- package/dist/commands/execute/handoff.mjs +0 -31
- package/dist/commands/execute/handoff.mjs.map +0 -1
- package/dist/commands/execute/incremental-carry.d.mts +0 -1
- package/dist/commands/execute/incremental-carry.mjs +0 -20
- package/dist/commands/execute/incremental-carry.mjs.map +0 -1
- package/dist/commands/execute/incremental-scope.d.mts +0 -1
- package/dist/commands/execute/incremental-scope.mjs +0 -29
- package/dist/commands/execute/incremental-scope.mjs.map +0 -1
- package/dist/commands/execute/integrate-stages.d.mts +0 -1
- package/dist/commands/execute/integrate-stages.mjs +0 -24
- package/dist/commands/execute/integrate-stages.mjs.map +0 -1
- package/dist/commands/execute/pane-title.d.mts +0 -1
- package/dist/commands/execute/pane-title.mjs +0 -20
- package/dist/commands/execute/pane-title.mjs.map +0 -1
- package/dist/commands/execute/plan-items.d.mts +0 -1
- package/dist/commands/execute/plan-items.mjs +0 -9
- package/dist/commands/execute/plan-items.mjs.map +0 -1
- package/dist/commands/execute/plan-validate.d.mts +0 -1
- package/dist/commands/execute/plan-validate.mjs +0 -68
- package/dist/commands/execute/plan-validate.mjs.map +0 -1
- package/dist/commands/execute/plan-verify.d.mts +0 -1
- package/dist/commands/execute/plan-verify.mjs +0 -43
- package/dist/commands/execute/plan-verify.mjs.map +0 -1
- package/dist/commands/execute/spawn-followups.d.mts +0 -1
- package/dist/commands/execute/spawn-followups.mjs +0 -22
- package/dist/commands/execute/spawn-followups.mjs.map +0 -1
- package/dist/commands/execute/team.d.mts +0 -3
- package/dist/commands/execute/team.mjs +0 -66
- package/dist/commands/execute/team.mjs.map +0 -1
- package/dist/commands/execute/token-usage.d.mts +0 -1
- package/dist/commands/execute/token-usage.mjs +0 -19
- package/dist/commands/execute/token-usage.mjs.map +0 -1
- package/dist/commands/execute/worker-audit-check.d.mts +0 -1
- package/dist/commands/execute/worker-audit-check.mjs +0 -34
- package/dist/commands/execute/worker-audit-check.mjs.map +0 -1
- package/dist/commands/execute/worker-dispatch.d.mts +0 -7
- package/dist/commands/execute/worker-dispatch.mjs +0 -64
- package/dist/commands/execute/worker-dispatch.mjs.map +0 -1
- package/dist/commands/execute/worker-state.d.mts +0 -1
- package/dist/commands/execute/worker-state.mjs +0 -28
- package/dist/commands/execute/worker-state.mjs.map +0 -1
- package/dist/commands/execute/worktree-lookup.d.mts +0 -1
- package/dist/commands/execute/worktree-lookup.mjs +0 -92
- package/dist/commands/execute/worktree-lookup.mjs.map +0 -1
- package/dist/commands/execute/worktree-status.d.mts +0 -1
- package/dist/commands/execute/worktree-status.mjs +0 -121
- package/dist/commands/execute/worktree-status.mjs.map +0 -1
- package/dist/commands/inspect/code-review.d.mts +0 -1
- package/dist/commands/inspect/code-review.mjs +0 -32
- package/dist/commands/inspect/code-review.mjs.map +0 -1
- package/dist/commands/inspect/container.d.mts +0 -1
- package/dist/commands/inspect/container.mjs +0 -25
- package/dist/commands/inspect/container.mjs.map +0 -1
- package/dist/commands/inspect/context-cost.d.mts +0 -1
- package/dist/commands/inspect/context-cost.mjs +0 -25
- package/dist/commands/inspect/context-cost.mjs.map +0 -1
- package/dist/commands/inspect/design-prep.d.mts +0 -1
- package/dist/commands/inspect/design-prep.mjs +0 -22
- package/dist/commands/inspect/design-prep.mjs.map +0 -1
- package/dist/commands/inspect/error-report.d.mts +0 -1
- package/dist/commands/inspect/error-report.mjs +0 -25
- package/dist/commands/inspect/error-report.mjs.map +0 -1
- package/dist/commands/inspect/error-zip.d.mts +0 -1
- package/dist/commands/inspect/error-zip.mjs +0 -24
- package/dist/commands/inspect/error-zip.mjs.map +0 -1
- package/dist/commands/inspect/log-report.d.mts +0 -1
- package/dist/commands/inspect/log-report.mjs +0 -26
- package/dist/commands/inspect/log-report.mjs.map +0 -1
- package/dist/commands/inspect/model-io.d.mts +0 -1
- package/dist/commands/inspect/model-io.mjs +0 -25
- package/dist/commands/inspect/model-io.mjs.map +0 -1
- package/dist/commands/inspect/profile-show.d.mts +0 -1
- package/dist/commands/inspect/profile-show.mjs +0 -28
- package/dist/commands/inspect/profile-show.mjs.map +0 -1
- package/dist/commands/inspect/recap.d.mts +0 -1
- package/dist/commands/inspect/recap.mjs +0 -30
- package/dist/commands/inspect/recap.mjs.map +0 -1
- package/dist/commands/inspect/resolve-task-key.d.mts +0 -1
- package/dist/commands/inspect/resolve-task-key.mjs +0 -24
- package/dist/commands/inspect/resolve-task-key.mjs.map +0 -1
- package/dist/commands/inspect/rollup.d.mts +0 -1
- package/dist/commands/inspect/rollup.mjs +0 -25
- package/dist/commands/inspect/rollup.mjs.map +0 -1
- package/dist/commands/inspect/run-audit.d.mts +0 -1
- package/dist/commands/inspect/run-audit.mjs +0 -25
- package/dist/commands/inspect/run-audit.mjs.map +0 -1
- package/dist/commands/inspect/set-work-status.d.mts +0 -1
- package/dist/commands/inspect/set-work-status.mjs +0 -29
- package/dist/commands/inspect/set-work-status.mjs.map +0 -1
- package/dist/commands/inspect/stage-map.d.mts +0 -1
- package/dist/commands/inspect/stage-map.mjs +0 -131
- package/dist/commands/inspect/stage-map.mjs.map +0 -1
- package/dist/commands/inspect/task-list.d.mts +0 -1
- package/dist/commands/inspect/task-list.mjs +0 -149
- package/dist/commands/inspect/task-list.mjs.map +0 -1
- package/dist/commands/inspect/task-show.d.mts +0 -1
- package/dist/commands/inspect/task-show.mjs +0 -108
- package/dist/commands/inspect/task-show.mjs.map +0 -1
- package/dist/commands/inspect/time-report.d.mts +0 -1
- package/dist/commands/inspect/time-report.mjs +0 -24
- package/dist/commands/inspect/time-report.mjs.map +0 -1
- package/dist/commands/inspect/usage-report.d.mts +0 -1
- package/dist/commands/inspect/usage-report.mjs +0 -23
- package/dist/commands/inspect/usage-report.mjs.map +0 -1
- package/dist/commands/inspect/user-response.d.mts +0 -1
- package/dist/commands/inspect/user-response.mjs +0 -35
- package/dist/commands/inspect/user-response.mjs.map +0 -1
- package/dist/commands/inspect/worker-liveness.d.mts +0 -1
- package/dist/commands/inspect/worker-liveness.mjs +0 -45
- package/dist/commands/inspect/worker-liveness.mjs.map +0 -1
- package/dist/commands/lifecycle/contract-check.d.mts +0 -1
- package/dist/commands/lifecycle/contract-check.mjs +0 -18
- package/dist/commands/lifecycle/contract-check.mjs.map +0 -1
- package/dist/commands/lifecycle/migrate.d.mts +0 -1
- package/dist/commands/lifecycle/migrate.mjs +0 -30
- package/dist/commands/lifecycle/migrate.mjs.map +0 -1
- package/dist/commands/lifecycle/model.d.mts +0 -1
- package/dist/commands/lifecycle/model.mjs +0 -22
- package/dist/commands/lifecycle/model.mjs.map +0 -1
- package/dist/commands/manager.d.mts +0 -3
- package/dist/commands/manager.mjs +0 -50
- package/dist/commands/manager.mjs.map +0 -1
- package/dist/commands/report/agent-activity.d.mts +0 -1
- package/dist/commands/report/agent-activity.mjs +0 -20
- package/dist/commands/report/agent-activity.mjs.map +0 -1
- package/dist/commands/report/approval-decision.d.mts +0 -1
- package/dist/commands/report/approval-decision.mjs +0 -21
- package/dist/commands/report/approval-decision.mjs.map +0 -1
- package/dist/commands/report/design-snapshot.d.mts +0 -1
- package/dist/commands/report/design-snapshot.mjs +0 -19
- package/dist/commands/report/design-snapshot.mjs.map +0 -1
- package/dist/commands/report/finalize.d.mts +0 -4
- package/dist/commands/report/finalize.mjs +0 -64
- package/dist/commands/report/finalize.mjs.map +0 -1
- package/dist/commands/report/inject-report-index.d.mts +0 -1
- package/dist/commands/report/inject-report-index.mjs +0 -21
- package/dist/commands/report/inject-report-index.mjs.map +0 -1
- package/dist/commands/report/render-final-report.d.mts +0 -1
- package/dist/commands/report/render-final-report.mjs +0 -23
- package/dist/commands/report/render-final-report.mjs.map +0 -1
- package/dist/commands/report/render-views.d.mts +0 -1
- package/dist/commands/report/render-views.mjs +0 -27
- package/dist/commands/report/render-views.mjs.map +0 -1
- package/dist/commands/report/translate.d.mts +0 -1
- package/dist/commands/report/translate.mjs +0 -33
- package/dist/commands/report/translate.mjs.map +0 -1
- package/runtime/bin/lib/okstra/tmux-pane.sh +0 -40
- package/runtime/bin/lib/okstra-ctl/cmd-batch.sh +0 -59
- package/runtime/bin/lib/okstra-ctl/cmd-list.sh +0 -35
- package/runtime/bin/lib/okstra-ctl/cmd-open.sh +0 -36
- package/runtime/bin/lib/okstra-ctl/cmd-projects.sh +0 -26
- package/runtime/bin/lib/okstra-ctl/cmd-reconcile.sh +0 -29
- package/runtime/bin/lib/okstra-ctl/cmd-reindex.sh +0 -38
- package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +0 -345
- package/runtime/bin/lib/okstra-ctl/cmd-show.sh +0 -27
- package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +0 -92
- package/runtime/bin/lib/okstra-ctl/main.sh +0 -41
- package/runtime/bin/lib/okstra-ctl/prepare.sh +0 -31
- package/runtime/bin/lib/okstra-ctl/usage.sh +0 -23
- package/runtime/bin/okstra-central.sh +0 -152
- package/runtime/bin/okstra-incremental-carry.py +0 -10
- package/runtime/bin/okstra-incremental-scope.py +0 -10
- package/runtime/bin/okstra-team-reconcile.sh +0 -36
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +0 -1156
- package/runtime/python/okstra_ctl/batch.py +0 -60
- package/runtime/python/okstra_ctl/clarification_items.py +0 -1050
- package/runtime/python/okstra_ctl/container_registry.py +0 -72
- package/runtime/python/okstra_ctl/improvement_assignment.py +0 -61
- package/runtime/python/okstra_ctl/resolver.py +0 -54
- package/runtime/python/okstra_ctl/team_reconcile.py +0 -275
- package/runtime/python/okstra_ctl/tmux.py +0 -134
- package/runtime/python/okstra_ctl/worktree.py +0 -1099
|
@@ -5,7 +5,6 @@ This function replaces the ~50-step wiring that previously lived in bash
|
|
|
5
5
|
`okstra.sh`. It is called by:
|
|
6
6
|
- `okstra.sh` (thin wrapper: argv → prepare_task_bundle → optional exec claude)
|
|
7
7
|
- `okstra-run` skill (collects inputs via AskUserQuestion → calls this)
|
|
8
|
-
- okstra-ctl rerun (passes recorded invocation argv through)
|
|
9
8
|
|
|
10
9
|
The function is read-modify-write on disk inside per-task mutex; it does not
|
|
11
10
|
mutate the calling process environment or rely on inherited env values for
|
|
@@ -16,7 +15,13 @@ state passing, and are read once at the start.
|
|
|
16
15
|
from __future__ import annotations
|
|
17
16
|
|
|
18
17
|
import hashlib
|
|
19
|
-
from argparse import
|
|
18
|
+
from argparse import (
|
|
19
|
+
Action,
|
|
20
|
+
ArgumentError,
|
|
21
|
+
ArgumentParser,
|
|
22
|
+
Namespace,
|
|
23
|
+
RawDescriptionHelpFormatter,
|
|
24
|
+
)
|
|
20
25
|
import json
|
|
21
26
|
import os
|
|
22
27
|
import re
|
|
@@ -30,7 +35,7 @@ from pathlib import Path
|
|
|
30
35
|
from typing import Mapping, Sequence
|
|
31
36
|
|
|
32
37
|
from okstra_project import project_json_path, upsert_project_json
|
|
33
|
-
from okstra_project.
|
|
38
|
+
from okstra_project.slug import slugify
|
|
34
39
|
from . import fix_cycles
|
|
35
40
|
from .analysis_packet import build_analysis_packet
|
|
36
41
|
from .stage_ledger import (
|
|
@@ -38,6 +43,7 @@ from .stage_ledger import (
|
|
|
38
43
|
render_stage_ledger,
|
|
39
44
|
stage_ledger_notice,
|
|
40
45
|
)
|
|
46
|
+
from .prior_planning import build_prior_planning_summary
|
|
41
47
|
from .analysis_inputs import (
|
|
42
48
|
ANALYSIS_TASK_TYPES,
|
|
43
49
|
AnalysisInputError,
|
|
@@ -134,7 +140,7 @@ from .schema_excerpt import build_schema_excerpt
|
|
|
134
140
|
from .registry.host_registry import default_host_registry
|
|
135
141
|
from .registry.provider_registry import default_provider_registry
|
|
136
142
|
from .role_requirements import RoleProfile, RoleProfileError, load_role_profile
|
|
137
|
-
from .
|
|
143
|
+
from .agent.invocation import (
|
|
138
144
|
AgentInstruction,
|
|
139
145
|
AgentInstructionSource,
|
|
140
146
|
AgentInvocationRequest,
|
|
@@ -170,7 +176,9 @@ from .render import (
|
|
|
170
176
|
)
|
|
171
177
|
from okstra_project.dirs import okstra_home
|
|
172
178
|
|
|
179
|
+
from . import cmux
|
|
173
180
|
from .dispatch_state import (
|
|
181
|
+
BACKEND_CLI_WRAPPER,
|
|
174
182
|
BACKEND_CMUX_PANE,
|
|
175
183
|
detect_terminal_backend,
|
|
176
184
|
generate_claude_session_id,
|
|
@@ -192,6 +200,7 @@ from .session import (
|
|
|
192
200
|
write_claude_resume_command_file,
|
|
193
201
|
)
|
|
194
202
|
from .pr_template import PrTemplateError, resolve_pr_template_path
|
|
203
|
+
from .worker_artifacts import worker_provider_id
|
|
195
204
|
from .workers import (
|
|
196
205
|
normalize_workers,
|
|
197
206
|
resolve_optional_workers,
|
|
@@ -386,20 +395,33 @@ def _validate_approved_plan_conformance(path: Path) -> None:
|
|
|
386
395
|
bad = malformed_conformance_stages(data)
|
|
387
396
|
if not bad:
|
|
388
397
|
return
|
|
398
|
+
planning = data.get("implementationPlanning") if isinstance(data, dict) else None
|
|
399
|
+
declared_by_stage = {
|
|
400
|
+
stage.get("stage"): stage.get("conformanceTests")
|
|
401
|
+
for stage in (planning or {}).get("stages") or []
|
|
402
|
+
if isinstance(stage, dict)
|
|
403
|
+
}
|
|
389
404
|
# 한 stage 씩 고치고 다시 막히는 왕복을 피하려면 전부 나열해야 한다 —
|
|
390
405
|
# 실패한 런에서 검증기는 stage 2 만 지목했지만 실제로는 2~7 전부였다.
|
|
406
|
+
# 기록값을 병기한다: 무엇과 다른지 말하지 않는 실패는 실행이 스스로
|
|
407
|
+
# 고칠 수 없다 (validate_selected_direction_ref 의 snapshotPath 선례).
|
|
391
408
|
stages = ", ".join(str(number) for number in bad)
|
|
409
|
+
got_lines = "".join(
|
|
410
|
+
f" stage {number}: got {declared_by_stage.get(number)!r}\n"
|
|
411
|
+
for number in bad
|
|
412
|
+
)
|
|
392
413
|
raise PrepareError(
|
|
393
414
|
f"approved plan data.json has malformed conformanceTests for stage(s) "
|
|
394
415
|
f"{stages}: {data_path}\n"
|
|
395
416
|
" each declaring stage must read "
|
|
396
417
|
"`<task_root>/qa/scripts/stage-<N>.<ext> "
|
|
397
|
-
"(requires=[db|io|http|external,...])`
|
|
398
|
-
"`Conformance tests: stage-<N> — `
|
|
399
|
-
"(prompts/profiles/implementation-planning.md)
|
|
418
|
+
"(requires=[db|io|http|external,...])` — the data.json value carries "
|
|
419
|
+
"only the remainder after the `Conformance tests: stage-<N> — ` "
|
|
420
|
+
"prefix (prompts/profiles/implementation-planning.md) — or carry "
|
|
400
421
|
"`Conformance exemption: <reason>` instead.\n"
|
|
401
|
-
"
|
|
402
|
-
"
|
|
422
|
+
f"{got_lines}"
|
|
423
|
+
" correct the stage's `conformanceTests` value in the data.json to "
|
|
424
|
+
"the expected form, then approve it."
|
|
403
425
|
)
|
|
404
426
|
|
|
405
427
|
|
|
@@ -544,6 +566,9 @@ class PrepareInputs:
|
|
|
544
566
|
# project.json → global config → 스킬 디폴트 순으로 해석된다.
|
|
545
567
|
pr_template_path: str = ""
|
|
546
568
|
render_only: bool = False
|
|
569
|
+
# 워커가 뜰 표면을 호출자가 명시할 때만 채운다. 빈 값이면 prepare 가 직전
|
|
570
|
+
# run 의 값을 물려받거나 새로 탐지한다(`_resolve_terminal_backend`).
|
|
571
|
+
terminal_backend: str = ""
|
|
547
572
|
approve_plan_ack: bool = False
|
|
548
573
|
# implementation 전용: 유저가 implementation-planning final-report 에서 고른
|
|
549
574
|
# Option Candidate 이름. `--implementation-option <name>` 으로 전달되어
|
|
@@ -607,16 +632,28 @@ def _validate_approved_plan(path: str) -> None:
|
|
|
607
632
|
raise PrepareError("\n".join(lines))
|
|
608
633
|
|
|
609
634
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
635
|
+
def _stage_validator_path() -> Path:
|
|
636
|
+
"""설치본·체크아웃·빌드 런타임 중 validators 를 실제로 가진 루트에서 해소한다.
|
|
637
|
+
|
|
638
|
+
부모 개수를 세는 계산은 세 배치 중 하나에서만 맞는다 — 설치본은 패키지가
|
|
639
|
+
`~/.okstra/lib/python/` 이라 validators 가 `~/.okstra/lib/` 아래에 있고,
|
|
640
|
+
체크아웃은 `<repo>/validators` 다.
|
|
641
|
+
"""
|
|
642
|
+
from .paths import STAGE_VALIDATOR_RELATIVE, find_asset_root
|
|
643
|
+
|
|
644
|
+
root = find_asset_root(STAGE_VALIDATOR_RELATIVE)
|
|
645
|
+
if root is None:
|
|
646
|
+
raise PrepareError(
|
|
647
|
+
"could not locate validate-implementation-plan-stages.py."
|
|
648
|
+
f"{_INSTALL_HINT}"
|
|
649
|
+
)
|
|
650
|
+
return root.joinpath(*STAGE_VALIDATOR_RELATIVE)
|
|
614
651
|
|
|
615
652
|
|
|
616
653
|
def _validate_stage_structure(plan_path: str) -> None:
|
|
617
654
|
"""Run validators/validate-implementation-plan-stages.py against the approved plan."""
|
|
618
655
|
r = _subprocess.run(
|
|
619
|
-
[sys.executable, str(
|
|
656
|
+
[sys.executable, str(_stage_validator_path()), "--plan", plan_path],
|
|
620
657
|
capture_output=True, text=True,
|
|
621
658
|
)
|
|
622
659
|
if r.returncode != 0:
|
|
@@ -684,6 +721,221 @@ def _ensure_task_directories(ctx: dict) -> None:
|
|
|
684
721
|
Path(ctx[key]).mkdir(parents=True, exist_ok=True)
|
|
685
722
|
|
|
686
723
|
|
|
724
|
+
_ARCHIVE_DIR_NAME = "archive"
|
|
725
|
+
|
|
726
|
+
# 재실행 잔재를 걷어낼 디렉터리. `(디렉터리 이름, ctx 경로 키, 매니페스트 카테고리)`.
|
|
727
|
+
# **이전 seq 파일을 읽는 소비자가 한 곳도 없는 디렉터리만** 넣는다. 조사 결과는
|
|
728
|
+
# `_archive_prior_run_seqs` docstring 에 있다.
|
|
729
|
+
_ROTATED_RUN_DIRS: tuple[tuple[str, str, str], ...] = (
|
|
730
|
+
("worker-results", "WORKER_RESULTS_PATH", "workerResults"),
|
|
731
|
+
("prompts", "RUN_PROMPTS_DIR", "prompts"),
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
_ARCHIVE_README_NAME = "README.md"
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _seq_of_entry(name: str, task_type_segment: str) -> str:
|
|
738
|
+
"""`*-<task-type>-NNN[.<ext>]` 항목의 seq. 아니면 빈 문자열.
|
|
739
|
+
|
|
740
|
+
`paths.next_run_seq` 가 카운트하는 것과 같은 모양을 고른다 — 파일과 확장자
|
|
741
|
+
없는 디렉터리(`duty-contracts-<task-type>-NNN/`) 둘 다. 이 규칙에 안 걸리는
|
|
742
|
+
항목(`.agent-invocations/`, 잠금 파일)은 seq 소유가 아니므로 건드리지 않는다.
|
|
743
|
+
"""
|
|
744
|
+
match = re.search(
|
|
745
|
+
rf"-{re.escape(task_type_segment)}-(\d{{3,}})(?:\.[^.]+)?$", name
|
|
746
|
+
)
|
|
747
|
+
return match.group(1) if match else ""
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _published_run_seqs(run_dir: Path, task_type_segment: str) -> dict[str, str]:
|
|
751
|
+
"""최근 published 리포트를 낸 런이 카테고리별로 기록한 seq.
|
|
752
|
+
|
|
753
|
+
reports/ 는 회전 대상이 아니라 언제나 전부 남아 있고, 그 리포트를 연
|
|
754
|
+
매니페스트의 `runSequencesByCategory` 가 같은 런의 나머지 카테고리 seq 를
|
|
755
|
+
묶는 유일한 기록이다. 재실행이 카테고리별로 다른 속도로 올라가므로
|
|
756
|
+
(`paths.next_run_seq` 가 디렉터리마다 따로 스캔한다) 이 대응 없이는 "리포트
|
|
757
|
+
017 의 worker-results 는 몇 번인가" 에 답할 방법이 없다.
|
|
758
|
+
"""
|
|
759
|
+
reports = run_dir / "reports"
|
|
760
|
+
manifests = run_dir / "manifests"
|
|
761
|
+
if not reports.is_dir() or not manifests.is_dir():
|
|
762
|
+
return {}
|
|
763
|
+
# 리포트 파일명은 `-NNN.data.json` 처럼 확장자가 두 마디라 `_seq_of_entry`
|
|
764
|
+
# 의 한 마디 규칙에 안 걸린다. 그 규칙은 seq 카운터(`paths.next_run_seq`)와
|
|
765
|
+
# 맞추려고 있는 것이고, 여기서 필요한 건 리포트 이름 규칙이다.
|
|
766
|
+
report_seq_re = re.compile(
|
|
767
|
+
rf"^final-report-{re.escape(task_type_segment)}-(\d{{3,}})"
|
|
768
|
+
r"(?:\.data)?\.(?:md|json)$"
|
|
769
|
+
)
|
|
770
|
+
seqs = sorted(
|
|
771
|
+
{
|
|
772
|
+
match.group(1) for match in (
|
|
773
|
+
report_seq_re.match(path.name) for path in reports.iterdir()
|
|
774
|
+
) if match
|
|
775
|
+
},
|
|
776
|
+
key=int,
|
|
777
|
+
)
|
|
778
|
+
if not seqs:
|
|
779
|
+
return {}
|
|
780
|
+
published = seqs[-1]
|
|
781
|
+
for path in manifests.glob(f"run-manifest-{task_type_segment}-*.json"):
|
|
782
|
+
try:
|
|
783
|
+
payload = load_owned_object(path, artifact="run manifest")
|
|
784
|
+
except (OSError, JsonBoundaryError) as exc:
|
|
785
|
+
# 못 읽은 매니페스트는 회전 판단의 근거를 줄이므로 알린다. prepare 를
|
|
786
|
+
# 세우지는 않는다 — 하한을 못 풀면 아무것도 안 옮기는 쪽으로 떨어진다.
|
|
787
|
+
print(
|
|
788
|
+
f"okstra: cannot read run manifest for archive floor: {path} ({exc})",
|
|
789
|
+
file=sys.stderr,
|
|
790
|
+
)
|
|
791
|
+
continue
|
|
792
|
+
categories = payload.get("runSequencesByCategory")
|
|
793
|
+
if not isinstance(categories, dict):
|
|
794
|
+
continue
|
|
795
|
+
if str(categories.get("reports") or "") != published:
|
|
796
|
+
continue
|
|
797
|
+
return {
|
|
798
|
+
key: str(value)
|
|
799
|
+
for key, value in categories.items()
|
|
800
|
+
if isinstance(value, str) and value
|
|
801
|
+
}
|
|
802
|
+
return {}
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _archive_prior_run_seqs(ctx: dict) -> None:
|
|
806
|
+
"""이 run dir 에 쌓인 이전 seq 산출물을 `archive/run-<seq>/` 로 옮긴다.
|
|
807
|
+
|
|
808
|
+
같은 `runs/<task-type>/` 에 24회 재실행한 task 에서 worker-results 에만
|
|
809
|
+
200개 넘는 파일이 쌓였고, 그 디렉터리를 글롭하는 검사들의 실패 건수가
|
|
810
|
+
재실행을 거치며 30 → 140 으로 늘었다. 재실행 자체는 정상 동작이므로 잔재를
|
|
811
|
+
치우는 자리는 prepare 다.
|
|
812
|
+
|
|
813
|
+
**회전 대상은 두 디렉터리뿐이다.** 소비자 전수 조사에서 이전 seq 파일을
|
|
814
|
+
읽는 소비자가 하나도 없는 곳만 골랐다:
|
|
815
|
+
|
|
816
|
+
- `worker-results/` — 읽는 쪽은 `run_audit._current_seq_results` 와
|
|
817
|
+
`context_cost._current_seq_worker_results` 둘인데 **둘 다 파일명의 최대
|
|
818
|
+
seq 로 스코핑**하며, 각각 이전 seq 혼입으로 생긴 실제 오측정 사례를
|
|
819
|
+
주석에 달고 있다.
|
|
820
|
+
- `prompts/` — 이전 seq 프롬프트를 경로로 여는 소비자가 없다.
|
|
821
|
+
`log_report` 는 `runs`/`prompts` 를 포함하는 경로를 rglob 하므로
|
|
822
|
+
`archive/run-NNN/prompts/` 도 계속 집계한다.
|
|
823
|
+
|
|
824
|
+
**`state/` 와 `logs/` 는 남긴다.** 둘 다 이전 seq 를 실제로 읽는다:
|
|
825
|
+
|
|
826
|
+
- `state/` — `okstra plan-items seed --prior-state` 가 이전 런의
|
|
827
|
+
`plan-body-verification-<task-type>-<seq>.json` 을, `prior_planning` 이
|
|
828
|
+
이전 런의 `convergence-<task-type>-<seq>.json` 을, `time-report` 가
|
|
829
|
+
타임라인이 기록한 모든 런의 `team-state-*.json` 을 연다. published 리포트를
|
|
830
|
+
상대로 하는 재실행은 그 리포트의 state seq 가 읽혀야 성립한다.
|
|
831
|
+
- `logs/` — `error_log_core` 가 `runs/*/logs/errors-*.jsonl` 을 한 단계
|
|
832
|
+
글롭으로만 훑는다. `archive/run-NNN/logs/` 는 그 패턴에 안 걸려서, 옮기면
|
|
833
|
+
`error-report`/`error-zip`/prepare 의 이전 런 오류 요약이 조용히 비게 된다.
|
|
834
|
+
|
|
835
|
+
보존 하한(floor)은 최근 published 리포트가 그 카테고리에 기록한 seq다. 그
|
|
836
|
+
seq 와 그 위는 전부 남고, 아래만 옮긴다. 매니페스트로 그 대응을 못 풀면
|
|
837
|
+
**아무것도 안 옮긴다** — 디렉터리의 최대 seq 를 하한으로 대신 쓰면 "한 seq 만
|
|
838
|
+
남기고 전부" 가 되어 보수적 기본값이 아니다. 하한이 언제나 최대 이전 seq
|
|
839
|
+
이하이므로 `paths.next_run_seq`(디렉터리의 **최대** seq 만 읽는다)가 보는
|
|
840
|
+
값은 변하지 않고, seq 배정은 단조성을 유지한다.
|
|
841
|
+
|
|
842
|
+
옮기기지 지우기가 아니다. 목적지가 이미 있으면 건너뛴다.
|
|
843
|
+
"""
|
|
844
|
+
run_dir = Path(ctx["RUN_DIR"])
|
|
845
|
+
segment = ctx["TASK_TYPE_SEGMENT"]
|
|
846
|
+
archive_root = run_dir / _ARCHIVE_DIR_NAME
|
|
847
|
+
published = _published_run_seqs(run_dir, segment)
|
|
848
|
+
moved: list[tuple[str, str, str]] = []
|
|
849
|
+
for dir_name, ctx_key, category in _ROTATED_RUN_DIRS:
|
|
850
|
+
directory = Path(ctx[ctx_key])
|
|
851
|
+
if not directory.is_dir():
|
|
852
|
+
continue
|
|
853
|
+
by_seq: dict[str, list[Path]] = {}
|
|
854
|
+
for entry in directory.iterdir():
|
|
855
|
+
seq = _seq_of_entry(entry.name, segment)
|
|
856
|
+
if seq:
|
|
857
|
+
by_seq.setdefault(seq, []).append(entry)
|
|
858
|
+
floor = published.get(category, "")
|
|
859
|
+
if not by_seq or not floor:
|
|
860
|
+
# 하한을 못 정하면 아무것도 안 옮긴다. 디렉터리의 최대 seq 를 하한으로
|
|
861
|
+
# 대신 쓰면 "한 seq 만 남기고 전부" 가 되어 보수적 기본값이 아니다.
|
|
862
|
+
continue
|
|
863
|
+
for seq in sorted(by_seq, key=int):
|
|
864
|
+
if int(seq) >= int(floor):
|
|
865
|
+
continue
|
|
866
|
+
destination = archive_root / f"run-{seq}" / dir_name
|
|
867
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
868
|
+
for entry in by_seq[seq]:
|
|
869
|
+
target = destination / entry.name
|
|
870
|
+
if target.exists():
|
|
871
|
+
continue
|
|
872
|
+
shutil.move(str(entry), str(target))
|
|
873
|
+
moved.append((seq, dir_name, entry.name))
|
|
874
|
+
if moved:
|
|
875
|
+
_write_archive_readme(archive_root, segment, published, moved)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _write_archive_readme(
|
|
879
|
+
archive_root: Path,
|
|
880
|
+
task_type_segment: str,
|
|
881
|
+
published: dict[str, str],
|
|
882
|
+
moved: list[tuple[str, str, str]],
|
|
883
|
+
) -> None:
|
|
884
|
+
"""archive/ 가 자기를 설명하게 한다.
|
|
885
|
+
|
|
886
|
+
이 디렉터리를 처음 보는 사람에게는 산출물이 사라진 것으로 보인다. 무엇이
|
|
887
|
+
어느 seq 에서 왜 옮겨졌고 되돌리는 방법이 무엇인지를 디렉터리 안에 적어,
|
|
888
|
+
이 대화 밖에서도 답이 나오게 한다.
|
|
889
|
+
"""
|
|
890
|
+
counts: dict[tuple[str, str], int] = {}
|
|
891
|
+
for seq, dir_name, _name in moved:
|
|
892
|
+
counts[(seq, dir_name)] = counts.get((seq, dir_name), 0) + 1
|
|
893
|
+
rows = "\n".join(
|
|
894
|
+
f"| {seq} | `{dir_name}/` | {count} | `run-{seq}/{dir_name}/` |"
|
|
895
|
+
for (seq, dir_name), count in sorted(counts.items())
|
|
896
|
+
)
|
|
897
|
+
floors = ", ".join(
|
|
898
|
+
f"`{category}`={seq}" for category, seq in sorted(published.items())
|
|
899
|
+
) or "해소 불가 (매니페스트에 published 리포트의 seq 대응이 없음)"
|
|
900
|
+
(archive_root / _ARCHIVE_README_NAME).write_text(
|
|
901
|
+
f"""# 재실행 잔재 보관소 — `runs/{task_type_segment}/`
|
|
902
|
+
|
|
903
|
+
`okstra` 는 같은 `runs/{task_type_segment}/` 에 재실행할 때마다 새 seq 를
|
|
904
|
+
배정한다. 이전 seq 산출물이 그대로 쌓이면 그 디렉터리를 글롭하는 검사들이 다른
|
|
905
|
+
런의 파일까지 이 런의 것으로 세어, 실패 건수가 재실행마다 늘어난다. prepare 가
|
|
906
|
+
그 잔재를 여기로 **옮긴다(지우지 않는다)**.
|
|
907
|
+
|
|
908
|
+
## 이번에 옮긴 것
|
|
909
|
+
|
|
910
|
+
| seq | 원래 디렉터리 | 항목 수 | 보관 위치 |
|
|
911
|
+
|---|---|---|---|
|
|
912
|
+
{rows}
|
|
913
|
+
|
|
914
|
+
보존 하한: 최근 published 리포트가 기록한 seq — {floors}. 이 seq 와 그 위는
|
|
915
|
+
원래 자리에 그대로 있다.
|
|
916
|
+
|
|
917
|
+
## 옮기지 않는 것
|
|
918
|
+
|
|
919
|
+
`reports/`, `manifests/`, `state/`, `status/`, `sessions/`, `logs/`, `carry/`,
|
|
920
|
+
`user-responses/` 는 건드리지 않는다. 이전 seq 를 실제로 읽는 소비자가 있다 —
|
|
921
|
+
`state/` 는 `okstra plan-items seed --prior-state` 와 `prior_planning`,
|
|
922
|
+
`logs/` 는 `error-report` / `error-zip` 의 `runs/*/logs/errors-*.jsonl` 글롭.
|
|
923
|
+
|
|
924
|
+
## 되돌리려면
|
|
925
|
+
|
|
926
|
+
원래 자리로 되돌린다. 예:
|
|
927
|
+
|
|
928
|
+
```sh
|
|
929
|
+
mv run-<seq>/worker-results/* ../worker-results/
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
되돌린 뒤 그 seq 가 디렉터리의 최대 seq 가 되지 않는지 확인한다 — seq 배정
|
|
933
|
+
(`okstra_ctl.paths.next_run_seq`)은 디렉터리의 최대 seq 만 읽는다.
|
|
934
|
+
""",
|
|
935
|
+
encoding="utf-8",
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
|
|
687
939
|
def _role_execution_refs_from_ctx(ctx: dict) -> list[str]:
|
|
688
940
|
try:
|
|
689
941
|
payload = json.loads(ctx.get("EXECUTION_IDENTITY_JSON") or "{}")
|
|
@@ -705,9 +957,7 @@ def _record_start(
|
|
|
705
957
|
cwd: str,
|
|
706
958
|
brief_sha256: str,
|
|
707
959
|
) -> None:
|
|
708
|
-
"""record_start hook
|
|
709
|
-
이지만 python 직접 호출이라 환경 변수 의존 없음.
|
|
710
|
-
"""
|
|
960
|
+
"""record_start hook 호출 — 중앙 인덱스에 이 run 의 시작을 기록한다."""
|
|
711
961
|
from datetime import datetime, timezone
|
|
712
962
|
from okstra_ctl import record_start
|
|
713
963
|
from .locks import central_lock
|
|
@@ -715,7 +965,7 @@ def _record_start(
|
|
|
715
965
|
home = okstra_home()
|
|
716
966
|
home.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
717
967
|
os.chmod(home, 0o700)
|
|
718
|
-
#
|
|
968
|
+
# 중앙 홈 bootstrap — 디렉터리·jsonl 파일 보장.
|
|
719
969
|
for sub in ("archive", ".locks", "batches", "projects"):
|
|
720
970
|
(home / sub).mkdir(exist_ok=True)
|
|
721
971
|
for f in ("active.jsonl", "recent.jsonl"):
|
|
@@ -780,7 +1030,7 @@ def _remove_leaked_reservation(ctx: dict, run_seq: int) -> None:
|
|
|
780
1030
|
)
|
|
781
1031
|
except Exception as exc: # noqa: BLE001 — cleanup 실패는 비치명적
|
|
782
1032
|
print(
|
|
783
|
-
f"
|
|
1033
|
+
f"central index: reservation cleanup failed after record_start error ({exc})",
|
|
784
1034
|
file=sys.stderr,
|
|
785
1035
|
)
|
|
786
1036
|
|
|
@@ -819,6 +1069,31 @@ def _record_artifact_runtime_render_only_event(inp: PrepareInputs, ctx: dict) ->
|
|
|
819
1069
|
)
|
|
820
1070
|
|
|
821
1071
|
|
|
1072
|
+
def _workers_flag_value(roster_csv: str) -> str:
|
|
1073
|
+
"""로스터 CSV 를 `--workers` 가 받는 어휘(provider id)로 되돌린다.
|
|
1074
|
+
|
|
1075
|
+
두 어휘가 다르다. 로스터 행은 슬롯 id(`codex-verifier`)이고 `--workers` 는
|
|
1076
|
+
프로파일의 `Required workers:` 가 선언한 **provider 허용목록**이다
|
|
1077
|
+
([`workers.py`](workers.py)). 로스터를 그대로 실으면 재실행 argv 가
|
|
1078
|
+
`unknown workers: codex-verifier` 로 죽는다 — 이 인자를 만드는 곳은 여기
|
|
1079
|
+
하나뿐이므로 변환도 여기서 한다.
|
|
1080
|
+
|
|
1081
|
+
같은 provider 의 슬롯 둘은 한 항목으로 접힌다. 슬롯 수는 이 인자가 아니라
|
|
1082
|
+
함께 실리는 `--role-count` · `--role-model` 이 나른다.
|
|
1083
|
+
"""
|
|
1084
|
+
seen: set[str] = set()
|
|
1085
|
+
out: list[str] = []
|
|
1086
|
+
for token in (part.strip() for part in roster_csv.split(",")):
|
|
1087
|
+
if not token:
|
|
1088
|
+
continue
|
|
1089
|
+
provider = worker_provider_id(token)
|
|
1090
|
+
if provider in seen:
|
|
1091
|
+
continue
|
|
1092
|
+
seen.add(provider)
|
|
1093
|
+
out.append(provider)
|
|
1094
|
+
return ",".join(out)
|
|
1095
|
+
|
|
1096
|
+
|
|
822
1097
|
def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
823
1098
|
"""rerun 충실 재현을 위한 canonical argv 재구성."""
|
|
824
1099
|
has_role_selection = bool(inp.role_counts_raw or inp.role_models_raw)
|
|
@@ -827,7 +1102,7 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
827
1102
|
{} if inp.host_session_context.entry_mode == "current-session"
|
|
828
1103
|
else legacy_fallbacks
|
|
829
1104
|
)
|
|
830
|
-
workers = inp.workers_override or (
|
|
1105
|
+
workers = inp.workers_override or _workers_flag_value(
|
|
831
1106
|
legacy_fallbacks.get("RECOMMENDED_ANALYSERS", "")
|
|
832
1107
|
)
|
|
833
1108
|
pairs = [
|
|
@@ -900,6 +1175,9 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
900
1175
|
])
|
|
901
1176
|
if inp.render_only:
|
|
902
1177
|
argv.append("--render-only")
|
|
1178
|
+
# `--terminal-backend` 은 일부러 빠진다. 그것은 "이번엔 표면 없이 간다"는
|
|
1179
|
+
# 1회성 선언이지 task 의 성질이 아니다 — cmux 가 멀쩡한 기계에서의 rerun 은
|
|
1180
|
+
# 다시 pane 을 받아야 한다.
|
|
903
1181
|
if not inp.plan_verification_enabled:
|
|
904
1182
|
argv.append("--no-plan-verification")
|
|
905
1183
|
argv.append("--yes")
|
|
@@ -1005,8 +1283,23 @@ def _resolve_runtime_assets(workspace_root: Path, inp: PrepareInputs) -> _Resolv
|
|
|
1005
1283
|
workspace_root / "templates" / "reports" / "final-report-v2.template.md"
|
|
1006
1284
|
)
|
|
1007
1285
|
lead_contract = workspace_root / "prompts" / "lead" / "okstra-lead-contract.md"
|
|
1008
|
-
|
|
1009
|
-
|
|
1286
|
+
# validators 는 workspace_root 아래가 아니라 "실행 중인 파이썬 패키지와 짝이
|
|
1287
|
+
# 되는 루트" 에 있다: 설치본은 `~/.okstra/lib/validators`, 체크아웃은
|
|
1288
|
+
# `<repo>/validators`, 빌드 런타임은 `runtime/validators`. workspace_root 로
|
|
1289
|
+
# 계산하면 `~/.okstra/bin/okstra.sh` 진입점(workspace_root=`~/.okstra`)이
|
|
1290
|
+
# 신선한 설치에서도 "validate-run.py 없음" 으로 죽었다.
|
|
1291
|
+
from .paths import (
|
|
1292
|
+
BRIEF_VALIDATOR_RELATIVE,
|
|
1293
|
+
RUN_VALIDATOR_RELATIVE,
|
|
1294
|
+
find_asset_root,
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
run_validator = (
|
|
1298
|
+
find_asset_root(RUN_VALIDATOR_RELATIVE) or workspace_root
|
|
1299
|
+
).joinpath(*RUN_VALIDATOR_RELATIVE)
|
|
1300
|
+
brief_validator = (
|
|
1301
|
+
find_asset_root(BRIEF_VALIDATOR_RELATIVE) or workspace_root
|
|
1302
|
+
).joinpath(*BRIEF_VALIDATOR_RELATIVE)
|
|
1010
1303
|
for required in (
|
|
1011
1304
|
task_index_template,
|
|
1012
1305
|
final_report_template,
|
|
@@ -1596,14 +1889,16 @@ def _apply_qa_waiver_if_requested(inp: "PrepareInputs", project_root: Path) -> N
|
|
|
1596
1889
|
if not inp.qa_waiver:
|
|
1597
1890
|
return
|
|
1598
1891
|
from .conformance import apply_qa_waiver, parse_qa_waiver_arg
|
|
1599
|
-
from .paths import task_dir
|
|
1892
|
+
from .paths import task_conformance_manifest_file, task_dir
|
|
1600
1893
|
parsed = parse_qa_waiver_arg(inp.qa_waiver)
|
|
1601
1894
|
if parsed is None:
|
|
1602
1895
|
raise PrepareError(
|
|
1603
1896
|
f'--qa-waiver must be "<stageKey>:<reason>", got {inp.qa_waiver!r}'
|
|
1604
1897
|
)
|
|
1605
1898
|
stage_key, reason = parsed
|
|
1606
|
-
manifest_path =
|
|
1899
|
+
manifest_path = task_conformance_manifest_file(
|
|
1900
|
+
task_dir(project_root, inp.task_group, inp.task_id)
|
|
1901
|
+
)
|
|
1607
1902
|
with worktree_provision_mutex(
|
|
1608
1903
|
okstra_home(), inp.project_id, slugify(inp.task_group), slugify(inp.task_id),
|
|
1609
1904
|
):
|
|
@@ -1626,10 +1921,9 @@ def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: i
|
|
|
1626
1921
|
the user re-waived this exact stage for this run via `--qa-waiver` (already
|
|
1627
1922
|
applied upstream in `_apply_qa_waiver_if_requested`)."""
|
|
1628
1923
|
from .conformance import clear_qa_waiver, parse_qa_waiver_arg
|
|
1629
|
-
from .paths import task_dir
|
|
1630
|
-
manifest_path = (
|
|
1924
|
+
from .paths import task_conformance_manifest_file, task_dir
|
|
1925
|
+
manifest_path = task_conformance_manifest_file(
|
|
1631
1926
|
task_dir(project_root, inp.task_group, inp.task_id)
|
|
1632
|
-
/ "qa" / "conformance-manifest.json"
|
|
1633
1927
|
)
|
|
1634
1928
|
if not manifest_path.is_file():
|
|
1635
1929
|
return
|
|
@@ -1723,7 +2017,7 @@ def _normalize_prepare_model_selection(
|
|
|
1723
2017
|
profile_file: Path,
|
|
1724
2018
|
) -> tuple[CanonicalModelSelection, RoleProfile]:
|
|
1725
2019
|
try:
|
|
1726
|
-
_validate_critic_choice(inp.critic
|
|
2020
|
+
_validate_critic_choice(inp.critic)
|
|
1727
2021
|
profile = load_role_profile(profile_file)
|
|
1728
2022
|
profile_workers = resolve_profile_workers(profile_file)
|
|
1729
2023
|
has_worker_models = bool(
|
|
@@ -1805,7 +2099,9 @@ def _resolve_prepare_assignments(
|
|
|
1805
2099
|
execution_provider_ids=provider_ids_for_snapshot,
|
|
1806
2100
|
)
|
|
1807
2101
|
host = _effective_host_session_context(inp, lead_runtime)
|
|
1808
|
-
role_models = _materialize_selection_models(
|
|
2102
|
+
role_models = _materialize_selection_models(
|
|
2103
|
+
selection, context, host, inp.lead_provider
|
|
2104
|
+
)
|
|
1809
2105
|
role_counts = _resolver_role_counts(profile, selection.role_counts)
|
|
1810
2106
|
try:
|
|
1811
2107
|
plan = resolve_assignments(
|
|
@@ -2104,6 +2400,7 @@ def _materialize_selection_models(
|
|
|
2104
2400
|
selection: CanonicalModelSelection,
|
|
2105
2401
|
context: AssignmentContext,
|
|
2106
2402
|
host: HostSessionContext,
|
|
2403
|
+
requested_lead_provider: str = "",
|
|
2107
2404
|
) -> dict[str, tuple[str, ...]]:
|
|
2108
2405
|
role_models = dict(selection.role_models)
|
|
2109
2406
|
for role, providers in selection.provider_constraints.items():
|
|
@@ -2123,9 +2420,81 @@ def _materialize_selection_models(
|
|
|
2123
2420
|
raise PrepareError(str(exc)) from exc
|
|
2124
2421
|
resolved.append(str(model.model_ref))
|
|
2125
2422
|
role_models[role] = tuple(resolved)
|
|
2423
|
+
_apply_unselected_role_defaults(
|
|
2424
|
+
role_models, context, host, requested_lead_provider
|
|
2425
|
+
)
|
|
2126
2426
|
return role_models
|
|
2127
2427
|
|
|
2128
2428
|
|
|
2429
|
+
_LEGACY_ROLE_DEFAULT_ENV = {
|
|
2430
|
+
"leader": "OKSTRA_DEFAULT_LEAD_MODEL",
|
|
2431
|
+
"report-writer": "OKSTRA_DEFAULT_REPORT_WRITER_MODEL",
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
|
|
2435
|
+
def _apply_unselected_role_defaults(
|
|
2436
|
+
role_models: dict[str, tuple[str, ...]],
|
|
2437
|
+
context: AssignmentContext,
|
|
2438
|
+
host: HostSessionContext,
|
|
2439
|
+
requested_lead_provider: str,
|
|
2440
|
+
) -> None:
|
|
2441
|
+
"""아무도 고르지 않은 lead · report-writer 역할의 기본값을 채운다.
|
|
2442
|
+
|
|
2443
|
+
두 가지가 여기 모인다. **lead 의 provider 는 호스트가 정한다** —
|
|
2444
|
+
codex 호스트의 in-session lead 는 codex 여야 하고 다른 provider 요청은
|
|
2445
|
+
거부된다(`resolve_context_lead_provider`). 그리고
|
|
2446
|
+
`OKSTRA_DEFAULT_LEAD_MODEL` · `OKSTRA_DEFAULT_REPORT_WRITER_MODEL` 은
|
|
2447
|
+
문서화된 사용자 노브다([`usage.sh`](../lib/okstra/usage.sh)).
|
|
2448
|
+
|
|
2449
|
+
둘 다 레거시 바인딩 경로에만 있었다. 그래서 위저드로 연 런은 env 노브를
|
|
2450
|
+
이미 무시했고, 호스트 고정은 렌더 단계(`render.py` `_lead_assignment`)에서
|
|
2451
|
+
한 번 더 계산돼 두 값이 갈릴 수 있었다. 해소를 resolver 입력인
|
|
2452
|
+
`role_models` 로 옮겨 모든 경로가 같은 값을 본다.
|
|
2453
|
+
|
|
2454
|
+
명시 선택(`--role-model` · `--lead-model` · `--report-writer-model` 및 그
|
|
2455
|
+
변환 결과)이 이미 그 역할을 채웠으면 건드리지 않는다. env 는 기본값이지
|
|
2456
|
+
오버라이드가 아니다.
|
|
2457
|
+
"""
|
|
2458
|
+
# 호스트 적합성은 lead 를 누가 골랐든 prepare 에서 판정한다. 이 호출을
|
|
2459
|
+
# 조건부로 두면 `--lead-provider` 가 명시된 런만 검사를 건너뛰고, 그 불일치가
|
|
2460
|
+
# 렌더 단계에서 raw `HostCapabilityMismatch` 로 터진다 — prepare 가 이미
|
|
2461
|
+
# worktree 와 상태 파일을 만든 뒤다.
|
|
2462
|
+
try:
|
|
2463
|
+
host_lead_provider = resolve_context_lead_provider(
|
|
2464
|
+
context=context,
|
|
2465
|
+
requested_provider=requested_lead_provider,
|
|
2466
|
+
).provider
|
|
2467
|
+
except (
|
|
2468
|
+
HostCapabilityMismatch,
|
|
2469
|
+
HostNotRegistered,
|
|
2470
|
+
ProviderUnavailable,
|
|
2471
|
+
) as exc:
|
|
2472
|
+
raise PrepareError(str(exc)) from exc
|
|
2473
|
+
|
|
2474
|
+
for role, env_name in _LEGACY_ROLE_DEFAULT_ENV.items():
|
|
2475
|
+
if role in role_models:
|
|
2476
|
+
continue
|
|
2477
|
+
if role == "leader":
|
|
2478
|
+
# current-session lead 의 정체성은 호스트가 증언한 것뿐이다. 여기서
|
|
2479
|
+
# 기본값을 채우면 호스트가 증언하지 않은 모델을 합성하게 된다 —
|
|
2480
|
+
# 증언이 `unknown` 인 세션도 `unknown` 으로 남아야 한다.
|
|
2481
|
+
if host.entry_mode == "current-session":
|
|
2482
|
+
continue
|
|
2483
|
+
provider = host_lead_provider
|
|
2484
|
+
else:
|
|
2485
|
+
provider = _fallback_legacy_provider(role, context, host)
|
|
2486
|
+
raw_model = _legacy_claude_model_override(provider, env_name)
|
|
2487
|
+
if not raw_model and role != "leader":
|
|
2488
|
+
continue
|
|
2489
|
+
try:
|
|
2490
|
+
model = resolve_model_selection(context.pool, provider, role, raw_model)
|
|
2491
|
+
except (ValueError, RuntimeError) as exc:
|
|
2492
|
+
raise PrepareError(
|
|
2493
|
+
f"{env_name}: {exc}" if raw_model else str(exc)
|
|
2494
|
+
) from exc
|
|
2495
|
+
role_models[role] = (str(model.model_ref),)
|
|
2496
|
+
|
|
2497
|
+
|
|
2129
2498
|
def _fallback_legacy_provider(
|
|
2130
2499
|
role: str,
|
|
2131
2500
|
context: AssignmentContext,
|
|
@@ -2502,7 +2871,7 @@ def _resolve_model_bindings(
|
|
|
2502
2871
|
) -> _ModelBindings:
|
|
2503
2872
|
"""worker 모델 + critic 선택 + executor 바인딩을 한 묶음으로 해소·검증한다."""
|
|
2504
2873
|
m = _resolve_worker_models(inp, workers, context)
|
|
2505
|
-
critic_choice = _validate_critic_choice(inp.critic
|
|
2874
|
+
critic_choice = _validate_critic_choice(inp.critic)
|
|
2506
2875
|
critic_model_execution = ""
|
|
2507
2876
|
critic_meta = None
|
|
2508
2877
|
if critic_choice in provider_ids("critic"):
|
|
@@ -2547,7 +2916,6 @@ def _resolve_model_bindings(
|
|
|
2547
2916
|
resolved=translator_meta,
|
|
2548
2917
|
role="translator",
|
|
2549
2918
|
)
|
|
2550
|
-
_reject_split_worker_models(executor_assignment, worker_assignments)
|
|
2551
2919
|
invocation_assignments = _build_invocation_assignments(
|
|
2552
2920
|
lead_assignment,
|
|
2553
2921
|
worker_assignments,
|
|
@@ -2581,23 +2949,15 @@ def _resolve_model_bindings(
|
|
|
2581
2949
|
)
|
|
2582
2950
|
|
|
2583
2951
|
|
|
2584
|
-
|
|
2585
|
-
"
|
|
2586
|
-
"error-analysis",
|
|
2587
|
-
"implementation-planning",
|
|
2588
|
-
"final-verification",
|
|
2589
|
-
})
|
|
2590
|
-
|
|
2952
|
+
def _validate_critic_choice(raw_value: str) -> str:
|
|
2953
|
+
"""critic 은 어느 phase 에서도 opt-in 이다 — 프로필이 `min: 0` 으로 선언한다.
|
|
2591
2954
|
|
|
2592
|
-
|
|
2955
|
+
위저드에서 "추가 안 함" 을 고를 수 있는 역할을 이 경로에서만 거부하면 같은
|
|
2956
|
+
구성이 진입점마다 다르게 판정된다.
|
|
2957
|
+
"""
|
|
2593
2958
|
critic_choice = (raw_value or "").strip().lower()
|
|
2594
2959
|
allowed_critics = ["", *provider_ids("critic")]
|
|
2595
2960
|
if critic_choice == "off":
|
|
2596
|
-
if task_type in _CRITIC_REQUIRED_TASK_TYPES:
|
|
2597
|
-
raise PrepareError(
|
|
2598
|
-
"--critic off is not allowed; critic is required "
|
|
2599
|
-
f"for {task_type}"
|
|
2600
|
-
)
|
|
2601
2961
|
return "off"
|
|
2602
2962
|
if critic_choice not in allowed_critics:
|
|
2603
2963
|
raise PrepareError(
|
|
@@ -2632,17 +2992,14 @@ def _project_assignment_plan(
|
|
|
2632
2992
|
role="report-writer",
|
|
2633
2993
|
)
|
|
2634
2994
|
)
|
|
2635
|
-
|
|
2636
|
-
row for row in plan.assignments
|
|
2637
|
-
if row.role not in {"leader", "critic", "implementer", "report-writer"}
|
|
2638
|
-
)
|
|
2995
|
+
roster_rows = _worker_id_scope(plan)
|
|
2639
2996
|
worker_assignments = tuple(
|
|
2640
2997
|
_role_assignment(
|
|
2641
2998
|
resolved=row,
|
|
2642
2999
|
role=row.role,
|
|
2643
|
-
worker_id=_canonical_worker_id(row,
|
|
3000
|
+
worker_id=_canonical_worker_id(row, roster_rows),
|
|
2644
3001
|
)
|
|
2645
|
-
for row in
|
|
3002
|
+
for row in roster_rows
|
|
2646
3003
|
)
|
|
2647
3004
|
if report_rows:
|
|
2648
3005
|
worker_assignments = (
|
|
@@ -2664,15 +3021,57 @@ def _project_assignment_plan(
|
|
|
2664
3021
|
return [row.worker_id for row in worker_assignments], models
|
|
2665
3022
|
|
|
2666
3023
|
|
|
3024
|
+
def _analysis_worker_rows(
|
|
3025
|
+
plan: AssignmentPlan,
|
|
3026
|
+
) -> tuple[ResolvedAssignment, ...]:
|
|
3027
|
+
"""워커로 디스패치되는 분석 역할 배정 — 리드·critic·실행자·리포트라이터 제외."""
|
|
3028
|
+
return tuple(
|
|
3029
|
+
row for row in plan.assignments
|
|
3030
|
+
if row.role not in {"leader", "critic", "implementer", "report-writer"}
|
|
3031
|
+
)
|
|
3032
|
+
|
|
3033
|
+
|
|
3034
|
+
def _worker_id_scope(
|
|
3035
|
+
plan: AssignmentPlan,
|
|
3036
|
+
) -> tuple[ResolvedAssignment, ...]:
|
|
3037
|
+
"""worker_id 를 발급받는 배정 전체를 정본 순서로 모은다.
|
|
3038
|
+
|
|
3039
|
+
실행자를 앞에 세우는 것은 같은 (provider, 역할) 쌍이 겹칠 때 붙는 번호를
|
|
3040
|
+
결정적으로 만들기 위해서다 — `worker_prompt_policy` 는 `EXECUTOR_WORKER_ID`
|
|
3041
|
+
와 같은 id 를 실행자로 실체화하므로, 실행자 id 가 흔들리면 실행자 프롬프트가
|
|
3042
|
+
통째로 바뀐다.
|
|
3043
|
+
"""
|
|
3044
|
+
return (*plan.by_role("implementer"), *_analysis_worker_rows(plan))
|
|
3045
|
+
|
|
3046
|
+
|
|
2667
3047
|
def _canonical_worker_id(
|
|
2668
3048
|
assignment: ResolvedAssignment,
|
|
2669
3049
|
rows: Sequence[ResolvedAssignment],
|
|
2670
3050
|
) -> str:
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
3051
|
+
"""로스터 슬롯의 worker_id — 언제나 `<provider>-<역할>`.
|
|
3052
|
+
|
|
3053
|
+
역할을 조건부로 붙이면(한 provider 가 두 슬롯을 차지할 때만) 같은 역할이
|
|
3054
|
+
로스터 구성에 따라 다른 이름 모양을 갖는다: 같은 검증자가 어떤 런에서는
|
|
3055
|
+
`codex`, 어떤 런에서는 `codex-verifier` 다. 그러면 `--workers` 인자를 손으로
|
|
3056
|
+
조립할 수 없고 — 리드는 로스터를 먼저 읽어야만 슬롯을 지명할 수 있다.
|
|
3057
|
+
항상 붙이면 id 가 자기 역할을 말한다.
|
|
3058
|
+
|
|
3059
|
+
같은 (provider, 역할) 쌍이 또 있으면(모델이 다른 검증자 둘) 두 번째부터
|
|
3060
|
+
번호를 덧붙인다. `ordinal` 로는 셀 수 없다 — role 안에서만 매겨져
|
|
3061
|
+
([`assignment_resolver`](assignment_resolver.py)) implementer/claude 와
|
|
3062
|
+
verifier/claude 가 둘 다 1 이다. 그래서 `rows` 순서에서 위치로 센다.
|
|
3063
|
+
|
|
3064
|
+
고정 id 인 `report-writer` 는 이 함수를 거치지 않는다.
|
|
3065
|
+
"""
|
|
3066
|
+
provider = assignment.provider_id
|
|
3067
|
+
prior_same_role = 0
|
|
3068
|
+
for row in rows:
|
|
3069
|
+
if row is assignment:
|
|
3070
|
+
break
|
|
3071
|
+
if row.provider_id == provider and row.role == assignment.role:
|
|
3072
|
+
prior_same_role += 1
|
|
3073
|
+
slot = f"{provider}-{assignment.role}"
|
|
3074
|
+
return slot if prior_same_role == 0 else f"{slot}-{prior_same_role + 1}"
|
|
2676
3075
|
|
|
2677
3076
|
|
|
2678
3077
|
def _project_model_bindings(
|
|
@@ -2695,7 +3094,7 @@ def _project_model_bindings(
|
|
|
2695
3094
|
_role_assignment(
|
|
2696
3095
|
resolved=executor_rows[0],
|
|
2697
3096
|
role="executor",
|
|
2698
|
-
worker_id=executor_rows[0]
|
|
3097
|
+
worker_id=_canonical_worker_id(executor_rows[0], _worker_id_scope(plan)),
|
|
2699
3098
|
)
|
|
2700
3099
|
if executor_rows
|
|
2701
3100
|
else None
|
|
@@ -2711,8 +3110,6 @@ def _project_model_bindings(
|
|
|
2711
3110
|
provider: _legacy_projection_from_plan(provider, plan, context)
|
|
2712
3111
|
for provider in ("claude", "codex", "antigravity")
|
|
2713
3112
|
}
|
|
2714
|
-
_reject_executor_outside_roster(executor_assignment, workers)
|
|
2715
|
-
_reject_split_worker_models(executor_assignment, workers)
|
|
2716
3113
|
invocation_assignments = _build_invocation_assignments(
|
|
2717
3114
|
lead_assignment,
|
|
2718
3115
|
workers,
|
|
@@ -2799,64 +3196,6 @@ def _selected_execution_provider_ids(
|
|
|
2799
3196
|
return tuple(dict.fromkeys(selected))
|
|
2800
3197
|
|
|
2801
3198
|
|
|
2802
|
-
def _reject_executor_outside_roster(
|
|
2803
|
-
executor: RoleAssignment | None,
|
|
2804
|
-
workers: tuple[RoleAssignment, ...],
|
|
2805
|
-
) -> None:
|
|
2806
|
-
"""Refuse an executor whose provider the roster never dispatches.
|
|
2807
|
-
|
|
2808
|
-
An implementation run opens its executor with `--workers <provider>`, so a
|
|
2809
|
-
provider absent from the roster has no invocation to open. The legacy
|
|
2810
|
-
selection path says so outright; the canonical path derived its roster from
|
|
2811
|
-
role models alone, so `--executor <provider>` rendered fine and the run only
|
|
2812
|
-
failed later, at `requested worker(s) are not in this run roster`.
|
|
2813
|
-
"""
|
|
2814
|
-
if executor is None:
|
|
2815
|
-
return
|
|
2816
|
-
roster = {row.worker_id for row in workers if row.worker_id}
|
|
2817
|
-
if executor.worker_id in roster:
|
|
2818
|
-
return
|
|
2819
|
-
raise PrepareError(
|
|
2820
|
-
f"--executor {executor.worker_id} is not in this run's roster "
|
|
2821
|
-
f"({', '.join(sorted(roster)) or 'empty'}); the executor is dispatched "
|
|
2822
|
-
f"as a worker, so give it a roster slot — "
|
|
2823
|
-
f"--role-model verifier={executor.worker_id}/<model> — or pick an "
|
|
2824
|
-
f"executor already in the roster."
|
|
2825
|
-
)
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
def _reject_split_worker_models(
|
|
2829
|
-
executor: RoleAssignment | None,
|
|
2830
|
-
workers: tuple[RoleAssignment, ...],
|
|
2831
|
-
) -> None:
|
|
2832
|
-
"""Refuse one worker id standing for two roles on two different models.
|
|
2833
|
-
|
|
2834
|
-
The roster names a worker by provider, so an implementation run whose
|
|
2835
|
-
executor and verifier are the same provider shares that id — but
|
|
2836
|
-
`invocationAssignments["initial/<id>"]` can hold only one model, and it
|
|
2837
|
-
holds the verifier's. Dispatch then looks the executor's role execution up
|
|
2838
|
-
by that assignment, finds no row with a matching execution value, and stops
|
|
2839
|
-
at `v2 execution identity does not match a canonical role execution`. The
|
|
2840
|
-
render used to succeed and only the dispatch failed, by which point the run
|
|
2841
|
-
had already claimed its stage.
|
|
2842
|
-
"""
|
|
2843
|
-
if executor is None:
|
|
2844
|
-
return
|
|
2845
|
-
peer = next(
|
|
2846
|
-
(row for row in workers if row.worker_id == executor.worker_id),
|
|
2847
|
-
None,
|
|
2848
|
-
)
|
|
2849
|
-
if peer is None or peer.model_execution_value == executor.model_execution_value:
|
|
2850
|
-
return
|
|
2851
|
-
raise PrepareError(
|
|
2852
|
-
f"worker {executor.worker_id!r} is the executor on "
|
|
2853
|
-
f"{executor.model_execution_value!r} and a verifier on "
|
|
2854
|
-
f"{peer.model_execution_value!r}; one worker id carries one model. "
|
|
2855
|
-
f"Give both roles the same model, or pick a different --executor "
|
|
2856
|
-
f"provider."
|
|
2857
|
-
)
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
3199
|
def _resolve_executor_assignment(
|
|
2861
3200
|
inp: PrepareInputs,
|
|
2862
3201
|
workers: list[str],
|
|
@@ -3131,8 +3470,17 @@ def write_verification_target_snapshot(
|
|
|
3131
3470
|
normalized = normalized.rstrip() + "\n"
|
|
3132
3471
|
digest = "sha256:" + hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
|
3133
3472
|
path = instruction_set / "verification-target.md"
|
|
3473
|
+
# 계산 규칙을 값과 함께 적는다. 워커는 이 다이제스트를 프롬프트 앵커로도
|
|
3474
|
+
# 받는데, 규칙을 모르면 파일 전체 바이트로 재고 반드시 불일치를 본다 —
|
|
3475
|
+
# 실측(2026-08-26): 그렇게 잰 검증자가 스냅샷이 변조됐다고 보고했다.
|
|
3476
|
+
# 다이제스트 줄은 줄머리 앵커로 잘리므로(`_TARGET_DIGEST_RE`) 그 뒤에
|
|
3477
|
+
# 붙는 이 줄은 본문에 들어가지 않는다.
|
|
3134
3478
|
path.write_text(
|
|
3135
|
-
normalized
|
|
3479
|
+
normalized
|
|
3480
|
+
+ f"- **Verification target digest:** `{digest}`\n"
|
|
3481
|
+
+ "- **Digest rule:** `sha256` of the text above this line, with CRLF "
|
|
3482
|
+
"and CR normalized to LF, trailing whitespace stripped, and exactly "
|
|
3483
|
+
"one closing newline. This line and the digest line are not covered.\n",
|
|
3136
3484
|
encoding="utf-8",
|
|
3137
3485
|
)
|
|
3138
3486
|
return path, digest
|
|
@@ -3276,6 +3624,15 @@ def _write_instruction_set_sources(
|
|
|
3276
3624
|
clarification_body = attached_user_responses_section(
|
|
3277
3625
|
Path(inp.approved_plan_path)
|
|
3278
3626
|
)
|
|
3627
|
+
elif inp.task_type == "implementation-planning" and inp.selected_direction_path:
|
|
3628
|
+
# 새 계획 carry-in: 후보비교 리포트는 `--selected-direction` 으로 이미
|
|
3629
|
+
# 정규화돼 들어가지만, 그 스냅샷이 싣는 것은 선택된 방향 자체뿐이다
|
|
3630
|
+
# (`implementation_direction.write_selected_direction_snapshot`).
|
|
3631
|
+
# 같은 사이드카의 `C-NNN` 답변은 그 경로로 오지 않는다 — 붙이지 않던
|
|
3632
|
+
# 동안 사용자는 계획 런에 답변을 전달할 채널이 없었다.
|
|
3633
|
+
clarification_body = attached_user_responses_section(
|
|
3634
|
+
Path(inp.selected_direction_path)
|
|
3635
|
+
)
|
|
3279
3636
|
else:
|
|
3280
3637
|
clarification_body = ""
|
|
3281
3638
|
_stage_or_clear(
|
|
@@ -3294,6 +3651,15 @@ def _write_instruction_set_sources(
|
|
|
3294
3651
|
build_stage_ledger(Path(ctx["TASK_MANIFEST_PATH"]).parent)
|
|
3295
3652
|
if inp.task_type == "implementation-planning" else None
|
|
3296
3653
|
)
|
|
3654
|
+
# 새 계획을 여는 진입점은 `--selected-direction` 하나뿐이고
|
|
3655
|
+
# (`run._validate_planning_entry_inputs`), 답변된 clarification 이 없는
|
|
3656
|
+
# 재실행은 전부 이 경로로 들어온다. clarification 재실행 쪽은 직전 리포트를
|
|
3657
|
+
# 이미 `clarification-response.md` 로 받으므로 여기서 또 싣지 않는다.
|
|
3658
|
+
prior_planning_summary = (
|
|
3659
|
+
build_prior_planning_summary(Path(ctx["TASK_MANIFEST_PATH"]).parent)
|
|
3660
|
+
if inp.task_type == "implementation-planning" and inp.selected_direction_path
|
|
3661
|
+
else ""
|
|
3662
|
+
)
|
|
3297
3663
|
packet = build_analysis_packet(
|
|
3298
3664
|
task_key=ctx["TASK_KEY"],
|
|
3299
3665
|
task_type=ctx["TASK_TYPE"],
|
|
@@ -3310,6 +3676,7 @@ def _write_instruction_set_sources(
|
|
|
3310
3676
|
fix_cycles.read_rows(Path(ctx["TASK_MANIFEST_PATH"]).parent)),
|
|
3311
3677
|
stage_ledger_json=render_stage_ledger(stage_ledger),
|
|
3312
3678
|
stage_ledger_notice=stage_ledger_notice(stage_ledger),
|
|
3679
|
+
prior_planning_summary=prior_planning_summary,
|
|
3313
3680
|
)
|
|
3314
3681
|
if inp.task_type in ANALYSIS_TASK_TYPES:
|
|
3315
3682
|
packet += (
|
|
@@ -3697,6 +4064,9 @@ def _write_bundle_artifacts(
|
|
|
3697
4064
|
"""
|
|
3698
4065
|
_ensure_task_directories(ctx)
|
|
3699
4066
|
migrate_legacy_run_artifacts(ctx)
|
|
4067
|
+
# 회전은 legacy 이관 **뒤**다. 이관이 run dir 루트의 옛 평면 파일을 이 하위
|
|
4068
|
+
# 디렉터리들로 옮기므로, 먼저 돌면 그 파일들이 회전 대상에서 빠진다.
|
|
4069
|
+
_archive_prior_run_seqs(ctx)
|
|
3700
4070
|
cleanup_obsolete_generated_docs(
|
|
3701
4071
|
project_root=project_root, instruction_set_dir=Path(ctx["INSTRUCTION_SET_PATH"]),
|
|
3702
4072
|
)
|
|
@@ -3842,11 +4212,37 @@ def _record_run_in_central_index(
|
|
|
3842
4212
|
)
|
|
3843
4213
|
except Exception as exc:
|
|
3844
4214
|
print(
|
|
3845
|
-
f"
|
|
4215
|
+
f"central index: record_start failed; central index will be incomplete ({exc})",
|
|
3846
4216
|
file=sys.stderr,
|
|
3847
4217
|
)
|
|
3848
4218
|
if run_seq_override is not None:
|
|
3849
4219
|
_remove_leaked_reservation(ctx, run_seq_override)
|
|
4220
|
+
return
|
|
4221
|
+
_reconcile_prior_runs(ctx)
|
|
4222
|
+
|
|
4223
|
+
|
|
4224
|
+
def _reconcile_prior_runs(ctx: dict) -> None:
|
|
4225
|
+
"""이 프로젝트의 이전 run 행들을 디스크 사실로 재조정한다. 절대 치명적이지 않다.
|
|
4226
|
+
|
|
4227
|
+
`validators/validate-run.py` 가 종결 시점에 같은 재조정을 돌리지만, 검증까지
|
|
4228
|
+
못 가고 죽은 run 은 그 경로를 타지 않아 `running` 인 채 남는다. 그런 행은
|
|
4229
|
+
다음 run 이 시작될 때 여기서 abort 타임아웃으로 정리된다.
|
|
4230
|
+
|
|
4231
|
+
`_record_start` 가 중앙 락을 이미 놓은 뒤에 부른다 — flock 은 재진입 불가라
|
|
4232
|
+
락 안에서 부르면 self-deadlock 이다(`locks.central_lock`).
|
|
4233
|
+
"""
|
|
4234
|
+
from .reconcile import reconcile_home
|
|
4235
|
+
|
|
4236
|
+
project_id = str(ctx.get("PROJECT_ID", ""))
|
|
4237
|
+
if not project_id:
|
|
4238
|
+
return
|
|
4239
|
+
try:
|
|
4240
|
+
reconcile_home(okstra_home(), project=project_id)
|
|
4241
|
+
except Exception as exc: # noqa: BLE001 — 재조정 실패는 prepare 를 무르지 않는다
|
|
4242
|
+
print(
|
|
4243
|
+
f"central index: reconcile after record_start failed ({exc})",
|
|
4244
|
+
file=sys.stderr,
|
|
4245
|
+
)
|
|
3850
4246
|
|
|
3851
4247
|
|
|
3852
4248
|
def _initial_workflow_ctx(
|
|
@@ -4037,6 +4433,75 @@ def _provision_task_and_stage(
|
|
|
4037
4433
|
return _ProvisionedTask(worktree, None, fv_stage_arg)
|
|
4038
4434
|
|
|
4039
4435
|
|
|
4436
|
+
def _prior_run_backend(project_root: Path, inp: PrepareInputs) -> str:
|
|
4437
|
+
"""이 task 의 같은 phase 를 마지막으로 돌린 run 이 기록한 `terminalBackend`.
|
|
4438
|
+
그런 run 이 없으면 "".
|
|
4439
|
+
|
|
4440
|
+
stage 하위 트리(`runs/<task-type>/stage-N/`)까지 함께 훑는다. stage 는 이
|
|
4441
|
+
시점에 아직 정해지지 않았고, 어느 stage 였든 "같은 기계에서 방금 cmux 에
|
|
4442
|
+
붙었다"는 사실은 같기 때문이다.
|
|
4443
|
+
"""
|
|
4444
|
+
from .paths import runs_dir_of, task_dir
|
|
4445
|
+
|
|
4446
|
+
run_root = runs_dir_of(
|
|
4447
|
+
task_dir(project_root, inp.task_group, inp.task_id)
|
|
4448
|
+
) / slugify(inp.task_type)
|
|
4449
|
+
seq_pattern = re.compile(r"-(\d{3,})\.json$")
|
|
4450
|
+
latest = (-1, "")
|
|
4451
|
+
for manifest in run_root.rglob("run-manifest-*.json"):
|
|
4452
|
+
if manifest.parent.name != "manifests":
|
|
4453
|
+
continue
|
|
4454
|
+
matched = seq_pattern.search(manifest.name)
|
|
4455
|
+
if not matched:
|
|
4456
|
+
continue
|
|
4457
|
+
seq = int(matched.group(1))
|
|
4458
|
+
if seq <= latest[0]:
|
|
4459
|
+
continue
|
|
4460
|
+
try:
|
|
4461
|
+
payload = load_owned_object(manifest, artifact="run-manifest")
|
|
4462
|
+
except JsonBoundaryError:
|
|
4463
|
+
# 못 읽는 옛 매니페스트가 이 판정을 막을 이유는 없다 — 다음
|
|
4464
|
+
# 후보를 계속 본다.
|
|
4465
|
+
continue
|
|
4466
|
+
latest = (seq, str(payload.get("terminalBackend") or ""))
|
|
4467
|
+
return latest[1]
|
|
4468
|
+
|
|
4469
|
+
|
|
4470
|
+
def _resolve_terminal_backend(project_root: Path, inp: PrepareInputs) -> str:
|
|
4471
|
+
"""이 run 의 워커가 뜰 표면. 탐지는 하되, 조용한 강등은 하지 않는다.
|
|
4472
|
+
|
|
4473
|
+
표면은 사용자가 보는 것이다. cmux 아래에서 시작한 task 가 다음 run 부터
|
|
4474
|
+
말없이 cli-wrapper 로 내려가면 워커는 pane 없이 백그라운드로 돌고, 리드는
|
|
4475
|
+
매니페스트를 그대로 믿기 때문에 아무도 바뀐 줄 모른다. 실제로 그렇게
|
|
4476
|
+
내려간다: 탐지의 마지막 단계는 cmux 소켓 ping 인데, codex 의
|
|
4477
|
+
workspace-write 샌드박스는 AF_UNIX connect 를 EPERM 으로 막는다. 그래서
|
|
4478
|
+
cmux pane 안에서 돌던 리드가 자기 샌드박스 안에서 `okstra render-bundle` 을
|
|
4479
|
+
다시 실행하면 같은 cmux 가 그대로 떠 있는데도 탐지가 실패한다.
|
|
4480
|
+
|
|
4481
|
+
그래서 강등만 거부한다. 반대 방향(cli-wrapper → cmux-pane)은 화면에
|
|
4482
|
+
드러나므로 막을 이유가 없다. 의도한 강등은 `--terminal-backend cli-wrapper`
|
|
4483
|
+
로 선언한다 — 그때는 탐지도 하지 않는다.
|
|
4484
|
+
"""
|
|
4485
|
+
requested = inp.terminal_backend.strip()
|
|
4486
|
+
if requested:
|
|
4487
|
+
return requested
|
|
4488
|
+
detected = detect_terminal_backend()
|
|
4489
|
+
if detected == BACKEND_CMUX_PANE:
|
|
4490
|
+
return detected
|
|
4491
|
+
if _prior_run_backend(project_root, inp) != BACKEND_CMUX_PANE:
|
|
4492
|
+
return detected
|
|
4493
|
+
observed, remedy = cmux.unreachable_advice(cmux.unreachable_reason())
|
|
4494
|
+
raise PrepareError(" ".join(part for part in (
|
|
4495
|
+
f"this task's previous {inp.task_type} run put its workers on cmux "
|
|
4496
|
+
f"panes, and now {observed}.",
|
|
4497
|
+
"Preparing now would move them to background cli-wrapper processes "
|
|
4498
|
+
"with no surface to watch, so this run stops instead.",
|
|
4499
|
+
remedy,
|
|
4500
|
+
"To make the run wrapper-backed on purpose, pass "
|
|
4501
|
+
"`--terminal-backend cli-wrapper`.",
|
|
4502
|
+
) if part))
|
|
4503
|
+
|
|
4504
|
+
|
|
4040
4505
|
def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
4041
4506
|
"""Produce a complete okstra task bundle on disk. See module docstring."""
|
|
4042
4507
|
workspace_root = Path(inp.workspace_root)
|
|
@@ -4048,9 +4513,9 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4048
4513
|
json.loads(runtime_resolution_json or "{}")
|
|
4049
4514
|
except json.JSONDecodeError as exc:
|
|
4050
4515
|
raise PrepareError(f"invalid runtime resolution data: {exc}") from exc
|
|
4051
|
-
#
|
|
4516
|
+
# Resolved once here and reused below, so the gate and the manifest cannot
|
|
4052
4517
|
# disagree about which backend this run is on.
|
|
4053
|
-
terminal_backend =
|
|
4518
|
+
terminal_backend = _resolve_terminal_backend(project_root, inp)
|
|
4054
4519
|
# Outside cmux only claude-code has a dispatch backend of its own. Under
|
|
4055
4520
|
# cmux okstra owns the panes for every lead, so the render-only restriction
|
|
4056
4521
|
# no longer applies to any runtime.
|
|
@@ -4105,20 +4570,12 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4105
4570
|
terminal_backend=terminal_backend,
|
|
4106
4571
|
requires_executor=requires_executor,
|
|
4107
4572
|
)
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
selected_reviewers = ",".join(workers)
|
|
4115
|
-
else:
|
|
4116
|
-
models = _resolve_model_bindings(
|
|
4117
|
-
inp,
|
|
4118
|
-
workers,
|
|
4119
|
-
assignment_context,
|
|
4120
|
-
requires_executor,
|
|
4121
|
-
)
|
|
4573
|
+
workers, models = _project_assignment_plan(
|
|
4574
|
+
inp,
|
|
4575
|
+
assignment_plan,
|
|
4576
|
+
assignment_context,
|
|
4577
|
+
)
|
|
4578
|
+
selected_reviewers = ",".join(workers)
|
|
4122
4579
|
execution_manifest = _build_static_execution_manifest(
|
|
4123
4580
|
assignment_plan,
|
|
4124
4581
|
assignment_context,
|
|
@@ -4135,8 +4592,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4135
4592
|
_register_and_check_project(project_root, inp)
|
|
4136
4593
|
|
|
4137
4594
|
# ---- paths under per-task mutex (writes run-context-*.json) ----
|
|
4138
|
-
# OKSTRA_RUN_SEQ_OVERRIDE:
|
|
4139
|
-
#
|
|
4595
|
+
# OKSTRA_RUN_SEQ_OVERRIDE: 테스트 hook 이 미리 reserve 한 seq 를 강제하는
|
|
4596
|
+
# user-knob 환경 변수.
|
|
4140
4597
|
raw_override = os.environ.get("OKSTRA_RUN_SEQ_OVERRIDE", "").strip()
|
|
4141
4598
|
run_seq_override = int(raw_override) if raw_override else None
|
|
4142
4599
|
|
|
@@ -4235,7 +4692,11 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4235
4692
|
# jsonl under ~/.claude/projects/<encoded-cwd>/. Leaving this blank
|
|
4236
4693
|
# caused Phase 7 token-usage collection to fail (lead jsonl not
|
|
4237
4694
|
# found) and the validator to report `sessionId=`.
|
|
4238
|
-
|
|
4695
|
+
#
|
|
4696
|
+
# 인코딩의 기준은 **부르는 세션의 cwd** 이지 대상 프로젝트 루트가 아니다.
|
|
4697
|
+
# `--cwd` 로 다른 프로젝트를 몰 때 둘은 갈라지고, 프로젝트 루트로 인코딩한
|
|
4698
|
+
# 디렉터리의 최신 파일은 이 런과 무관한 남의 세션이다.
|
|
4699
|
+
claude_session_id = resolve_inproc_lead_session_id(Path.cwd())
|
|
4239
4700
|
else:
|
|
4240
4701
|
claude_session_id = generate_claude_session_id()
|
|
4241
4702
|
|
|
@@ -4430,9 +4891,37 @@ def _add_runtime_resolution_arguments(parser: ArgumentParser) -> None:
|
|
|
4430
4891
|
)
|
|
4431
4892
|
|
|
4432
4893
|
|
|
4894
|
+
_CODEX_RUN_EPILOG = r"""The dry-run entry point for the Codex lead adapter. It runs with
|
|
4895
|
+
`--render-only --lead-runtime codex` fixed, materializes the normal okstra task
|
|
4896
|
+
bundle, and prints the rendered lead prompt without dispatching workers.
|
|
4897
|
+
|
|
4898
|
+
Usage:
|
|
4899
|
+
okstra codex-run --project-root <dir> --project-id <id> \
|
|
4900
|
+
--task-group <tg> --task-id <tid> --task-type <type> \
|
|
4901
|
+
--task-brief <path-or-rel> [--workers <list>] [--directive <text>] \
|
|
4902
|
+
[--lead-model <m>] [--claude-model <m>] [--codex-model <m>] \
|
|
4903
|
+
[--antigravity-model <m>] [--report-writer-model <m>] \
|
|
4904
|
+
[--related-tasks <list>] [--base-ref <ref>] \
|
|
4905
|
+
[--clarification-response <path>] [--work-category <cat>] \
|
|
4906
|
+
[--selected-direction <selection-final-report.md>] \
|
|
4907
|
+
[--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \
|
|
4908
|
+
[--fix-cycle <yes|no>]
|
|
4909
|
+
|
|
4910
|
+
--render-only and --lead-runtime are owned by this command.
|
|
4911
|
+
Worker dispatch is intentionally disabled here; use 'okstra codex-dispatch'
|
|
4912
|
+
with the rendered run manifest for CLI-backed codex/antigravity workers.
|
|
4913
|
+
"""
|
|
4914
|
+
|
|
4915
|
+
|
|
4433
4916
|
def build_prepare_argument_parser():
|
|
4434
4917
|
"""Build the shared flat-argument parser used by public prepare shims."""
|
|
4435
|
-
|
|
4918
|
+
# 이 파서의 도움말이 사용자에게 닿는 경로는 `okstra codex-run --help` 뿐이다
|
|
4919
|
+
# — render-bundle / run 은 Node 쪽 USAGE 를 찍고 --help 를 넘기지 않는다.
|
|
4920
|
+
p = ArgumentParser(
|
|
4921
|
+
prog="okstra codex-run",
|
|
4922
|
+
epilog=_CODEX_RUN_EPILOG,
|
|
4923
|
+
formatter_class=RawDescriptionHelpFormatter,
|
|
4924
|
+
)
|
|
4436
4925
|
p.add_argument("--workspace-root", required=True)
|
|
4437
4926
|
p.add_argument("--project-root", required=True)
|
|
4438
4927
|
p.add_argument("--project-id", required=True)
|
|
@@ -4550,6 +5039,17 @@ def build_prepare_argument_parser():
|
|
|
4550
5039
|
),
|
|
4551
5040
|
)
|
|
4552
5041
|
p.add_argument("--render-only", action="store_true", dest="render_only")
|
|
5042
|
+
p.add_argument(
|
|
5043
|
+
"--terminal-backend",
|
|
5044
|
+
dest="terminal_backend",
|
|
5045
|
+
choices=(BACKEND_CLI_WRAPPER, BACKEND_CMUX_PANE),
|
|
5046
|
+
default="",
|
|
5047
|
+
help=(
|
|
5048
|
+
"워커가 뜰 표면을 명시한다. 미지정이면 탐지하되, 직전 run 이 "
|
|
5049
|
+
"cmux-pane 이었는데 지금 cmux 에 못 붙는 경우 조용히 내려가지 "
|
|
5050
|
+
"않고 멈춘다."
|
|
5051
|
+
),
|
|
5052
|
+
)
|
|
4553
5053
|
p.add_argument(
|
|
4554
5054
|
"--no-plan-verification",
|
|
4555
5055
|
action="store_false",
|
|
@@ -4740,6 +5240,7 @@ def main(argv: list[str]) -> int:
|
|
|
4740
5240
|
reverify_scope=args.reverify_scope,
|
|
4741
5241
|
pr_template_path=args.pr_template_path,
|
|
4742
5242
|
render_only=args.render_only,
|
|
5243
|
+
terminal_backend=args.terminal_backend,
|
|
4743
5244
|
approve_plan_ack=args.approve_plan_ack,
|
|
4744
5245
|
implementation_option=args.implementation_option,
|
|
4745
5246
|
plan_verification_enabled=args.plan_verification_enabled,
|