okstra 0.186.6 → 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 (548) 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 +487 -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 +32 -7
  232. package/runtime/python/okstra_ctl/report_finalize.py +206 -15
  233. package/runtime/python/okstra_ctl/report_html/common.py +4 -9
  234. package/runtime/python/okstra_ctl/report_html/render.py +11 -12
  235. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +2 -5
  236. package/runtime/python/okstra_ctl/report_language.py +8 -4
  237. package/runtime/python/okstra_ctl/report_narrative.py +90 -3
  238. package/runtime/python/okstra_ctl/report_projections.py +8 -2
  239. package/runtime/python/okstra_ctl/report_synthesis_packet.py +258 -1
  240. package/runtime/python/okstra_ctl/report_translation.py +2 -35
  241. package/runtime/python/okstra_ctl/report_views.py +4 -22
  242. package/runtime/python/okstra_ctl/resolve_task_key.py +13 -0
  243. package/runtime/python/okstra_ctl/rollup.py +13 -0
  244. package/runtime/python/okstra_ctl/run.py +639 -138
  245. package/runtime/python/okstra_ctl/run_audit.py +13 -0
  246. package/runtime/python/okstra_ctl/run_index_row.py +0 -12
  247. package/runtime/python/okstra_ctl/seeding.py +21 -11
  248. package/runtime/python/okstra_ctl/sequence.py +1 -1
  249. package/runtime/python/okstra_ctl/session.py +64 -14
  250. package/runtime/python/okstra_ctl/set_work_status.py +18 -0
  251. package/runtime/python/okstra_ctl/stage_integrate.py +15 -1
  252. package/runtime/python/okstra_ctl/stage_ledger.py +1 -1
  253. package/runtime/python/okstra_ctl/stage_map.py +57 -11
  254. package/runtime/python/okstra_ctl/stage_map_cli.py +83 -0
  255. package/runtime/python/okstra_ctl/stage_map_view.py +65 -0
  256. package/runtime/python/okstra_ctl/stage_targets.py +3 -12
  257. package/runtime/python/okstra_ctl/task_list_cli.py +138 -0
  258. package/runtime/python/okstra_ctl/task_show_cli.py +66 -0
  259. package/runtime/python/okstra_ctl/task_target.py +4 -16
  260. package/runtime/python/okstra_ctl/team.py +49 -2
  261. package/runtime/python/okstra_ctl/time_report.py +17 -2
  262. package/runtime/python/okstra_ctl/usage_report.py +11 -0
  263. package/runtime/python/okstra_ctl/user_response.py +55 -218
  264. package/runtime/python/okstra_ctl/user_response_values.py +242 -0
  265. package/runtime/python/okstra_ctl/validation_contract.py +3 -0
  266. package/runtime/python/okstra_ctl/verification_target.py +68 -0
  267. package/runtime/python/okstra_ctl/wizard.py +457 -92
  268. package/runtime/python/okstra_ctl/worker_artifacts.py +75 -16
  269. package/runtime/python/okstra_ctl/worker_audit_check.py +22 -0
  270. package/runtime/python/okstra_ctl/worker_dispatch.py +21 -0
  271. package/runtime/python/okstra_ctl/worker_liveness.py +33 -0
  272. package/runtime/python/okstra_ctl/worker_prompt_body.py +15 -2
  273. package/runtime/python/okstra_ctl/worker_prompt_contract.py +98 -7
  274. package/runtime/python/okstra_ctl/worker_prompt_headers.py +24 -2
  275. package/runtime/python/okstra_ctl/worker_prompt_policy.py +18 -3
  276. package/runtime/python/okstra_ctl/worker_request.py +2 -1
  277. package/runtime/python/okstra_ctl/worker_runner.py +16 -9
  278. package/runtime/python/okstra_ctl/worker_state.py +16 -1
  279. package/runtime/python/okstra_ctl/workflow.py +18 -1
  280. package/runtime/python/okstra_ctl/worktree/__init__.py +92 -0
  281. package/runtime/python/okstra_ctl/worktree/cleanliness.py +89 -0
  282. package/runtime/python/okstra_ctl/worktree/decisions.py +127 -0
  283. package/runtime/python/okstra_ctl/worktree/git_ops.py +165 -0
  284. package/runtime/python/okstra_ctl/worktree/linking.py +249 -0
  285. package/runtime/python/okstra_ctl/worktree/naming.py +91 -0
  286. package/runtime/python/okstra_ctl/worktree/provision.py +385 -0
  287. package/runtime/python/okstra_ctl/worktree/sync_config.py +181 -0
  288. package/runtime/python/okstra_ctl/worktree_cli.py +75 -0
  289. package/runtime/python/okstra_ctl/worktree_lookup_cli.py +39 -0
  290. package/runtime/python/okstra_ctl/worktree_registry.py +7 -0
  291. package/runtime/python/okstra_ctl/worktree_status_cli.py +53 -0
  292. package/runtime/python/okstra_ctl/write_policy.py +90 -213
  293. package/runtime/python/okstra_project/__init__.py +0 -4
  294. package/runtime/python/okstra_project/dirs.py +2 -2
  295. package/runtime/python/okstra_project/phase_pointer.py +84 -0
  296. package/runtime/python/okstra_project/slug.py +24 -0
  297. package/runtime/python/okstra_project/state.py +22 -230
  298. package/runtime/python/okstra_token_usage/claude.py +9 -1
  299. package/runtime/python/okstra_token_usage/cli.py +3 -1
  300. package/runtime/python/okstra_token_usage/collect.py +71 -10
  301. package/runtime/python/okstra_token_usage/cursor.py +2 -0
  302. package/runtime/python/okstra_token_usage/grok.py +1 -5
  303. package/runtime/python/okstra_token_usage/paths.py +26 -0
  304. package/runtime/python/okstra_token_usage/pricing.py +19 -7
  305. package/runtime/schemas/convergence-critic-results-v1.0.schema.json +5 -0
  306. package/runtime/schemas/execution-manifest-v2.schema.json +10 -10
  307. package/runtime/schemas/final-report-v2.0.schema.json +13 -26
  308. package/runtime/schemas/final-report-v3.0.schema.json +177 -41
  309. package/runtime/schemas/report-narrative-v3.0.schema.json +3 -2
  310. package/runtime/skills/okstra-brief-gen/SKILL.md +35 -2
  311. package/runtime/skills/okstra-container-build/SKILL.md +16 -47
  312. package/runtime/skills/okstra-inspect/facets/history.md +1 -1
  313. package/runtime/skills/okstra-inspect/facets/status.md +7 -6
  314. package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
  315. package/runtime/skills/okstra-run/SKILL.md +7 -5
  316. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  317. package/runtime/templates/reports/brief.template.md +6 -2
  318. package/runtime/templates/reports/error-analysis-input.template.md +2 -0
  319. package/runtime/templates/reports/final-verification-input.template.md +2 -0
  320. package/runtime/templates/reports/html/base.template.html +13 -0
  321. package/runtime/templates/reports/html/i18n/en.json +19 -0
  322. package/runtime/templates/reports/html/i18n/ko.json +19 -0
  323. package/runtime/templates/reports/html/tasks/final-verification.template.html +2 -2
  324. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +19 -9
  325. package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
  326. package/runtime/templates/reports/implementation-input.template.md +7 -1
  327. package/runtime/templates/reports/implementation-planning-input.template.md +4 -0
  328. package/runtime/templates/reports/quick-input.template.md +2 -0
  329. package/runtime/templates/reports/release-handoff-input.template.md +6 -1
  330. package/runtime/templates/reports/report.js +20 -3
  331. package/runtime/templates/reports/schedule.template.md +2 -0
  332. package/runtime/templates/reports/settings.template.json +0 -10
  333. package/runtime/templates/reports/task-brief.template.md +3 -0
  334. package/runtime/templates/reports/user-response.template.md +7 -3
  335. package/runtime/validators/checks/fixtures-01.py +57 -0
  336. package/runtime/validators/checks/fixtures-02.py +565 -0
  337. package/runtime/validators/checks/runners-01.py +108 -0
  338. package/runtime/validators/checks/validate-assets-01.py +60 -0
  339. package/runtime/validators/checks/validate-prompt-metadata-01.py +261 -0
  340. package/runtime/validators/checks/validate-tasks-01.py +46 -0
  341. package/runtime/validators/checks/validate-tasks-02.py +85 -0
  342. package/runtime/validators/checks/validate-tasks-03.py +61 -0
  343. package/runtime/validators/checks/validate-tasks-04.py +118 -0
  344. package/runtime/validators/forbidden_actions.py +73 -3
  345. package/runtime/validators/lib/common.sh +5 -0
  346. package/runtime/validators/lib/fixtures.sh +7 -591
  347. package/runtime/validators/lib/paths.sh +13 -0
  348. package/runtime/validators/lib/runners.sh +6 -104
  349. package/runtime/validators/lib/summary.sh +1 -1
  350. package/runtime/validators/lib/validate-assets.sh +6 -56
  351. package/runtime/validators/lib/validate-prompt-metadata.sh +6 -257
  352. package/runtime/validators/lib/validate-tasks.sh +9 -294
  353. package/runtime/validators/validate-implementation-plan-stages.py +4 -4
  354. package/runtime/validators/validate-run.py +1369 -2654
  355. package/runtime/validators/validate-workflow.sh +56 -16
  356. package/runtime/validators/validate_improvement_report.py +2 -1
  357. package/runtime/validators/validate_session_conformance.py +295 -49
  358. package/dist/commands/execute/agent-prompt.d.mts +0 -1
  359. package/dist/commands/execute/agent-prompt.mjs +0 -24
  360. package/dist/commands/execute/agent-prompt.mjs.map +0 -1
  361. package/dist/commands/execute/codex-dispatch.d.mts +0 -3
  362. package/dist/commands/execute/codex-dispatch.mjs +0 -6
  363. package/dist/commands/execute/codex-dispatch.mjs.map +0 -1
  364. package/dist/commands/execute/codex-run.d.mts +0 -3
  365. package/dist/commands/execute/codex-run.mjs +0 -62
  366. package/dist/commands/execute/codex-run.mjs.map +0 -1
  367. package/dist/commands/execute/convergence.d.mts +0 -1
  368. package/dist/commands/execute/convergence.mjs +0 -38
  369. package/dist/commands/execute/convergence.mjs.map +0 -1
  370. package/dist/commands/execute/error-log.d.mts +0 -1
  371. package/dist/commands/execute/error-log.mjs +0 -18
  372. package/dist/commands/execute/error-log.mjs.map +0 -1
  373. package/dist/commands/execute/git-reconcile.d.mts +0 -1
  374. package/dist/commands/execute/git-reconcile.mjs +0 -30
  375. package/dist/commands/execute/git-reconcile.mjs.map +0 -1
  376. package/dist/commands/execute/handoff.d.mts +0 -1
  377. package/dist/commands/execute/handoff.mjs +0 -31
  378. package/dist/commands/execute/handoff.mjs.map +0 -1
  379. package/dist/commands/execute/incremental-carry.d.mts +0 -1
  380. package/dist/commands/execute/incremental-carry.mjs +0 -20
  381. package/dist/commands/execute/incremental-carry.mjs.map +0 -1
  382. package/dist/commands/execute/incremental-scope.d.mts +0 -1
  383. package/dist/commands/execute/incremental-scope.mjs +0 -29
  384. package/dist/commands/execute/incremental-scope.mjs.map +0 -1
  385. package/dist/commands/execute/integrate-stages.d.mts +0 -1
  386. package/dist/commands/execute/integrate-stages.mjs +0 -24
  387. package/dist/commands/execute/integrate-stages.mjs.map +0 -1
  388. package/dist/commands/execute/pane-title.d.mts +0 -1
  389. package/dist/commands/execute/pane-title.mjs +0 -20
  390. package/dist/commands/execute/pane-title.mjs.map +0 -1
  391. package/dist/commands/execute/plan-items.d.mts +0 -1
  392. package/dist/commands/execute/plan-items.mjs +0 -9
  393. package/dist/commands/execute/plan-items.mjs.map +0 -1
  394. package/dist/commands/execute/plan-validate.d.mts +0 -1
  395. package/dist/commands/execute/plan-validate.mjs +0 -68
  396. package/dist/commands/execute/plan-validate.mjs.map +0 -1
  397. package/dist/commands/execute/plan-verify.d.mts +0 -1
  398. package/dist/commands/execute/plan-verify.mjs +0 -43
  399. package/dist/commands/execute/plan-verify.mjs.map +0 -1
  400. package/dist/commands/execute/spawn-followups.d.mts +0 -1
  401. package/dist/commands/execute/spawn-followups.mjs +0 -22
  402. package/dist/commands/execute/spawn-followups.mjs.map +0 -1
  403. package/dist/commands/execute/team.d.mts +0 -3
  404. package/dist/commands/execute/team.mjs +0 -66
  405. package/dist/commands/execute/team.mjs.map +0 -1
  406. package/dist/commands/execute/token-usage.d.mts +0 -1
  407. package/dist/commands/execute/token-usage.mjs +0 -19
  408. package/dist/commands/execute/token-usage.mjs.map +0 -1
  409. package/dist/commands/execute/worker-audit-check.d.mts +0 -1
  410. package/dist/commands/execute/worker-audit-check.mjs +0 -34
  411. package/dist/commands/execute/worker-audit-check.mjs.map +0 -1
  412. package/dist/commands/execute/worker-dispatch.d.mts +0 -7
  413. package/dist/commands/execute/worker-dispatch.mjs +0 -64
  414. package/dist/commands/execute/worker-dispatch.mjs.map +0 -1
  415. package/dist/commands/execute/worker-state.d.mts +0 -1
  416. package/dist/commands/execute/worker-state.mjs +0 -28
  417. package/dist/commands/execute/worker-state.mjs.map +0 -1
  418. package/dist/commands/execute/worktree-lookup.d.mts +0 -1
  419. package/dist/commands/execute/worktree-lookup.mjs +0 -92
  420. package/dist/commands/execute/worktree-lookup.mjs.map +0 -1
  421. package/dist/commands/execute/worktree-status.d.mts +0 -1
  422. package/dist/commands/execute/worktree-status.mjs +0 -121
  423. package/dist/commands/execute/worktree-status.mjs.map +0 -1
  424. package/dist/commands/inspect/code-review.d.mts +0 -1
  425. package/dist/commands/inspect/code-review.mjs +0 -32
  426. package/dist/commands/inspect/code-review.mjs.map +0 -1
  427. package/dist/commands/inspect/container.d.mts +0 -1
  428. package/dist/commands/inspect/container.mjs +0 -25
  429. package/dist/commands/inspect/container.mjs.map +0 -1
  430. package/dist/commands/inspect/context-cost.d.mts +0 -1
  431. package/dist/commands/inspect/context-cost.mjs +0 -25
  432. package/dist/commands/inspect/context-cost.mjs.map +0 -1
  433. package/dist/commands/inspect/design-prep.d.mts +0 -1
  434. package/dist/commands/inspect/design-prep.mjs +0 -22
  435. package/dist/commands/inspect/design-prep.mjs.map +0 -1
  436. package/dist/commands/inspect/error-report.d.mts +0 -1
  437. package/dist/commands/inspect/error-report.mjs +0 -25
  438. package/dist/commands/inspect/error-report.mjs.map +0 -1
  439. package/dist/commands/inspect/error-zip.d.mts +0 -1
  440. package/dist/commands/inspect/error-zip.mjs +0 -24
  441. package/dist/commands/inspect/error-zip.mjs.map +0 -1
  442. package/dist/commands/inspect/log-report.d.mts +0 -1
  443. package/dist/commands/inspect/log-report.mjs +0 -26
  444. package/dist/commands/inspect/log-report.mjs.map +0 -1
  445. package/dist/commands/inspect/model-io.d.mts +0 -1
  446. package/dist/commands/inspect/model-io.mjs +0 -25
  447. package/dist/commands/inspect/model-io.mjs.map +0 -1
  448. package/dist/commands/inspect/profile-show.d.mts +0 -1
  449. package/dist/commands/inspect/profile-show.mjs +0 -28
  450. package/dist/commands/inspect/profile-show.mjs.map +0 -1
  451. package/dist/commands/inspect/recap.d.mts +0 -1
  452. package/dist/commands/inspect/recap.mjs +0 -30
  453. package/dist/commands/inspect/recap.mjs.map +0 -1
  454. package/dist/commands/inspect/resolve-task-key.d.mts +0 -1
  455. package/dist/commands/inspect/resolve-task-key.mjs +0 -24
  456. package/dist/commands/inspect/resolve-task-key.mjs.map +0 -1
  457. package/dist/commands/inspect/rollup.d.mts +0 -1
  458. package/dist/commands/inspect/rollup.mjs +0 -25
  459. package/dist/commands/inspect/rollup.mjs.map +0 -1
  460. package/dist/commands/inspect/run-audit.d.mts +0 -1
  461. package/dist/commands/inspect/run-audit.mjs +0 -25
  462. package/dist/commands/inspect/run-audit.mjs.map +0 -1
  463. package/dist/commands/inspect/set-work-status.d.mts +0 -1
  464. package/dist/commands/inspect/set-work-status.mjs +0 -29
  465. package/dist/commands/inspect/set-work-status.mjs.map +0 -1
  466. package/dist/commands/inspect/stage-map.d.mts +0 -1
  467. package/dist/commands/inspect/stage-map.mjs +0 -131
  468. package/dist/commands/inspect/stage-map.mjs.map +0 -1
  469. package/dist/commands/inspect/task-list.d.mts +0 -1
  470. package/dist/commands/inspect/task-list.mjs +0 -149
  471. package/dist/commands/inspect/task-list.mjs.map +0 -1
  472. package/dist/commands/inspect/task-show.d.mts +0 -1
  473. package/dist/commands/inspect/task-show.mjs +0 -108
  474. package/dist/commands/inspect/task-show.mjs.map +0 -1
  475. package/dist/commands/inspect/time-report.d.mts +0 -1
  476. package/dist/commands/inspect/time-report.mjs +0 -24
  477. package/dist/commands/inspect/time-report.mjs.map +0 -1
  478. package/dist/commands/inspect/usage-report.d.mts +0 -1
  479. package/dist/commands/inspect/usage-report.mjs +0 -23
  480. package/dist/commands/inspect/usage-report.mjs.map +0 -1
  481. package/dist/commands/inspect/user-response.d.mts +0 -1
  482. package/dist/commands/inspect/user-response.mjs +0 -35
  483. package/dist/commands/inspect/user-response.mjs.map +0 -1
  484. package/dist/commands/inspect/worker-liveness.d.mts +0 -1
  485. package/dist/commands/inspect/worker-liveness.mjs +0 -45
  486. package/dist/commands/inspect/worker-liveness.mjs.map +0 -1
  487. package/dist/commands/lifecycle/contract-check.d.mts +0 -1
  488. package/dist/commands/lifecycle/contract-check.mjs +0 -18
  489. package/dist/commands/lifecycle/contract-check.mjs.map +0 -1
  490. package/dist/commands/lifecycle/migrate.d.mts +0 -1
  491. package/dist/commands/lifecycle/migrate.mjs +0 -30
  492. package/dist/commands/lifecycle/migrate.mjs.map +0 -1
  493. package/dist/commands/lifecycle/model.d.mts +0 -1
  494. package/dist/commands/lifecycle/model.mjs +0 -22
  495. package/dist/commands/lifecycle/model.mjs.map +0 -1
  496. package/dist/commands/manager.d.mts +0 -3
  497. package/dist/commands/manager.mjs +0 -50
  498. package/dist/commands/manager.mjs.map +0 -1
  499. package/dist/commands/report/agent-activity.d.mts +0 -1
  500. package/dist/commands/report/agent-activity.mjs +0 -20
  501. package/dist/commands/report/agent-activity.mjs.map +0 -1
  502. package/dist/commands/report/approval-decision.d.mts +0 -1
  503. package/dist/commands/report/approval-decision.mjs +0 -21
  504. package/dist/commands/report/approval-decision.mjs.map +0 -1
  505. package/dist/commands/report/design-snapshot.d.mts +0 -1
  506. package/dist/commands/report/design-snapshot.mjs +0 -19
  507. package/dist/commands/report/design-snapshot.mjs.map +0 -1
  508. package/dist/commands/report/finalize.d.mts +0 -4
  509. package/dist/commands/report/finalize.mjs +0 -64
  510. package/dist/commands/report/finalize.mjs.map +0 -1
  511. package/dist/commands/report/inject-report-index.d.mts +0 -1
  512. package/dist/commands/report/inject-report-index.mjs +0 -21
  513. package/dist/commands/report/inject-report-index.mjs.map +0 -1
  514. package/dist/commands/report/render-final-report.d.mts +0 -1
  515. package/dist/commands/report/render-final-report.mjs +0 -23
  516. package/dist/commands/report/render-final-report.mjs.map +0 -1
  517. package/dist/commands/report/render-views.d.mts +0 -1
  518. package/dist/commands/report/render-views.mjs +0 -27
  519. package/dist/commands/report/render-views.mjs.map +0 -1
  520. package/dist/commands/report/translate.d.mts +0 -1
  521. package/dist/commands/report/translate.mjs +0 -33
  522. package/dist/commands/report/translate.mjs.map +0 -1
  523. package/runtime/bin/lib/okstra/tmux-pane.sh +0 -40
  524. package/runtime/bin/lib/okstra-ctl/cmd-batch.sh +0 -59
  525. package/runtime/bin/lib/okstra-ctl/cmd-list.sh +0 -35
  526. package/runtime/bin/lib/okstra-ctl/cmd-open.sh +0 -36
  527. package/runtime/bin/lib/okstra-ctl/cmd-projects.sh +0 -26
  528. package/runtime/bin/lib/okstra-ctl/cmd-reconcile.sh +0 -29
  529. package/runtime/bin/lib/okstra-ctl/cmd-reindex.sh +0 -38
  530. package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +0 -345
  531. package/runtime/bin/lib/okstra-ctl/cmd-show.sh +0 -27
  532. package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +0 -92
  533. package/runtime/bin/lib/okstra-ctl/main.sh +0 -41
  534. package/runtime/bin/lib/okstra-ctl/prepare.sh +0 -31
  535. package/runtime/bin/lib/okstra-ctl/usage.sh +0 -23
  536. package/runtime/bin/okstra-central.sh +0 -152
  537. package/runtime/bin/okstra-incremental-carry.py +0 -10
  538. package/runtime/bin/okstra-incremental-scope.py +0 -10
  539. package/runtime/bin/okstra-team-reconcile.sh +0 -36
  540. package/runtime/python/okstra_ctl/agent_prompt_cli.py +0 -1156
  541. package/runtime/python/okstra_ctl/batch.py +0 -60
  542. package/runtime/python/okstra_ctl/clarification_items.py +0 -1050
  543. package/runtime/python/okstra_ctl/container_registry.py +0 -72
  544. package/runtime/python/okstra_ctl/improvement_assignment.py +0 -61
  545. package/runtime/python/okstra_ctl/resolver.py +0 -54
  546. package/runtime/python/okstra_ctl/team_reconcile.py +0 -275
  547. package/runtime/python/okstra_ctl/tmux.py +0 -134
  548. package/runtime/python/okstra_ctl/worktree.py +0 -1099
@@ -28,6 +28,7 @@ import subprocess
28
28
  from dataclasses import asdict, dataclass, field
29
29
  from datetime import datetime
30
30
  from pathlib import Path
31
+ from collections.abc import Mapping
31
32
  from typing import Any, Callable, Optional
32
33
 
33
34
  from okstra_ctl.application.advance_wizard import plan_prompt
@@ -96,6 +97,7 @@ from okstra_ctl.clarification_items import (
96
97
  from okstra_ctl.convergence_store import write_json_atomic
97
98
  from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
98
99
  from okstra_ctl.incremental_scope import (
100
+ CARRY_ALL_SCOPE,
99
101
  parse_stage_graph,
100
102
  preview_link_availability_for_report,
101
103
  )
@@ -242,6 +244,8 @@ _RECOMMENDATION_CAP = 3
242
244
  # Pick-vs-free-text tokens shared by suggestion-aware prompts.
243
245
  PICK_USE_SUGGESTED = "__use_suggested__"
244
246
  PICK_TYPE_CUSTOM = "__free_input__"
247
+ # 취소지 탈출구가 아니다 — 자유 입력 뒤에 올 수 있는 유일한 값.
248
+ _ABORT_OPTION = "__abort__"
245
249
  # workers_override 에서 "옵션 워커를 추가하지 않음" 을 뜻하는 sentinel.
246
250
  _DEFAULT_ROSTER_TOKEN = "__default_roster__"
247
251
  _RECENT_PREFIX = "__recent:"
@@ -590,6 +594,34 @@ class Prompt:
590
594
  # only meaningful when kind == "pick_group": one entry per AskUserQuestion tab
591
595
  questions: list["Prompt"] = field(default_factory=list)
592
596
 
597
+ def __post_init__(self) -> None:
598
+ """자유 입력 선택지는 언제나 마지막이다.
599
+
600
+ 목록이 자유 입력으로 열리면 질문을 통째로 사용자에게 되돌려준 것이다 —
601
+ 앞선 선택지들이 이 run 이 이미 알아낸 것이고, 자유 입력은 그것들이
602
+ 전부 맞지 않을 때의 탈출구다. 실측: 재검증 범위 질문의 `unlinked`
603
+ 분기가 `직접 입력` 을 첫 선택지로 내놓았고, 리드가 preamble 에 해당
604
+ stage 번호를 이미 적어 놓은 화면에서도 사용자는 그 번호를 다시
605
+ 타이핑해야 했다.
606
+
607
+ 중단(abort)만 그 뒤에 올 수 있다. 중단은 탈출구가 아니라 취소이고,
608
+ `brief_carry` 가 실제로 그 모양이다 — 전환(추천) / 직접 입력 / 중단.
609
+
610
+ 배치 지점이 12곳이라 관례로는 지켜지지 않는다. 값을 만드는 자리에서
611
+ 막는다.
612
+ """
613
+ values = [option.value for option in self.options]
614
+ if PICK_TYPE_CUSTOM not in values:
615
+ return
616
+ after = values[values.index(PICK_TYPE_CUSTOM) + 1:]
617
+ offenders = [value for value in after if value != _ABORT_OPTION]
618
+ if offenders:
619
+ raise WizardError(
620
+ f"wizard step {self.step!r}: the free-input option "
621
+ f"({PICK_TYPE_CUSTOM!r}) must come after every real choice — "
622
+ f"only {_ABORT_OPTION!r} may follow it, but {offenders} do"
623
+ )
624
+
593
625
  @property
594
626
  def id(self) -> str:
595
627
  """Stable public identifier shared by static and role-instance prompts."""
@@ -649,8 +681,11 @@ def _require_file(path_str: str, project_root: Path, label: str) -> Path:
649
681
  return p
650
682
 
651
683
 
684
+ # 후보는 리포트 **기록**으로 찾는다. 전체 열람본(`.md`)은 `2bcd575` 이후 완료
685
+ # 산출물이 아니라 요청 시 렌더라, `.md` 를 글롭하는 동안 이 목록은 항상 비었고
686
+ # 방향 선택 단계 자체가 뜨지 않았다 — 비교가 끝나도 계획으로 갈 길이 없었다.
652
687
  _SELECTION_REPORT_RE = re.compile(
653
- r"^final-report-implementation-option-selection-(?P<seq>\d{3,})\.md$"
688
+ r"^final-report-implementation-option-selection-(?P<seq>\d{3,})\.data\.json$"
654
689
  )
655
690
 
656
691
 
@@ -667,7 +702,9 @@ def _selected_direction_candidates(state: WizardState) -> list[str]:
667
702
  / "reports"
668
703
  )
669
704
  candidates: list[tuple[int, Path]] = []
670
- for report in reports.glob("final-report-implementation-option-selection-*.md"):
705
+ for report in reports.glob(
706
+ "final-report-implementation-option-selection-*.data.json"
707
+ ):
671
708
  match = _SELECTION_REPORT_RE.fullmatch(report.name)
672
709
  if match is None:
673
710
  continue
@@ -729,22 +766,55 @@ def _build_selected_direction_pick(state: WizardState) -> Prompt:
729
766
  step=S_SELECTED_DIRECTION_PICK,
730
767
  kind="pick",
731
768
  label=t["label"],
732
- options=[_opt(path, path) for path in candidates],
769
+ options=[
770
+ _opt(path, _selection_option_label(state, path, t))
771
+ for path in candidates
772
+ ],
733
773
  echo_template=t["echo_template"],
734
774
  )
735
775
 
736
776
 
777
+ def _selection_option_label(state: WizardState, rel_path: str, t: dict) -> str:
778
+ """후보 경로에 그 리포트가 실제로 무엇을 담았는지 붙인다.
779
+
780
+ 목록은 최신순이라 후보가 0건인 최근 재실행이 맨 위에 온다. 경로만 보이면
781
+ 사용자가 그것을 고르고, 방향 없는 리포트로 계획을 열려다 막힌다.
782
+ """
783
+ record = Path(state.project_root) / rel_path
784
+ try:
785
+ data = load_owned_object(record, artifact="implementation option data")
786
+ except (OSError, JsonBoundaryError):
787
+ return rel_path
788
+ selection = data.get("implementationOptionSelection")
789
+ if not isinstance(selection, Mapping):
790
+ return rel_path
791
+ rows = selection.get("rankedOptions")
792
+ ids = [
793
+ str(row.get("id"))
794
+ for row in rows
795
+ if isinstance(row, Mapping) and row.get("id")
796
+ ] if isinstance(rows, list) else []
797
+ if not ids:
798
+ return t["labels"]["no_options"].format(path=rel_path)
799
+ return t["labels"]["options"].format(path=rel_path, ids=", ".join(ids))
800
+
801
+
737
802
  def _submit_selected_direction_pick(
738
803
  state: WizardState, value: str
739
804
  ) -> Optional[str]:
805
+ t = _p(state.workspace_root, S_SELECTED_DIRECTION_PICK)
740
806
  candidates = _selected_direction_candidates(state)
741
807
  if value not in candidates:
742
- raise WizardError(
743
- _p(state.workspace_root, S_SELECTED_DIRECTION_PICK)["errors"][
744
- "unknown"
745
- ].format(value=value)
746
- )
808
+ raise WizardError(t["errors"]["unknown"].format(value=value))
747
809
  state.selected_direction_path = value
810
+ # 두 입력은 상호 배타다 — 방향이 정해진 순간 이 런은 새 계획이고,
811
+ # clarification 자리에 남은 값은 render-bundle 이 거절할 이유일 뿐이다.
812
+ # 거절을 여기서 앞당기는 대신 값을 비우고, 무엇이 비워졌는지 echo 로
813
+ # 알린다. 사용자 답변은 손실되지 않는다: 그 사이드카는
814
+ # `--selected-direction` 경로로 계획 런에 첨부된다(`run.py` 참조).
815
+ if state.clarification_response_path:
816
+ state.clarification_response_path = ""
817
+ return t["echo_variants"]["cleared_clarification"].format(value=value)
748
818
  return f"selected-direction: {value}"
749
819
 
750
820
 
@@ -1191,8 +1261,6 @@ def _convert_v1_provider_selections(
1191
1261
  for requirement in profile.roles
1192
1262
  if not requirement.dynamic and requirement.min_count > 0
1193
1263
  }
1194
- if state.host_entry_mode == "spawn-process":
1195
- static_roles.add("leader")
1196
1264
  for role, provider, raw_model in provider_rows:
1197
1265
  if role not in static_roles or not isinstance(provider, str) or not provider:
1198
1266
  continue
@@ -1231,6 +1299,7 @@ def _wizard_state_from_json(payload: dict[str, Any]) -> WizardState:
1231
1299
  if version != 2:
1232
1300
  state.execution_identity_version = 2
1233
1301
  _convert_v1_provider_selections(state, payload)
1302
+ _discard_implicit_leader_selection(state)
1234
1303
  return state
1235
1304
 
1236
1305
 
@@ -1274,6 +1343,24 @@ def _is_role_selection_step(step_id: str) -> bool:
1274
1343
  return step_id.startswith(("role-count:", "role-model:", "role-add:"))
1275
1344
 
1276
1345
 
1346
+ def _discard_implicit_leader_selection(state: WizardState) -> None:
1347
+ """호스트에 고정된 리더의 구형 선택값을 상태에서 제거한다."""
1348
+ state.role_models.pop("leader", None)
1349
+ leader_steps = {
1350
+ step_id
1351
+ for step_id in state.role_selection_order
1352
+ if step_id.startswith("role-model:leader:")
1353
+ }
1354
+ state.role_selection_order = [
1355
+ step_id for step_id in state.role_selection_order
1356
+ if step_id not in leader_steps
1357
+ ]
1358
+ state.answered = [
1359
+ step_id for step_id in state.answered
1360
+ if step_id not in leader_steps
1361
+ ]
1362
+
1363
+
1277
1364
  def _selected_role_count(
1278
1365
  state: WizardState,
1279
1366
  requirement: RoleRequirement,
@@ -1292,16 +1379,12 @@ def _selectable_static_requirements(
1292
1379
  state: WizardState,
1293
1380
  profile: RoleProfile,
1294
1381
  ) -> tuple[RoleRequirement, ...]:
1295
- requirements: list[RoleRequirement] = []
1296
- if state.host_entry_mode == "spawn-process":
1297
- requirements.append(RoleRequirement("leader", 1, 1, 1, "lead"))
1298
- requirements.extend(
1382
+ return tuple(
1299
1383
  requirement
1300
1384
  for requirement in profile.roles
1301
1385
  if not requirement.dynamic
1302
1386
  and _selected_role_count(state, requirement) > 0
1303
1387
  )
1304
- return tuple(requirements)
1305
1388
 
1306
1389
 
1307
1390
  def _count_prompt(
@@ -1354,6 +1437,14 @@ def _role_add_prompt(
1354
1437
  maximum=str(requirement.max_count),
1355
1438
  )
1356
1439
  suffix = prompt["options"].get("default_suffix", "")
1440
+ # 역할을 빼면 그 역할이 맡던 판정이 사라진다. 그 결과를 아는 역할만 0 옵션에
1441
+ # 경고를 단다 — 예: critic 이 없으면 분석자 동수를 가를 주체가 없다.
1442
+ skip_warnings = prompt["options"].get("skip_warnings", {})
1443
+ skip_warning = (
1444
+ skip_warnings.get(requirement.role, "")
1445
+ if isinstance(skip_warnings, dict)
1446
+ else ""
1447
+ )
1357
1448
 
1358
1449
  def _default_suffix(count: int) -> str:
1359
1450
  return suffix if count == requirement.recommended_count else ""
@@ -1361,7 +1452,10 @@ def _role_add_prompt(
1361
1452
  options = [
1362
1453
  _opt(
1363
1454
  "0",
1364
- prompt["options"]["skip"].format(default_suffix=_default_suffix(0)),
1455
+ prompt["options"]["skip"].format(
1456
+ default_suffix=_default_suffix(0),
1457
+ skip_warning=skip_warning,
1458
+ ),
1365
1459
  ),
1366
1460
  ]
1367
1461
  for count in range(1, requirement.max_count + 1):
@@ -1984,10 +2078,12 @@ def _p(workspace_root: str, step_id: str, **vars: str) -> dict:
1984
2078
  label_template = raw.get("label", "")
1985
2079
  fv_label_template = raw.get("label_final_verification", "")
1986
2080
  unlinked_template = raw.get("label_unlinked", "")
2081
+ no_answers_template = raw.get("label_no_answers", "")
1987
2082
  try:
1988
2083
  label = label_template.format(**vars)
1989
2084
  label_final_verification = fv_label_template.format(**vars)
1990
2085
  label_unlinked = unlinked_template.format(**vars)
2086
+ label_no_answers = no_answers_template.format(**vars)
1991
2087
  except KeyError as exc:
1992
2088
  missing = exc.args[0] if exc.args else "<unknown>"
1993
2089
  raise WizardError(
@@ -1997,6 +2093,7 @@ def _p(workspace_root: str, step_id: str, **vars: str) -> dict:
1997
2093
  "label": label,
1998
2094
  "label_final_verification": label_final_verification,
1999
2095
  "label_unlinked": label_unlinked,
2096
+ "label_no_answers": label_no_answers,
2000
2097
  "echo_template": raw.get("echo_template", ""),
2001
2098
  "options": raw.get("options", {}),
2002
2099
  "options_final_verification": raw.get("options_final_verification", {}),
@@ -2304,7 +2401,7 @@ def _submit_task_pick(state: WizardState, value: str) -> Optional[str]:
2304
2401
  # task_type seeded from manifest's nextRecommendedPhase as the recommended default
2305
2402
  root = find_task_root(Path(state.project_root), value)
2306
2403
  manifest = (read_task_manifest(root) or {}) if root else {}
2307
- state.task_type = next_phase.autofill_task_type(manifest)
2404
+ state.task_type = _recommended_task_type(state, manifest)
2308
2405
  return f"task: {value}"
2309
2406
 
2310
2407
 
@@ -2592,20 +2689,81 @@ def _submit_task_id_text(state: WizardState, value: str) -> Optional[str]:
2592
2689
  return f"task-id: {state.task_id}"
2593
2690
 
2594
2691
 
2692
+ def _existing_task_manifest(state: WizardState) -> dict[str, Any]:
2693
+ """현재 task-key 의 task-manifest. 없으면 {}."""
2694
+ if not (state.project_id and state.task_group and state.task_id):
2695
+ return {}
2696
+ key = f"{state.project_id}:{state.task_group}:{state.task_id}"
2697
+ root = find_task_root(Path(state.project_root), key)
2698
+ if root is None:
2699
+ return {}
2700
+ manifest = read_task_manifest(root) or {}
2701
+ return manifest if isinstance(manifest, dict) else {}
2702
+
2703
+
2595
2704
  def _existing_task_workflow(state: WizardState) -> dict:
2596
2705
  """현재 task-key 가 이미 존재하면 그 manifest 의 workflow dict 를 반환한다.
2597
2706
 
2598
2707
  picker 로 기존 task 를 고른 경우뿐 아니라, new-task 흐름으로 같은
2599
2708
  task-group/task-id 를 다시 입력한 경우(=사실상 이어가기)에도 직전 phase
2600
2709
  기반 추천이 끊기지 않게 하는 안전장치. 없으면 {}."""
2601
- if not (state.project_id and state.task_group and state.task_id):
2710
+ workflow = _existing_task_manifest(state).get("workflow") or {}
2711
+ return workflow if isinstance(workflow, dict) else {}
2712
+
2713
+
2714
+ def _report_data_for_pointer(
2715
+ state: WizardState, manifest: Mapping[str, Any],
2716
+ ) -> dict[str, Any]:
2717
+ rel = manifest.get("latestReportRecordPath")
2718
+ if not isinstance(rel, str) or not rel.strip():
2602
2719
  return {}
2603
- key = f"{state.project_id}:{state.task_group}:{state.task_id}"
2604
- root = find_task_root(Path(state.project_root), key)
2605
- if root is None:
2720
+ path = Path(rel)
2721
+ if not path.is_absolute():
2722
+ path = Path(state.project_root) / rel
2723
+ try:
2724
+ data = load_owned_object(path, artifact="latest report record")
2725
+ except (JsonBoundaryError, OSError):
2606
2726
  return {}
2607
- workflow = (read_task_manifest(root) or {}).get("workflow") or {}
2608
- return workflow if isinstance(workflow, dict) else {}
2727
+ return data if isinstance(data, dict) else {}
2728
+
2729
+
2730
+ def _recommended_task_type(
2731
+ state: WizardState, manifest: Mapping[str, Any],
2732
+ ) -> str:
2733
+ """매니페스트 포인터가 ready 면 그 phase, 끝난 계획의 낡은 pending 이면 리포트 투영."""
2734
+ filled = next_phase.autofill_task_type(manifest)
2735
+ if filled:
2736
+ return filled
2737
+ workflow = manifest.get("workflow")
2738
+ if not isinstance(workflow, Mapping):
2739
+ return ""
2740
+ if (
2741
+ workflow.get("currentPhase") != "implementation-planning"
2742
+ or workflow.get("currentPhaseState") != "completed"
2743
+ ):
2744
+ return ""
2745
+ pointer = next_phase.project(_report_data_for_pointer(state, manifest))
2746
+ if pointer["status"] != next_phase.STATUS_READY:
2747
+ return ""
2748
+ return pointer["phase"]
2749
+
2750
+
2751
+ def _implementation_needs_approval(
2752
+ state: WizardState,
2753
+ manifest: Mapping[str, Any],
2754
+ workflow: Mapping[str, Any],
2755
+ ) -> bool:
2756
+ if workflow.get("awaitingApproval") is True:
2757
+ return True
2758
+ if (
2759
+ workflow.get("currentPhase") != "implementation-planning"
2760
+ or workflow.get("currentPhaseState") != "completed"
2761
+ ):
2762
+ return False
2763
+ frontmatter = _report_data_for_pointer(state, manifest).get("frontmatter")
2764
+ if not isinstance(frontmatter, dict):
2765
+ return True
2766
+ return frontmatter.get("approved") is not True
2609
2767
 
2610
2768
 
2611
2769
  def _phase_after(task_type: str) -> str:
@@ -2748,7 +2906,8 @@ def _build_task_type(state: WizardState) -> Prompt:
2748
2906
  options.append(_opt(task_type, f"{task_type}{suffix}",
2749
2907
  description_by_type[task_type]))
2750
2908
 
2751
- workflow = _existing_task_workflow(state)
2909
+ manifest = _existing_task_manifest(state)
2910
+ workflow = manifest.get("workflow") if isinstance(manifest.get("workflow"), dict) else {}
2752
2911
  revision_requested = _latest_revision_requested_analysis_type(state)
2753
2912
  # 포인터에서 phase 를 꺼내는 것은 `status` 가 `ready` 일 때뿐이다. prepare 는
2754
2913
  # 실행이 끝나지 않은 run 의 포인터를 일부러 `pending` 으로 내려두는데
@@ -2760,15 +2919,18 @@ def _build_task_type(state: WizardState) -> Prompt:
2760
2919
  # `ready` 가 아니면 추천은 비고, 아래 `currentPhase` 재실행 옵션이 남는다.
2761
2920
  # 실패한 run 의 포인터가 `{"phase": "", "status": "blocked"}` 라는 점에서
2762
2921
  # 그것이 맞는 제안이다 — 그 옵션은 포인터가 아니라 `currentPhase` 에서 온다.
2763
- # 계획 승인 대기는 `awaitingApproval` 로 표시한다. 구현이 추천이지만 먼저
2764
- # 승인을 받아야 하므로 접미사로 구분한다. 열린 C-NNN 때문에 blocked 면
2765
- # 재실행은 답이 기록된 뒤에만 고르라고 접미사로 말한다.
2922
+ # 끝난 계획의 낡은 pending 은 최신 리포트에서 다시 투영한다. 승인 대기는
2923
+ # `awaitingApproval` 또는 미승인 계획 리포트로 접미사를 붙인다. 열린
2924
+ # C-NNN 때문에 blocked 면 재실행은 답이 기록된 뒤에만 고르라고 말한다.
2766
2925
  recommended = (revision_requested or state.task_type
2767
- or next_phase.autofill_task_type({"workflow": workflow}))
2926
+ or _recommended_task_type(state, manifest))
2768
2927
  if not recommended and not workflow:
2769
2928
  recommended = TASK_TYPE_VALUES[0]
2770
2929
  pointer = next_phase.promote(workflow.get("nextRecommendedPhase"))
2771
- if workflow.get("awaitingApproval") is True and recommended == "implementation":
2930
+ if (
2931
+ recommended == "implementation"
2932
+ and _implementation_needs_approval(state, manifest, workflow)
2933
+ ):
2772
2934
  recommended_suffix = approve_suffix
2773
2935
  if (
2774
2936
  pointer["status"] == next_phase.STATUS_BLOCKED
@@ -2955,7 +3117,7 @@ def _submit_brief_path(state: WizardState, value: str) -> Optional[str]:
2955
3117
 
2956
3118
 
2957
3119
  BRIEF_CARRY_SWITCH_ENTRY = "__switch_entry__"
2958
- BRIEF_CARRY_ABORT = "__abort__"
3120
+ BRIEF_CARRY_ABORT = _ABORT_OPTION
2959
3121
 
2960
3122
 
2961
3123
  def _build_brief_carry(state: WizardState) -> Prompt:
@@ -3431,19 +3593,25 @@ def _design_prep_items_by_id(state: WizardState) -> dict[str, dict[str, Any]]:
3431
3593
  return {str(item["id"]): item for item in items}
3432
3594
 
3433
3595
 
3434
- def _ensure_design_prep_queue(state: WizardState) -> None:
3435
- if state.design_prep_current or state.design_prep_queue:
3436
- return
3437
- if S_DESIGN_PREP_DECISION in state.answered:
3438
- return
3596
+ def _selected_impl_stage_numbers(state: WizardState) -> set[int]:
3597
+ numbers: set[int] = set()
3598
+ for token in (state.selected_stages or state.selected_stage or "").split(","):
3599
+ token = token.strip()
3600
+ if token.isdigit():
3601
+ numbers.add(int(token))
3602
+ return numbers
3603
+
3604
+
3605
+ def _blocking_unanswered_prep_ids(state: WizardState) -> list[str] | None:
3606
+ """고른 스테이지에서 구현을 막는 unanswered blocked id. 로드 불가면 None."""
3439
3607
  report_path = Path(state.approved_plan_path)
3440
3608
  if not re.fullmatch(
3441
3609
  r"final-report-implementation-planning-\d+\.data\.json",
3442
3610
  report_path.name,
3443
3611
  ):
3444
- return
3612
+ return None
3445
3613
  if not report_path.is_file():
3446
- return
3614
+ return None
3447
3615
  try:
3448
3616
  items = load_design_prep_items(report_path)
3449
3617
  effective = resolve_design_prep(report_path)
@@ -3453,13 +3621,48 @@ def _ensure_design_prep_queue(state: WizardState) -> None:
3453
3621
  str(item["id"]): item.get("decision")
3454
3622
  for item in effective.effective_items
3455
3623
  }
3456
- unresolved = [
3457
- item for item in items
3458
- if item.get("status") in ("blocked", "provisional")
3624
+ stages = _selected_impl_stage_numbers(state)
3625
+ # provisional 미응답은 구현을 막지 않는다. 작업 가정을 그대로 쓴다.
3626
+ return [
3627
+ str(item["id"])
3628
+ for item in items
3629
+ if item.get("status") == "blocked"
3459
3630
  and decisions.get(str(item["id"])) == "unanswered"
3631
+ and (
3632
+ not stages
3633
+ or stages.intersection(
3634
+ stage for stage in (item.get("stageRefs") or [])
3635
+ if isinstance(stage, int)
3636
+ )
3637
+ )
3638
+ ]
3639
+
3640
+
3641
+ def _prune_design_prep_session(state: WizardState, wanted: list[str]) -> None:
3642
+ """이미 만든 큐에서 더 이상 막지 않는 id 를 뺀다. 새 id 는 넣지 않는다."""
3643
+ wanted_set = set(wanted)
3644
+ if state.design_prep_current and state.design_prep_current not in wanted_set:
3645
+ state.design_prep_current = ""
3646
+ state.design_prep_decision = ""
3647
+ state.design_prep_overrides_json = ""
3648
+ state.design_prep_notes = ""
3649
+ state.design_prep_queue = [
3650
+ item_id for item_id in state.design_prep_queue if item_id in wanted_set
3460
3651
  ]
3461
- unresolved.sort(key=lambda item: item.get("status") != "blocked")
3462
- state.design_prep_queue = [str(item["id"]) for item in unresolved]
3652
+ if not state.design_prep_current:
3653
+ _advance_design_prep_item(state)
3654
+
3655
+
3656
+ def _ensure_design_prep_queue(state: WizardState) -> None:
3657
+ wanted = _blocking_unanswered_prep_ids(state)
3658
+ if wanted is None:
3659
+ return
3660
+ if state.design_prep_current or state.design_prep_queue:
3661
+ _prune_design_prep_session(state, wanted)
3662
+ return
3663
+ if S_DESIGN_PREP_DECISION in state.answered:
3664
+ return
3665
+ state.design_prep_queue = list(wanted)
3463
3666
  _advance_design_prep_item(state)
3464
3667
 
3465
3668
 
@@ -3579,6 +3782,8 @@ def _submit_design_prep_overrides(
3579
3782
 
3580
3783
 
3581
3784
  def _design_prep_confirm_applies(state: WizardState) -> bool:
3785
+ if state.task_type == "implementation" and state.approved_plan_path:
3786
+ _ensure_design_prep_queue(state)
3582
3787
  if state.design_prep_decision == "accept-draft":
3583
3788
  return True
3584
3789
  if state.design_prep_decision == "modify-draft":
@@ -3916,7 +4121,12 @@ def _suggest_latest_final_report(state: WizardState) -> str:
3916
4121
  f"{seg}/reports/final-report-*.data.json",
3917
4122
  Path(state.project_root),
3918
4123
  )
3919
- if best is None:
4124
+ # implementation-planning 의 `--clarification-response` 는 같은 phase 의
4125
+ # 계획서만 받는다(`run._validate_planning_entry_inputs`). 그 phase 에 아직
4126
+ # 리포트가 없으면 이 런은 새 계획이고, 답은 `--selected-direction` 으로
4127
+ # 들어간다 — 전체 phase 폴백은 여기서 직전 후보비교 리포트를 추천했고,
4128
+ # 그것을 고른 사용자는 두 상호 배타 입력을 동시에 갖게 됐다.
4129
+ if best is None and state.task_type != "implementation-planning":
3920
4130
  best = _newest_contained_final_report(
3921
4131
  runs_base,
3922
4132
  task_root,
@@ -4148,16 +4358,58 @@ def _reverify_scope_preview(state: WizardState) -> Optional[dict]:
4148
4358
  )
4149
4359
 
4150
4360
 
4151
- def _reverify_scope_pick_required(state: WizardState) -> bool:
4152
- """좁힐 여지가 있거나, unlinked id 의 stage 번호를 받아야 할 때 묻는다.
4361
+ SCOPE_SHAPE_LINKED = "linked"
4362
+ SCOPE_SHAPE_UNLINKED = "unlinked"
4363
+ SCOPE_SHAPE_NO_ANSWERS = "no-answers"
4364
+
4365
+
4366
+ def _reverify_scope_shape(state: WizardState) -> str:
4367
+ """이 재실행에서 되짚기(back-trace)가 무엇을 내놓았는가.
4153
4368
 
4154
- unlinked 는 full 확정이 아니다. SHA 가 바뀌었거나 입력을 읽을 수 없어
4155
- `wouldForceFull` 인 경우만 질문이 의미 없다.
4369
+ 셋은 서로 다른 picker 를 요구한다 — `linked` 는 자동 판정이 실제로 좁힐 수
4370
+ 있고, 나머지 둘은 자동이 낼 수 있는 답이 full 뿐이라 추천으로 내놓으면
4371
+ 거짓말이 된다.
4156
4372
  """
4157
- preview = _reverify_scope_preview(state)
4158
- return preview is not None and (
4159
- not preview["wouldForceFull"] or bool(preview["unlinkedIds"])
4160
- )
4373
+ preview = _reverify_scope_preview(state) or {}
4374
+ if preview.get("unlinkedIds"):
4375
+ return SCOPE_SHAPE_UNLINKED
4376
+ if preview.get("wouldForceFull"):
4377
+ return SCOPE_SHAPE_NO_ANSWERS
4378
+ return SCOPE_SHAPE_LINKED
4379
+
4380
+
4381
+ def _prior_stage_map_readable(state: WizardState) -> bool:
4382
+ """직전 리포트의 Stage Map 을 읽을 수 있는가 — 번호를 검증할 수 있는가."""
4383
+ try:
4384
+ return bool(_prior_stage_numbers(state))
4385
+ except WizardError:
4386
+ return False
4387
+
4388
+
4389
+ def _reverify_scope_pick_required(state: WizardState) -> bool:
4390
+ """직전 리포트를 읽을 수 있으면 묻는다.
4391
+
4392
+ 종전에는 `preview_link_availability` 의 `wouldForceFull` 이 참이면 묻지
4393
+ 않았다. 그 값은 답변된 id 가 0건일 때도 참인데, 그 경우야말로 질문이 필요한
4394
+ 경우다 — `--impacted` 는 답변과 **독립된 입력**이고
4395
+ `incremental_scope._decision_for_run` 은 answered 가 비어도 impacted 만으로
4396
+ `incremental` 을 낸다. 열린 항목이 하나도 남지 않은 리포트를 재실행하면서
4397
+ 한 stage 만 다시 보고 싶은 경우가 정확히 그 형태이고, 종전 게이트는 그때
4398
+ 질문을 없애 사용자가 CLI 플래그를 직접 쓰는 것 말고는 길이 없게 만들었다.
4399
+
4400
+ 남는 배제 사유는 하나 — Stage Map 을 못 읽으면 입력받은 번호를 검증할 수단이
4401
+ 없다(`_prior_stage_numbers` 가 드는 `no_stage_map`). 그때만 묻지 않는다.
4402
+
4403
+ **base ref(C1)는 여기서 안 본다**: `preview_link_availability` 는 이름 그대로
4404
+ SHA 없이 판정하고, 현재 base SHA 는 `render-bundle` 이 매니페스트를 쓴 뒤에야
4405
+ 존재한다. base ref 가 움직인 재실행에서도 질문은 그대로 나오고, full 판정은
4406
+ 나중에 `decide_scope` 가 내린다.
4407
+ """
4408
+ if _reverify_scope_preview(state) is None:
4409
+ return False
4410
+ if _reverify_scope_shape(state) != SCOPE_SHAPE_NO_ANSWERS:
4411
+ return True
4412
+ return _prior_stage_map_readable(state)
4161
4413
 
4162
4414
 
4163
4415
  def _reverify_scope_step_pending(state: WizardState) -> bool:
@@ -4190,26 +4442,77 @@ def _prior_stage_numbers(state: WizardState) -> set[int]:
4190
4442
  return stages
4191
4443
 
4192
4444
 
4445
+ _SCOPE_SHAPE_LABEL_KEY = {
4446
+ SCOPE_SHAPE_UNLINKED: "label_unlinked",
4447
+ SCOPE_SHAPE_NO_ANSWERS: "label_no_answers",
4448
+ }
4449
+
4450
+
4193
4451
  def _build_reverify_scope_pick(state: WizardState) -> Prompt:
4194
4452
  t = _p(state.workspace_root, "reverify_scope_pick")
4195
4453
  opts = t["options"]
4196
- preview = _reverify_scope_preview(state) or {}
4197
- unlinked = bool(preview.get("unlinkedIds"))
4198
- options = []
4199
- if not unlinked:
4200
- options.append(_opt("auto", opts["auto"]))
4201
- options.append(_opt("full", opts["full"]))
4202
- options.append(_opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]))
4454
+ shape = _reverify_scope_shape(state)
4455
+ if shape == SCOPE_SHAPE_LINKED:
4456
+ options = [
4457
+ _opt("auto", opts["auto"]),
4458
+ _opt("full", opts["full"]),
4459
+ _opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]),
4460
+ ]
4461
+ # 아래 두 모양에서 `auto` 를 빼는 이유는 같다 — 되짚기가 stage 를 못 냈으므로
4462
+ # 그 핀은 CLI 에 빈 `--impacted` 를 보내고 full(또는 `unresolved`)로만
4463
+ # 돌아온다. 고를 수 있는 것처럼 두면 추천이 아니라 함정이 된다.
4464
+ #
4465
+ # `carry-all` 이 그 자리를 메운다. stage 를 **추가만** 하는 재실행에서는
4466
+ # 지정할 번호가 없어 직접 입력도 쓸 수 없고, 남는 선택이 `full` 뿐이면
4467
+ # 이월해도 되는 stage 를 전부 다시 교차검증하는 비용을 치른다.
4468
+ elif shape == SCOPE_SHAPE_UNLINKED:
4469
+ # 되짚기가 stage 를 못 냈으므로 좁힐 근거가 없다 — 이 모양에서 안전한
4470
+ # 기본값은 full 이고, 그것이 추천이다. 직접 입력은 마지막이다: 목록이
4471
+ # 직접 입력으로 열리면 질문을 통째로 되돌려준 것이고, 실측에서 그
4472
+ # 화면은 리드가 preamble 에 stage 번호를 이미 적어 놓고도 자유 입력만
4473
+ # 내놓았다.
4474
+ options = [
4475
+ _opt("full", opts["full_recommended"]),
4476
+ _opt(CARRY_ALL_SCOPE, opts[CARRY_ALL_SCOPE]),
4477
+ _opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]),
4478
+ ]
4203
4479
  else:
4204
- options.append(_opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]))
4205
- options.append(_opt("full", opts["full"]))
4206
- label = t["label_unlinked"] if unlinked else t["label"]
4480
+ # 답변된 clarification 이 0건인 재실행. 사용자가 목적을 갖고 stage 를
4481
+ # 지정하러 온 자리라 full 을 추천으로 올리면 비싼 쪽으로 몰게 된다 —
4482
+ # 추천 없이 두고, 근거가 없다는 사실을 라벨이 말한다.
4483
+ options = [
4484
+ _opt(CARRY_ALL_SCOPE, opts[CARRY_ALL_SCOPE]),
4485
+ _opt("full", opts["full"]),
4486
+ _opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]),
4487
+ ]
4488
+ label = t[_SCOPE_SHAPE_LABEL_KEY.get(shape, "label")] + _scope_pick_context(state, shape, t)
4207
4489
  return Prompt(
4208
4490
  step=S_REVERIFY_SCOPE_PICK, kind="pick", label=label,
4209
4491
  options=options,
4210
4492
  echo_template=t["echo_template"])
4211
4493
 
4212
4494
 
4495
+ def _scope_pick_context(state: WizardState, shape: str, t: dict) -> str:
4496
+ """질문이 왜 나왔는지 화면에 싣는 한 문장.
4497
+
4498
+ `unlinked` 는 어느 id 가 연결되지 않았는지가 답을 고르는 근거인데, 종전에는
4499
+ 그 목록이 `errors.unlinked_auto`(자동을 고른 뒤에야 보이는 문구)에만 있었다.
4500
+ id 없이 "연결되지 않은 것이 있습니다" 만 받은 사용자는 자기 답변 중 무엇을
4501
+ 말하는지 모른 채 stage 번호를 대야 했다.
4502
+
4503
+ `no-answers` 는 추천할 근거가 없다는 사실 자체를 말한다 — 근거 없이 한
4504
+ 선택지를 추천으로 꾸미지 않는다.
4505
+ """
4506
+ labels = t.get("labels") or {}
4507
+ if shape == SCOPE_SHAPE_UNLINKED:
4508
+ ids = (_reverify_scope_preview(state) or {}).get("unlinkedIds") or []
4509
+ note = labels.get("unlinked_ids", "")
4510
+ return note.format(ids=", ".join(str(i) for i in ids)) if note and ids else ""
4511
+ if shape == SCOPE_SHAPE_NO_ANSWERS:
4512
+ return labels.get("no_basis", "")
4513
+ return ""
4514
+
4515
+
4213
4516
  def _submit_reverify_scope_pick(state: WizardState, value: str) -> Optional[str]:
4214
4517
  t = _p(state.workspace_root, "reverify_scope_pick")
4215
4518
  picked = value.strip().lower()
@@ -4217,22 +4520,40 @@ def _submit_reverify_scope_pick(state: WizardState, value: str) -> Optional[str]
4217
4520
  state.reverify_scope = ""
4218
4521
  state.reverify_scope_pending_text = True
4219
4522
  return "reverify-scope: 직접 입력"
4220
- if picked not in ("auto", "full"):
4523
+ if picked not in ("auto", "full", CARRY_ALL_SCOPE):
4221
4524
  raise WizardError(
4222
- f"expected 'auto' / 'full' / {PICK_TYPE_CUSTOM!r}, got: {value!r}"
4525
+ f"expected 'auto' / 'full' / {CARRY_ALL_SCOPE!r} / "
4526
+ f"{PICK_TYPE_CUSTOM!r}, got: {value!r}"
4223
4527
  )
4224
4528
  if picked == "auto":
4225
- preview = _reverify_scope_preview(state) or {}
4226
- unlinked = preview.get("unlinkedIds") or []
4227
- if unlinked:
4228
- raise WizardError(
4229
- t["errors"]["unlinked_auto"].format(ids=", ".join(unlinked))
4230
- )
4529
+ _refuse_auto_without_a_back_trace(state, t)
4231
4530
  state.reverify_scope = picked
4232
4531
  state.reverify_scope_pending_text = False
4233
4532
  return t["echo_suffixes"][picked]
4234
4533
 
4235
4534
 
4535
+ def _refuse_auto_without_a_back_trace(
4536
+ state: WizardState,
4537
+ t: dict,
4538
+ *,
4539
+ auto_key: str = "unlinked_auto",
4540
+ no_answers_key: str = "no_answers_auto",
4541
+ ) -> None:
4542
+ """되짚기가 stage 를 못 낸 재실행에서 `auto` 를 거절한다.
4543
+
4544
+ 두 모양의 사유가 다르므로 문구도 다르다 — 연결 안 된 id 는 그 id 를 이름으로
4545
+ 말해 줘야 사용자가 무엇의 stage 를 지정할지 알고, 답변이 아예 없는 경우에는
4546
+ 말할 id 자체가 없다. 두 진입점(picker 의 `auto`, stage 입력의 빈 줄)이 같은
4547
+ 판정을 쓰도록 한곳에 둔다.
4548
+ """
4549
+ shape = _reverify_scope_shape(state)
4550
+ if shape == SCOPE_SHAPE_UNLINKED:
4551
+ unlinked = (_reverify_scope_preview(state) or {}).get("unlinkedIds") or []
4552
+ raise WizardError(t["errors"][auto_key].format(ids=", ".join(unlinked)))
4553
+ if shape == SCOPE_SHAPE_NO_ANSWERS:
4554
+ raise WizardError(t["errors"][no_answers_key])
4555
+
4556
+
4236
4557
  def _build_reverify_scope_stages(state: WizardState) -> Prompt:
4237
4558
  t = _p(state.workspace_root, "reverify_scope_stages")
4238
4559
  return Prompt(
@@ -4244,12 +4565,11 @@ def _submit_reverify_scope_stages(state: WizardState, value: str) -> Optional[st
4244
4565
  t = _p(state.workspace_root, "reverify_scope_stages")
4245
4566
  tokens = [token.strip() for token in value.split(",") if token.strip()]
4246
4567
  if not tokens:
4247
- preview = _reverify_scope_preview(state) or {}
4248
- unlinked = preview.get("unlinkedIds") or []
4249
- if unlinked:
4250
- raise WizardError(
4251
- t["errors"]["unlinked_empty"].format(ids=", ".join(unlinked))
4252
- )
4568
+ # 빈 줄은 `auto` 로 떨어지므로 picker 의 `auto` 와 같은 배제가 걸린다 —
4569
+ # 여기만 열어 두면 picker 에서 막은 핀이 뒷문으로 들어온다.
4570
+ _refuse_auto_without_a_back_trace(
4571
+ state, t, auto_key="unlinked_empty", no_answers_key="no_answers_empty"
4572
+ )
4253
4573
  state.reverify_scope = "auto"
4254
4574
  state.reverify_scope_pending_text = False
4255
4575
  return t["echo_suffixes"]["auto"]
@@ -4502,8 +4822,24 @@ def _build_reuse_previous(state: WizardState) -> Prompt:
4502
4822
 
4503
4823
 
4504
4824
  def _restore_reused_lead(state: WizardState, inputs: dict) -> None:
4505
- requested_provider = _safe_provider(inputs.get("leadProvider"), "lead")
4825
+ """리드는 지금 host 에서 파생한다. 직전 run 의 기록을 물려받지 않는다.
4826
+
4827
+ run-inputs 의 ``leadProvider`` 는 그 run 이 어느 host 에서 돌았는지의 기록이지
4828
+ 사용자가 고른 값이 아니다. wizard 에는 리드 provider 를 묻는 단계 자체가 없고
4829
+ ``resolve_lead_provider`` 가 host 의 네이티브 provider 로 파생한다. 그 기록을
4830
+ 그대로 요청으로 되먹이면 Codex 에서 돌린 run 을 Claude Code 에서 이어받을 때
4831
+ host 검사에 걸려 brief/workers/모델 복원까지 통째로 막힌다.
4832
+
4833
+ 네이티브 리드가 없는 host(``external``) 에서만 기록된 값이 유일한 단서이므로
4834
+ 그때만 참고한다.
4835
+ """
4506
4836
  try:
4837
+ host = default_host_registry().resolve(state.host_runtime).descriptor
4838
+ requested_provider = (
4839
+ ""
4840
+ if host.native_provider_id
4841
+ else _safe_provider(inputs.get("leadProvider"), "lead")
4842
+ )
4507
4843
  assignment = resolve_lead_provider(
4508
4844
  host_id=state.host_runtime,
4509
4845
  requested_provider=requested_provider,
@@ -5410,6 +5746,12 @@ STEPS: list[Step] = [
5410
5746
  and S_APPROVE_PLAN_CONFIRM not in s.answered),
5411
5747
  build=_build_approve_plan_confirm, submit=_submit_approve_plan_confirm,
5412
5748
  owns=("approve_plan_candidate",)),
5749
+ Step(S_STAGE_PICK,
5750
+ applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
5751
+ and bool(s.approved_plan_path)
5752
+ and S_STAGE_PICK not in s.answered),
5753
+ build=_build_stage_pick, submit=_submit_stage_pick,
5754
+ owns=("selected_stage", "selected_stages")),
5413
5755
  Step(S_DESIGN_PREP_DECISION,
5414
5756
  applies=_design_prep_decision_applies,
5415
5757
  build=_build_design_prep_decision,
@@ -5431,12 +5773,6 @@ STEPS: list[Step] = [
5431
5773
  owns=("design_prep_decision", "design_prep_overrides_json",
5432
5774
  "design_prep_notes"),
5433
5775
  repeatable=True),
5434
- Step(S_STAGE_PICK,
5435
- applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
5436
- and bool(s.approved_plan_path)
5437
- and S_STAGE_PICK not in s.answered),
5438
- build=_build_stage_pick, submit=_submit_stage_pick,
5439
- owns=("selected_stage", "selected_stages")),
5440
5776
  Step(S_HANDOFF_STAGE_PICK,
5441
5777
  applies=lambda s: (s.task_type == "release-handoff"
5442
5778
  and bool(s.task_group)
@@ -5870,6 +6206,18 @@ def init_state(
5870
6206
  )
5871
6207
 
5872
6208
 
6209
+ def _auto_start_new_task_when_it_is_the_only_choice(state: WizardState) -> None:
6210
+ """미완료 task 가 없으면 중복된 새 작업 선택 화면을 건너뛴다."""
6211
+ prompt = next_prompt(state)
6212
+ if (
6213
+ prompt.step != S_TASK_PICK
6214
+ or [option.value for option in prompt.options] != [TASK_PICK_NEW_TOKEN]
6215
+ ):
6216
+ return
6217
+ _submit_task_pick(state, TASK_PICK_NEW_TOKEN)
6218
+ state.answered.append(S_TASK_PICK)
6219
+
6220
+
5873
6221
  def _build_group_prompt(state: WizardState, group_id: str) -> Prompt:
5874
6222
  """그룹의 적용가능·미답변 픽 멤버를 최대 GROUP_MAX_TABS 개 모은다.
5875
6223
 
@@ -6313,6 +6661,9 @@ def _reverify_scope_line(state: WizardState) -> Optional[str]:
6313
6661
  if state.reverify_scope == "full":
6314
6662
  return _msg(state.workspace_root, "confirmation",
6315
6663
  "reverify_scope_user_full")
6664
+ if state.reverify_scope == CARRY_ALL_SCOPE:
6665
+ return _msg(state.workspace_root, "confirmation",
6666
+ "reverify_scope_user_carry_all")
6316
6667
  if state.reverify_scope and state.reverify_scope != "auto":
6317
6668
  return _msg(state.workspace_root, "confirmation",
6318
6669
  "reverify_scope_user_stages", stages=state.reverify_scope)
@@ -6378,15 +6729,22 @@ def confirmation_block(state: WizardState) -> str:
6378
6729
  lines.append(f" workers : {roster}")
6379
6730
  if role_selection:
6380
6731
  profile = _load_role_profile_for_state(state)
6381
- if state.host_entry_mode == "current-session":
6382
- lines.append(_msg(
6383
- state.workspace_root,
6384
- "confirmation",
6385
- "static_role",
6386
- role="leader",
6387
- ordinal="1",
6388
- model="current-session",
6389
- ))
6732
+ native_provider = default_host_registry().resolve(
6733
+ state.host_runtime
6734
+ ).descriptor.native_provider_id
6735
+ leader_model = (
6736
+ "current-session"
6737
+ if state.host_entry_mode == "current-session"
6738
+ else f"{native_provider} (host runtime default)"
6739
+ )
6740
+ lines.append(_msg(
6741
+ state.workspace_root,
6742
+ "confirmation",
6743
+ "static_role",
6744
+ role="leader",
6745
+ ordinal="1",
6746
+ model=leader_model,
6747
+ ))
6390
6748
  for requirement in _selectable_static_requirements(state, profile):
6391
6749
  count = _selected_role_count(state, requirement)
6392
6750
  selected = state.role_models.get(requirement.role, [])
@@ -6715,6 +7073,7 @@ def _cli(argv: list[str]) -> int:
6715
7073
  )
6716
7074
  if args.critic:
6717
7075
  state.critic = args.critic
7076
+ _auto_start_new_task_when_it_is_the_only_choice(state)
6718
7077
  save_state_file(state_path, state)
6719
7078
  first = next_prompt(state)
6720
7079
  print(json.dumps({"ok": True, "next": prompt_payload(state, first)},
@@ -6750,7 +7109,13 @@ def _cli(argv: list[str]) -> int:
6750
7109
  except WizardError as exc:
6751
7110
  _save_resumed_state_file(state_path, persisted_state)
6752
7111
  try:
6753
- current = prompt_payload(state, next_prompt(state))
7112
+ # current 는 방금 디스크로 되돌린 상태에서 렌더한다. 뮤테이션이
7113
+ # 진행되다 만 state 로 렌더하면, 검증 전에 필드를 채운 핸들러
7114
+ # (예: reuse_previous)의 실패가 다음 스텝을 current 로 내보내
7115
+ # 재질문 계약("ok:false 면 같은 step 재제출")과 어긋난다.
7116
+ current = prompt_payload(
7117
+ persisted_state, next_prompt(persisted_state)
7118
+ )
6754
7119
  except WizardError:
6755
7120
  # 현재 step 의 build 자체가 실패하면(예: 손상된 Stage Map 으로
6756
7121
  # _build_stage_pick·_build_handoff_stage_pick 가 raise) recovery 재렌더가