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.
Files changed (541) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.d.mts +88 -1
  3. package/dist/cli-registry.mjs +68 -111
  4. package/dist/cli-registry.mjs.map +1 -1
  5. package/dist/commands/execute/render-bundle.mjs +0 -1
  6. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  7. package/dist/commands/execute/run.mjs +8 -3
  8. package/dist/commands/execute/run.mjs.map +1 -1
  9. package/dist/commands/lifecycle/check-project.mjs +1 -14
  10. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  11. package/dist/commands/lifecycle/config.mjs +38 -40
  12. package/dist/commands/lifecycle/config.mjs.map +1 -1
  13. package/dist/commands/lifecycle/doctor.d.mts +22 -7
  14. package/dist/commands/lifecycle/doctor.mjs +77 -49
  15. package/dist/commands/lifecycle/doctor.mjs.map +1 -1
  16. package/dist/commands/lifecycle/install.d.mts +12 -10
  17. package/dist/commands/lifecycle/install.mjs +104 -39
  18. package/dist/commands/lifecycle/install.mjs.map +1 -1
  19. package/dist/commands/lifecycle/paths.mjs +8 -3
  20. package/dist/commands/lifecycle/paths.mjs.map +1 -1
  21. package/dist/commands/lifecycle/preflight.mjs +2 -1
  22. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  23. package/dist/commands/lifecycle/setup.mjs +22 -36
  24. package/dist/commands/lifecycle/setup.mjs.map +1 -1
  25. package/dist/commands/lifecycle/uninstall.d.mts +4 -2
  26. package/dist/commands/lifecycle/uninstall.mjs +59 -15
  27. package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
  28. package/dist/commands/memory/memory.mjs +7 -1
  29. package/dist/commands/memory/memory.mjs.map +1 -1
  30. package/dist/lib/helper-scripts.d.mts +1 -1
  31. package/dist/lib/helper-scripts.mjs +10 -18
  32. package/dist/lib/helper-scripts.mjs.map +1 -1
  33. package/dist/lib/host-config.d.mts +72 -0
  34. package/dist/lib/host-config.mjs +404 -0
  35. package/dist/lib/host-config.mjs.map +1 -0
  36. package/dist/lib/host-registry-client.d.mts +2 -2
  37. package/dist/lib/host-registry-client.mjs +0 -3
  38. package/dist/lib/host-registry-client.mjs.map +1 -1
  39. package/dist/lib/install-assets.d.mts +1 -0
  40. package/dist/lib/install-assets.mjs +4 -0
  41. package/dist/lib/install-assets.mjs.map +1 -1
  42. package/dist/lib/proc.d.mts +2 -0
  43. package/dist/lib/proc.mjs +12 -0
  44. package/dist/lib/proc.mjs.map +1 -1
  45. package/dist/lib/python-command.d.mts +2 -0
  46. package/dist/lib/python-command.mjs +52 -0
  47. package/dist/lib/python-command.mjs.map +1 -0
  48. package/dist/lib/python-helper.d.mts +2 -5
  49. package/dist/lib/python-helper.mjs +3 -52
  50. package/dist/lib/python-helper.mjs.map +1 -1
  51. package/dist/lib/runtime-payload.d.mts +23 -0
  52. package/dist/lib/runtime-payload.mjs +57 -0
  53. package/dist/lib/runtime-payload.mjs.map +1 -0
  54. package/dist/lib/types.d.mts +24 -13
  55. package/docs/architecture/storage-model.md +21 -5
  56. package/docs/architecture.md +37 -36
  57. package/docs/cli.md +55 -76
  58. package/docs/coding-rules.md +295 -0
  59. package/docs/container.md +10 -36
  60. package/docs/contributor-change-matrix.md +1 -1
  61. package/docs/for-ai/skills/okstra-code-review.md +0 -1
  62. package/docs/for-ai/skills/okstra-container-build.md +8 -41
  63. package/docs/for-ai/skills/okstra-inspect.md +4 -4
  64. package/docs/for-ai/skills/okstra-manager.md +2 -2
  65. package/docs/for-ai/skills/okstra-rollup.md +0 -1
  66. package/docs/for-ai/skills/okstra-run.md +3 -3
  67. package/docs/for-ai/skills/okstra-user-response.md +0 -1
  68. package/docs/performance-improvement-plan-v2.md +1 -1
  69. package/docs/project-structure-overview.md +62 -62
  70. package/docs/task-process/README.md +3 -3
  71. package/docs/task-process/common-flow.md +1 -1
  72. package/docs/task-process/error-analysis.md +3 -3
  73. package/docs/task-process/implementation-planning.md +3 -1
  74. package/docs/task-process/release-handoff.md +1 -1
  75. package/package.json +3 -2
  76. package/runtime/BUILD.json +2 -2
  77. package/runtime/agents/workers/claude-worker.md +6 -11
  78. package/runtime/agents/workers/report-writer-worker.md +1 -1
  79. package/runtime/bin/lib/okstra/cli.sh +4 -0
  80. package/runtime/bin/lib/okstra/globals.sh +2 -0
  81. package/runtime/bin/lib/okstra/interactive.sh +26 -173
  82. package/runtime/bin/lib/okstra/project-resolver.sh +23 -61
  83. package/runtime/bin/lib/okstra/usage.sh +3 -3
  84. package/runtime/bin/okstra-compact-reminder.sh +1 -5
  85. package/runtime/bin/okstra-error-log.py +19 -2
  86. package/runtime/bin/okstra-import-check.py +26 -0
  87. package/runtime/bin/okstra-inject-report-index.py +5 -4
  88. package/runtime/bin/okstra-provider-exec.py +22 -17
  89. package/runtime/bin/okstra-render-final-report.py +16 -6
  90. package/runtime/bin/okstra-render-report-views.py +25 -18
  91. package/runtime/bin/okstra-report-translate.py +30 -18
  92. package/runtime/bin/okstra-spawn-followups.py +4 -0
  93. package/runtime/bin/okstra-token-usage.py +3 -0
  94. package/runtime/bin/okstra.sh +4 -2
  95. package/runtime/bin/okstra_bootstrap.py +58 -0
  96. package/runtime/prompts/coding-preflight/overview.md +1 -1
  97. package/runtime/prompts/duties/planning-worker.md +1 -1
  98. package/runtime/prompts/launch.template.md +35 -9
  99. package/runtime/prompts/lead/convergence.md +96 -87
  100. package/runtime/prompts/lead/okstra-lead-contract.md +63 -12
  101. package/runtime/prompts/lead/plan-body-verification.md +65 -46
  102. package/runtime/prompts/lead/report-writer.md +34 -4
  103. package/runtime/prompts/lead/team-contract.md +4 -1
  104. package/runtime/prompts/profiles/_clarification-recommendation.md +2 -1
  105. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  106. package/runtime/prompts/profiles/_common-contract.md +3 -3
  107. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  108. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  109. package/runtime/prompts/profiles/_implementation-executor.md +3 -1
  110. package/runtime/prompts/profiles/_implementation-verifier.md +16 -2
  111. package/runtime/prompts/profiles/error-analysis.md +4 -3
  112. package/runtime/prompts/profiles/final-verification.md +2 -2
  113. package/runtime/prompts/profiles/implementation-planning.md +22 -17
  114. package/runtime/prompts/profiles/implementation.md +3 -2
  115. package/runtime/prompts/profiles/requirements-discovery.md +2 -1
  116. package/runtime/prompts/wizard/prompts.ko.json +29 -8
  117. package/runtime/python/okstra_ctl/__init__.py +6 -27
  118. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -5
  119. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +21 -2
  120. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +3 -12
  121. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +40 -22
  122. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +14 -21
  123. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +37 -20
  124. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +86 -14
  125. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +12 -12
  126. package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +4 -7
  127. package/runtime/python/okstra_ctl/agent/__init__.py +1 -0
  128. package/runtime/python/okstra_ctl/{agent_activity.py → agent/activity.py} +12 -1
  129. package/runtime/python/okstra_ctl/{agent_invocation.py → agent/invocation.py} +34 -5
  130. package/runtime/python/okstra_ctl/agent/prompt_cli/__init__.py +19 -0
  131. package/runtime/python/okstra_ctl/agent/prompt_cli/__main__.py +11 -0
  132. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +215 -0
  133. package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +155 -0
  134. package/runtime/python/okstra_ctl/agent/prompt_cli/emit.py +66 -0
  135. package/runtime/python/okstra_ctl/agent/prompt_cli/inputs.py +138 -0
  136. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +408 -0
  137. package/runtime/python/okstra_ctl/agent/prompt_cli/results.py +197 -0
  138. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +107 -0
  139. package/runtime/python/okstra_ctl/analysis_packet.py +47 -4
  140. package/runtime/python/okstra_ctl/approval_decisions.py +260 -8
  141. package/runtime/python/okstra_ctl/assignment_resolver.py +0 -12
  142. package/runtime/python/okstra_ctl/attempt_evidence.py +12 -24
  143. package/runtime/python/okstra_ctl/blocking_checks.py +251 -0
  144. package/runtime/python/okstra_ctl/brief_frontmatter.py +0 -7
  145. package/runtime/python/okstra_ctl/clarification_items/__init__.py +102 -0
  146. package/runtime/python/okstra_ctl/clarification_items/carry.py +224 -0
  147. package/runtime/python/okstra_ctl/clarification_items/dispositions.py +135 -0
  148. package/runtime/python/okstra_ctl/clarification_items/parsing.py +253 -0
  149. package/runtime/python/okstra_ctl/clarification_items/rows.py +115 -0
  150. package/runtime/python/okstra_ctl/clarification_items/scan.py +215 -0
  151. package/runtime/python/okstra_ctl/clarification_items/sidecars.py +211 -0
  152. package/runtime/python/okstra_ctl/cmux.py +38 -31
  153. package/runtime/python/okstra_ctl/code_review_target.py +163 -1
  154. package/runtime/python/okstra_ctl/conformance.py +23 -0
  155. package/runtime/python/okstra_ctl/container.py +38 -421
  156. package/runtime/python/okstra_ctl/context_cost.py +16 -2
  157. package/runtime/python/okstra_ctl/contract_graph_cli.py +7 -0
  158. package/runtime/python/okstra_ctl/convergence.py +101 -15
  159. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +345 -0
  160. package/runtime/python/okstra_ctl/convergence_engine.py +382 -70
  161. package/runtime/python/okstra_ctl/convergence_store.py +5 -29
  162. package/runtime/python/okstra_ctl/design_prep.py +108 -12
  163. package/runtime/python/okstra_ctl/design_snapshot.py +11 -1
  164. package/runtime/python/okstra_ctl/design_surfaces.py +29 -2
  165. package/runtime/python/okstra_ctl/dispatch_core.py +102 -25
  166. package/runtime/python/okstra_ctl/dispatch_state.py +268 -39
  167. package/runtime/python/okstra_ctl/doctor_cli.py +48 -0
  168. package/runtime/python/okstra_ctl/domain/worker_exec.py +10 -5
  169. package/runtime/python/okstra_ctl/domain/worker_presentation.py +21 -1
  170. package/runtime/python/okstra_ctl/domain/worker_stream.py +52 -21
  171. package/runtime/python/okstra_ctl/domain/write_policy.py +219 -0
  172. package/runtime/python/okstra_ctl/entrypoints/hosts.py +9 -2
  173. package/runtime/python/okstra_ctl/error_log_core.py +1 -1
  174. package/runtime/python/okstra_ctl/error_report.py +14 -0
  175. package/runtime/python/okstra_ctl/error_zip.py +18 -2
  176. package/runtime/python/okstra_ctl/execution_identity.py +100 -7
  177. package/runtime/python/okstra_ctl/execution_manifest.py +67 -20
  178. package/runtime/python/okstra_ctl/execution_mutation_audit.py +90 -8
  179. package/runtime/python/okstra_ctl/final_report_paths.py +17 -0
  180. package/runtime/python/okstra_ctl/final_report_schema.py +90 -16
  181. package/runtime/python/okstra_ctl/git_reconcile.py +22 -2
  182. package/runtime/python/okstra_ctl/handoff.py +24 -1
  183. package/runtime/python/okstra_ctl/ids.py +6 -16
  184. package/runtime/python/okstra_ctl/implementation_direction.py +142 -33
  185. package/runtime/python/okstra_ctl/implementation_options.py +25 -11
  186. package/runtime/python/okstra_ctl/implementation_outcome.py +5 -2
  187. package/runtime/python/okstra_ctl/improvement_lenses.py +0 -14
  188. package/runtime/python/okstra_ctl/incremental_carry.py +69 -8
  189. package/runtime/python/okstra_ctl/incremental_scope.py +173 -12
  190. package/runtime/python/okstra_ctl/index.py +2 -2
  191. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +48 -8
  192. package/runtime/python/okstra_ctl/interactive_cli.py +223 -0
  193. package/runtime/python/okstra_ctl/json_boundary.py +10 -0
  194. package/runtime/python/okstra_ctl/listing.py +0 -91
  195. package/runtime/python/okstra_ctl/locks.py +5 -19
  196. package/runtime/python/okstra_ctl/log_report.py +14 -0
  197. package/runtime/python/okstra_ctl/manager_cli.py +22 -3
  198. package/runtime/python/okstra_ctl/manager_launch.py +4 -8
  199. package/runtime/python/okstra_ctl/material.py +2 -2
  200. package/runtime/python/okstra_ctl/migrate.py +23 -1
  201. package/runtime/python/okstra_ctl/model_cli.py +19 -5
  202. package/runtime/python/okstra_ctl/model_discovery.py +46 -47
  203. package/runtime/python/okstra_ctl/model_io/__init__.py +1 -0
  204. package/runtime/python/okstra_ctl/model_io/lines.py +163 -0
  205. package/runtime/python/okstra_ctl/model_io/references.py +370 -0
  206. package/runtime/python/okstra_ctl/model_io/renderers.py +498 -0
  207. package/runtime/python/okstra_ctl/model_io_cli.py +30 -945
  208. package/runtime/python/okstra_ctl/model_pool.py +10 -6
  209. package/runtime/python/okstra_ctl/models.py +8 -8
  210. package/runtime/python/okstra_ctl/mutation_recovery.py +5 -63
  211. package/runtime/python/okstra_ctl/next_phase.py +121 -69
  212. package/runtime/python/okstra_ctl/pane_reclaim.py +23 -0
  213. package/runtime/python/okstra_ctl/pane_title.py +11 -8
  214. package/runtime/python/okstra_ctl/path_hints.py +36 -11
  215. package/runtime/python/okstra_ctl/paths.py +76 -8
  216. package/runtime/python/okstra_ctl/plan_items.py +132 -16
  217. package/runtime/python/okstra_ctl/plan_items_cli.py +486 -36
  218. package/runtime/python/okstra_ctl/plan_run_root.py +1 -1
  219. package/runtime/python/okstra_ctl/plan_validate_cli.py +51 -0
  220. package/runtime/python/okstra_ctl/plan_verify_cli.py +80 -0
  221. package/runtime/python/okstra_ctl/prepare_error.py +20 -0
  222. package/runtime/python/okstra_ctl/prior_planning.py +157 -0
  223. package/runtime/python/okstra_ctl/profile_show.py +18 -1
  224. package/runtime/python/okstra_ctl/project_setup_cli.py +140 -0
  225. package/runtime/python/okstra_ctl/recap.py +22 -1
  226. package/runtime/python/okstra_ctl/reconcile.py +84 -0
  227. package/runtime/python/okstra_ctl/registry/host_registry.py +18 -1
  228. package/runtime/python/okstra_ctl/render.py +98 -32
  229. package/runtime/python/okstra_ctl/render_final_report.py +3 -7
  230. package/runtime/python/okstra_ctl/report_assembly.py +297 -7
  231. package/runtime/python/okstra_ctl/report_contract.py +30 -7
  232. package/runtime/python/okstra_ctl/report_finalize.py +206 -15
  233. package/runtime/python/okstra_ctl/report_html/common.py +2 -2
  234. package/runtime/python/okstra_ctl/report_html/render.py +8 -12
  235. package/runtime/python/okstra_ctl/report_language.py +8 -4
  236. package/runtime/python/okstra_ctl/report_narrative.py +90 -3
  237. package/runtime/python/okstra_ctl/report_projections.py +8 -2
  238. package/runtime/python/okstra_ctl/report_synthesis_packet.py +258 -1
  239. package/runtime/python/okstra_ctl/report_translation.py +2 -35
  240. package/runtime/python/okstra_ctl/report_views.py +4 -22
  241. package/runtime/python/okstra_ctl/resolve_task_key.py +13 -0
  242. package/runtime/python/okstra_ctl/rollup.py +13 -0
  243. package/runtime/python/okstra_ctl/run.py +639 -138
  244. package/runtime/python/okstra_ctl/run_audit.py +13 -0
  245. package/runtime/python/okstra_ctl/run_index_row.py +0 -12
  246. package/runtime/python/okstra_ctl/seeding.py +21 -11
  247. package/runtime/python/okstra_ctl/sequence.py +1 -1
  248. package/runtime/python/okstra_ctl/session.py +64 -14
  249. package/runtime/python/okstra_ctl/set_work_status.py +18 -0
  250. package/runtime/python/okstra_ctl/stage_integrate.py +15 -1
  251. package/runtime/python/okstra_ctl/stage_ledger.py +1 -1
  252. package/runtime/python/okstra_ctl/stage_map.py +57 -11
  253. package/runtime/python/okstra_ctl/stage_map_cli.py +83 -0
  254. package/runtime/python/okstra_ctl/stage_map_view.py +65 -0
  255. package/runtime/python/okstra_ctl/stage_targets.py +3 -12
  256. package/runtime/python/okstra_ctl/task_list_cli.py +138 -0
  257. package/runtime/python/okstra_ctl/task_show_cli.py +66 -0
  258. package/runtime/python/okstra_ctl/task_target.py +4 -16
  259. package/runtime/python/okstra_ctl/team.py +49 -2
  260. package/runtime/python/okstra_ctl/time_report.py +17 -2
  261. package/runtime/python/okstra_ctl/usage_report.py +11 -0
  262. package/runtime/python/okstra_ctl/user_response.py +55 -218
  263. package/runtime/python/okstra_ctl/user_response_values.py +242 -0
  264. package/runtime/python/okstra_ctl/validation_contract.py +3 -0
  265. package/runtime/python/okstra_ctl/verification_target.py +68 -0
  266. package/runtime/python/okstra_ctl/wizard.py +457 -92
  267. package/runtime/python/okstra_ctl/worker_artifacts.py +75 -16
  268. package/runtime/python/okstra_ctl/worker_audit_check.py +22 -0
  269. package/runtime/python/okstra_ctl/worker_dispatch.py +21 -0
  270. package/runtime/python/okstra_ctl/worker_liveness.py +33 -0
  271. package/runtime/python/okstra_ctl/worker_prompt_body.py +15 -2
  272. package/runtime/python/okstra_ctl/worker_prompt_contract.py +98 -7
  273. package/runtime/python/okstra_ctl/worker_prompt_headers.py +24 -2
  274. package/runtime/python/okstra_ctl/worker_prompt_policy.py +18 -3
  275. package/runtime/python/okstra_ctl/worker_request.py +2 -1
  276. package/runtime/python/okstra_ctl/worker_runner.py +16 -9
  277. package/runtime/python/okstra_ctl/worker_state.py +16 -1
  278. package/runtime/python/okstra_ctl/workflow.py +18 -1
  279. package/runtime/python/okstra_ctl/worktree/__init__.py +92 -0
  280. package/runtime/python/okstra_ctl/worktree/cleanliness.py +89 -0
  281. package/runtime/python/okstra_ctl/worktree/decisions.py +127 -0
  282. package/runtime/python/okstra_ctl/worktree/git_ops.py +165 -0
  283. package/runtime/python/okstra_ctl/worktree/linking.py +249 -0
  284. package/runtime/python/okstra_ctl/worktree/naming.py +91 -0
  285. package/runtime/python/okstra_ctl/worktree/provision.py +385 -0
  286. package/runtime/python/okstra_ctl/worktree/sync_config.py +181 -0
  287. package/runtime/python/okstra_ctl/worktree_cli.py +75 -0
  288. package/runtime/python/okstra_ctl/worktree_lookup_cli.py +39 -0
  289. package/runtime/python/okstra_ctl/worktree_registry.py +7 -0
  290. package/runtime/python/okstra_ctl/worktree_status_cli.py +53 -0
  291. package/runtime/python/okstra_ctl/write_policy.py +90 -213
  292. package/runtime/python/okstra_project/__init__.py +0 -4
  293. package/runtime/python/okstra_project/dirs.py +2 -2
  294. package/runtime/python/okstra_project/phase_pointer.py +84 -0
  295. package/runtime/python/okstra_project/slug.py +24 -0
  296. package/runtime/python/okstra_project/state.py +22 -230
  297. package/runtime/python/okstra_token_usage/claude.py +9 -1
  298. package/runtime/python/okstra_token_usage/cli.py +3 -1
  299. package/runtime/python/okstra_token_usage/collect.py +71 -10
  300. package/runtime/python/okstra_token_usage/cursor.py +2 -0
  301. package/runtime/python/okstra_token_usage/grok.py +1 -5
  302. package/runtime/python/okstra_token_usage/paths.py +26 -0
  303. package/runtime/python/okstra_token_usage/pricing.py +19 -7
  304. package/runtime/schemas/convergence-critic-results-v1.0.schema.json +5 -0
  305. package/runtime/schemas/execution-manifest-v2.schema.json +10 -10
  306. package/runtime/schemas/final-report-v2.0.schema.json +9 -26
  307. package/runtime/schemas/final-report-v3.0.schema.json +173 -41
  308. package/runtime/schemas/report-narrative-v3.0.schema.json +3 -2
  309. package/runtime/skills/okstra-brief-gen/SKILL.md +35 -2
  310. package/runtime/skills/okstra-container-build/SKILL.md +16 -47
  311. package/runtime/skills/okstra-inspect/facets/history.md +1 -1
  312. package/runtime/skills/okstra-inspect/facets/status.md +7 -6
  313. package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
  314. package/runtime/skills/okstra-run/SKILL.md +7 -5
  315. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  316. package/runtime/templates/reports/brief.template.md +6 -2
  317. package/runtime/templates/reports/error-analysis-input.template.md +2 -0
  318. package/runtime/templates/reports/final-verification-input.template.md +2 -0
  319. package/runtime/templates/reports/implementation-input.template.md +7 -1
  320. package/runtime/templates/reports/implementation-planning-input.template.md +4 -0
  321. package/runtime/templates/reports/quick-input.template.md +2 -0
  322. package/runtime/templates/reports/release-handoff-input.template.md +6 -1
  323. package/runtime/templates/reports/report.js +20 -3
  324. package/runtime/templates/reports/schedule.template.md +2 -0
  325. package/runtime/templates/reports/settings.template.json +0 -10
  326. package/runtime/templates/reports/task-brief.template.md +3 -0
  327. package/runtime/templates/reports/user-response.template.md +7 -3
  328. package/runtime/validators/checks/fixtures-01.py +57 -0
  329. package/runtime/validators/checks/fixtures-02.py +565 -0
  330. package/runtime/validators/checks/runners-01.py +108 -0
  331. package/runtime/validators/checks/validate-assets-01.py +60 -0
  332. package/runtime/validators/checks/validate-prompt-metadata-01.py +261 -0
  333. package/runtime/validators/checks/validate-tasks-01.py +46 -0
  334. package/runtime/validators/checks/validate-tasks-02.py +85 -0
  335. package/runtime/validators/checks/validate-tasks-03.py +61 -0
  336. package/runtime/validators/checks/validate-tasks-04.py +118 -0
  337. package/runtime/validators/forbidden_actions.py +73 -3
  338. package/runtime/validators/lib/common.sh +5 -0
  339. package/runtime/validators/lib/fixtures.sh +7 -591
  340. package/runtime/validators/lib/paths.sh +13 -0
  341. package/runtime/validators/lib/runners.sh +6 -104
  342. package/runtime/validators/lib/summary.sh +1 -1
  343. package/runtime/validators/lib/validate-assets.sh +6 -56
  344. package/runtime/validators/lib/validate-prompt-metadata.sh +6 -257
  345. package/runtime/validators/lib/validate-tasks.sh +9 -294
  346. package/runtime/validators/validate-implementation-plan-stages.py +4 -4
  347. package/runtime/validators/validate-run.py +1369 -2654
  348. package/runtime/validators/validate-workflow.sh +56 -16
  349. package/runtime/validators/validate_improvement_report.py +2 -1
  350. package/runtime/validators/validate_session_conformance.py +295 -49
  351. package/dist/commands/execute/agent-prompt.d.mts +0 -1
  352. package/dist/commands/execute/agent-prompt.mjs +0 -24
  353. package/dist/commands/execute/agent-prompt.mjs.map +0 -1
  354. package/dist/commands/execute/codex-dispatch.d.mts +0 -3
  355. package/dist/commands/execute/codex-dispatch.mjs +0 -6
  356. package/dist/commands/execute/codex-dispatch.mjs.map +0 -1
  357. package/dist/commands/execute/codex-run.d.mts +0 -3
  358. package/dist/commands/execute/codex-run.mjs +0 -62
  359. package/dist/commands/execute/codex-run.mjs.map +0 -1
  360. package/dist/commands/execute/convergence.d.mts +0 -1
  361. package/dist/commands/execute/convergence.mjs +0 -38
  362. package/dist/commands/execute/convergence.mjs.map +0 -1
  363. package/dist/commands/execute/error-log.d.mts +0 -1
  364. package/dist/commands/execute/error-log.mjs +0 -18
  365. package/dist/commands/execute/error-log.mjs.map +0 -1
  366. package/dist/commands/execute/git-reconcile.d.mts +0 -1
  367. package/dist/commands/execute/git-reconcile.mjs +0 -30
  368. package/dist/commands/execute/git-reconcile.mjs.map +0 -1
  369. package/dist/commands/execute/handoff.d.mts +0 -1
  370. package/dist/commands/execute/handoff.mjs +0 -31
  371. package/dist/commands/execute/handoff.mjs.map +0 -1
  372. package/dist/commands/execute/incremental-carry.d.mts +0 -1
  373. package/dist/commands/execute/incremental-carry.mjs +0 -20
  374. package/dist/commands/execute/incremental-carry.mjs.map +0 -1
  375. package/dist/commands/execute/incremental-scope.d.mts +0 -1
  376. package/dist/commands/execute/incremental-scope.mjs +0 -29
  377. package/dist/commands/execute/incremental-scope.mjs.map +0 -1
  378. package/dist/commands/execute/integrate-stages.d.mts +0 -1
  379. package/dist/commands/execute/integrate-stages.mjs +0 -24
  380. package/dist/commands/execute/integrate-stages.mjs.map +0 -1
  381. package/dist/commands/execute/pane-title.d.mts +0 -1
  382. package/dist/commands/execute/pane-title.mjs +0 -20
  383. package/dist/commands/execute/pane-title.mjs.map +0 -1
  384. package/dist/commands/execute/plan-items.d.mts +0 -1
  385. package/dist/commands/execute/plan-items.mjs +0 -9
  386. package/dist/commands/execute/plan-items.mjs.map +0 -1
  387. package/dist/commands/execute/plan-validate.d.mts +0 -1
  388. package/dist/commands/execute/plan-validate.mjs +0 -68
  389. package/dist/commands/execute/plan-validate.mjs.map +0 -1
  390. package/dist/commands/execute/plan-verify.d.mts +0 -1
  391. package/dist/commands/execute/plan-verify.mjs +0 -43
  392. package/dist/commands/execute/plan-verify.mjs.map +0 -1
  393. package/dist/commands/execute/spawn-followups.d.mts +0 -1
  394. package/dist/commands/execute/spawn-followups.mjs +0 -22
  395. package/dist/commands/execute/spawn-followups.mjs.map +0 -1
  396. package/dist/commands/execute/team.d.mts +0 -3
  397. package/dist/commands/execute/team.mjs +0 -66
  398. package/dist/commands/execute/team.mjs.map +0 -1
  399. package/dist/commands/execute/token-usage.d.mts +0 -1
  400. package/dist/commands/execute/token-usage.mjs +0 -19
  401. package/dist/commands/execute/token-usage.mjs.map +0 -1
  402. package/dist/commands/execute/worker-audit-check.d.mts +0 -1
  403. package/dist/commands/execute/worker-audit-check.mjs +0 -34
  404. package/dist/commands/execute/worker-audit-check.mjs.map +0 -1
  405. package/dist/commands/execute/worker-dispatch.d.mts +0 -7
  406. package/dist/commands/execute/worker-dispatch.mjs +0 -64
  407. package/dist/commands/execute/worker-dispatch.mjs.map +0 -1
  408. package/dist/commands/execute/worker-state.d.mts +0 -1
  409. package/dist/commands/execute/worker-state.mjs +0 -28
  410. package/dist/commands/execute/worker-state.mjs.map +0 -1
  411. package/dist/commands/execute/worktree-lookup.d.mts +0 -1
  412. package/dist/commands/execute/worktree-lookup.mjs +0 -92
  413. package/dist/commands/execute/worktree-lookup.mjs.map +0 -1
  414. package/dist/commands/execute/worktree-status.d.mts +0 -1
  415. package/dist/commands/execute/worktree-status.mjs +0 -121
  416. package/dist/commands/execute/worktree-status.mjs.map +0 -1
  417. package/dist/commands/inspect/code-review.d.mts +0 -1
  418. package/dist/commands/inspect/code-review.mjs +0 -32
  419. package/dist/commands/inspect/code-review.mjs.map +0 -1
  420. package/dist/commands/inspect/container.d.mts +0 -1
  421. package/dist/commands/inspect/container.mjs +0 -25
  422. package/dist/commands/inspect/container.mjs.map +0 -1
  423. package/dist/commands/inspect/context-cost.d.mts +0 -1
  424. package/dist/commands/inspect/context-cost.mjs +0 -25
  425. package/dist/commands/inspect/context-cost.mjs.map +0 -1
  426. package/dist/commands/inspect/design-prep.d.mts +0 -1
  427. package/dist/commands/inspect/design-prep.mjs +0 -22
  428. package/dist/commands/inspect/design-prep.mjs.map +0 -1
  429. package/dist/commands/inspect/error-report.d.mts +0 -1
  430. package/dist/commands/inspect/error-report.mjs +0 -25
  431. package/dist/commands/inspect/error-report.mjs.map +0 -1
  432. package/dist/commands/inspect/error-zip.d.mts +0 -1
  433. package/dist/commands/inspect/error-zip.mjs +0 -24
  434. package/dist/commands/inspect/error-zip.mjs.map +0 -1
  435. package/dist/commands/inspect/log-report.d.mts +0 -1
  436. package/dist/commands/inspect/log-report.mjs +0 -26
  437. package/dist/commands/inspect/log-report.mjs.map +0 -1
  438. package/dist/commands/inspect/model-io.d.mts +0 -1
  439. package/dist/commands/inspect/model-io.mjs +0 -25
  440. package/dist/commands/inspect/model-io.mjs.map +0 -1
  441. package/dist/commands/inspect/profile-show.d.mts +0 -1
  442. package/dist/commands/inspect/profile-show.mjs +0 -28
  443. package/dist/commands/inspect/profile-show.mjs.map +0 -1
  444. package/dist/commands/inspect/recap.d.mts +0 -1
  445. package/dist/commands/inspect/recap.mjs +0 -30
  446. package/dist/commands/inspect/recap.mjs.map +0 -1
  447. package/dist/commands/inspect/resolve-task-key.d.mts +0 -1
  448. package/dist/commands/inspect/resolve-task-key.mjs +0 -24
  449. package/dist/commands/inspect/resolve-task-key.mjs.map +0 -1
  450. package/dist/commands/inspect/rollup.d.mts +0 -1
  451. package/dist/commands/inspect/rollup.mjs +0 -25
  452. package/dist/commands/inspect/rollup.mjs.map +0 -1
  453. package/dist/commands/inspect/run-audit.d.mts +0 -1
  454. package/dist/commands/inspect/run-audit.mjs +0 -25
  455. package/dist/commands/inspect/run-audit.mjs.map +0 -1
  456. package/dist/commands/inspect/set-work-status.d.mts +0 -1
  457. package/dist/commands/inspect/set-work-status.mjs +0 -29
  458. package/dist/commands/inspect/set-work-status.mjs.map +0 -1
  459. package/dist/commands/inspect/stage-map.d.mts +0 -1
  460. package/dist/commands/inspect/stage-map.mjs +0 -131
  461. package/dist/commands/inspect/stage-map.mjs.map +0 -1
  462. package/dist/commands/inspect/task-list.d.mts +0 -1
  463. package/dist/commands/inspect/task-list.mjs +0 -149
  464. package/dist/commands/inspect/task-list.mjs.map +0 -1
  465. package/dist/commands/inspect/task-show.d.mts +0 -1
  466. package/dist/commands/inspect/task-show.mjs +0 -108
  467. package/dist/commands/inspect/task-show.mjs.map +0 -1
  468. package/dist/commands/inspect/time-report.d.mts +0 -1
  469. package/dist/commands/inspect/time-report.mjs +0 -24
  470. package/dist/commands/inspect/time-report.mjs.map +0 -1
  471. package/dist/commands/inspect/usage-report.d.mts +0 -1
  472. package/dist/commands/inspect/usage-report.mjs +0 -23
  473. package/dist/commands/inspect/usage-report.mjs.map +0 -1
  474. package/dist/commands/inspect/user-response.d.mts +0 -1
  475. package/dist/commands/inspect/user-response.mjs +0 -35
  476. package/dist/commands/inspect/user-response.mjs.map +0 -1
  477. package/dist/commands/inspect/worker-liveness.d.mts +0 -1
  478. package/dist/commands/inspect/worker-liveness.mjs +0 -45
  479. package/dist/commands/inspect/worker-liveness.mjs.map +0 -1
  480. package/dist/commands/lifecycle/contract-check.d.mts +0 -1
  481. package/dist/commands/lifecycle/contract-check.mjs +0 -18
  482. package/dist/commands/lifecycle/contract-check.mjs.map +0 -1
  483. package/dist/commands/lifecycle/migrate.d.mts +0 -1
  484. package/dist/commands/lifecycle/migrate.mjs +0 -30
  485. package/dist/commands/lifecycle/migrate.mjs.map +0 -1
  486. package/dist/commands/lifecycle/model.d.mts +0 -1
  487. package/dist/commands/lifecycle/model.mjs +0 -22
  488. package/dist/commands/lifecycle/model.mjs.map +0 -1
  489. package/dist/commands/manager.d.mts +0 -3
  490. package/dist/commands/manager.mjs +0 -50
  491. package/dist/commands/manager.mjs.map +0 -1
  492. package/dist/commands/report/agent-activity.d.mts +0 -1
  493. package/dist/commands/report/agent-activity.mjs +0 -20
  494. package/dist/commands/report/agent-activity.mjs.map +0 -1
  495. package/dist/commands/report/approval-decision.d.mts +0 -1
  496. package/dist/commands/report/approval-decision.mjs +0 -21
  497. package/dist/commands/report/approval-decision.mjs.map +0 -1
  498. package/dist/commands/report/design-snapshot.d.mts +0 -1
  499. package/dist/commands/report/design-snapshot.mjs +0 -19
  500. package/dist/commands/report/design-snapshot.mjs.map +0 -1
  501. package/dist/commands/report/finalize.d.mts +0 -4
  502. package/dist/commands/report/finalize.mjs +0 -64
  503. package/dist/commands/report/finalize.mjs.map +0 -1
  504. package/dist/commands/report/inject-report-index.d.mts +0 -1
  505. package/dist/commands/report/inject-report-index.mjs +0 -21
  506. package/dist/commands/report/inject-report-index.mjs.map +0 -1
  507. package/dist/commands/report/render-final-report.d.mts +0 -1
  508. package/dist/commands/report/render-final-report.mjs +0 -23
  509. package/dist/commands/report/render-final-report.mjs.map +0 -1
  510. package/dist/commands/report/render-views.d.mts +0 -1
  511. package/dist/commands/report/render-views.mjs +0 -27
  512. package/dist/commands/report/render-views.mjs.map +0 -1
  513. package/dist/commands/report/translate.d.mts +0 -1
  514. package/dist/commands/report/translate.mjs +0 -33
  515. package/dist/commands/report/translate.mjs.map +0 -1
  516. package/runtime/bin/lib/okstra/tmux-pane.sh +0 -40
  517. package/runtime/bin/lib/okstra-ctl/cmd-batch.sh +0 -59
  518. package/runtime/bin/lib/okstra-ctl/cmd-list.sh +0 -35
  519. package/runtime/bin/lib/okstra-ctl/cmd-open.sh +0 -36
  520. package/runtime/bin/lib/okstra-ctl/cmd-projects.sh +0 -26
  521. package/runtime/bin/lib/okstra-ctl/cmd-reconcile.sh +0 -29
  522. package/runtime/bin/lib/okstra-ctl/cmd-reindex.sh +0 -38
  523. package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +0 -345
  524. package/runtime/bin/lib/okstra-ctl/cmd-show.sh +0 -27
  525. package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +0 -92
  526. package/runtime/bin/lib/okstra-ctl/main.sh +0 -41
  527. package/runtime/bin/lib/okstra-ctl/prepare.sh +0 -31
  528. package/runtime/bin/lib/okstra-ctl/usage.sh +0 -23
  529. package/runtime/bin/okstra-central.sh +0 -152
  530. package/runtime/bin/okstra-incremental-carry.py +0 -10
  531. package/runtime/bin/okstra-incremental-scope.py +0 -10
  532. package/runtime/bin/okstra-team-reconcile.sh +0 -36
  533. package/runtime/python/okstra_ctl/agent_prompt_cli.py +0 -1156
  534. package/runtime/python/okstra_ctl/batch.py +0 -60
  535. package/runtime/python/okstra_ctl/clarification_items.py +0 -1050
  536. package/runtime/python/okstra_ctl/container_registry.py +0 -72
  537. package/runtime/python/okstra_ctl/improvement_assignment.py +0 -61
  538. package/runtime/python/okstra_ctl/resolver.py +0 -54
  539. package/runtime/python/okstra_ctl/team_reconcile.py +0 -275
  540. package/runtime/python/okstra_ctl/tmux.py +0 -134
  541. 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 Action, ArgumentError, ArgumentParser, Namespace
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.state import slugify
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 .agent_invocation import (
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,...])` after the "
398
- "`Conformance tests: stage-<N> — ` prefix "
399
- "(prompts/profiles/implementation-planning.md), or carry "
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
- " re-run implementation-planning so the declaration is regenerated in "
402
- "that form, then approve it."
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
- _STAGE_VALIDATOR_PATH = (
611
- Path(__file__).resolve().parents[2] / "validators"
612
- / "validate-implementation-plan-stages.py"
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(_STAGE_VALIDATOR_PATH), "--plan", plan_path],
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 호출. okstra-central.sh 의 bash wrapper 와 같은 동작
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
- # bootstrap (okstra_central_bootstrap 와 동등) — 디렉터리·jsonl 파일 보장.
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"okstra-central: reservation cleanup failed after record_start error ({exc})",
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
- run_validator = workspace_root / "validators" / "validate-run.py"
1009
- brief_validator = workspace_root / "validators" / "validate-brief.py"
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 = task_dir(project_root, inp.task_group, inp.task_id) / "qa" / "conformance-manifest.json"
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, inp.task_type)
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(selection, context, host)
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, inp.task_type)
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
- _CRITIC_REQUIRED_TASK_TYPES = frozenset({
2585
- "requirements-discovery",
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
- def _validate_critic_choice(raw_value: str, task_type: str = "") -> str:
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
- worker_rows = tuple(
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, worker_rows),
3000
+ worker_id=_canonical_worker_id(row, roster_rows),
2644
3001
  )
2645
- for row in worker_rows
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
- prior = sum(
2672
- row.provider_id == assignment.provider_id and row.ordinal < assignment.ordinal
2673
- for row in rows
2674
- )
2675
- return assignment.provider_id if prior == 0 else f"{assignment.provider_id}-{prior + 1}"
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].provider_id,
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 + f"- **Verification target digest:** `{digest}`\n",
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"okstra-central: record_start failed; central index will be incomplete ({exc})",
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
- # Probed once here and reused below, so the gate and the manifest cannot
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 = detect_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
- if _uses_canonical_assignment_projection(inp):
4109
- workers, models = _project_assignment_plan(
4110
- inp,
4111
- assignment_plan,
4112
- assignment_context,
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: okstra-ctl rerun / 테스트 hook 이 미리 reserve
4139
- # 한 seq 를 강제하는 user-knob 환경 변수.
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
- claude_session_id = resolve_inproc_lead_session_id(project_root)
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
- p = ArgumentParser()
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,