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
@@ -1,35 +1,94 @@
1
- """Provider-registry-derived worker artifact paths."""
1
+ """로스터 worker_id 어휘와 그로부터 나오는 아티팩트 경로."""
2
2
  from __future__ import annotations
3
3
 
4
4
  from pathlib import Path
5
5
  from typing import Any, Mapping
6
6
 
7
+ from .domain.role import ROLE_DEFINITIONS
7
8
  from .models import provider_ids
8
9
 
10
+ # worker_id 접미사로 쓰이는 역할 토큰 — 역할 카탈로그가 정본이다.
11
+ # 이 모듈은 leaf 로 남아야 한다(`worker_prompt_body` 를 끌어오면
12
+ # `run_context` ↔ `path_hints` 순환이 된다). 그래서 고정 worker_id 인
13
+ # `report-writer` 는 이름으로 특수 처리하지 않는다 — 마지막 하이픈 뒤가
14
+ # `writer` 라 어느 역할 토큰과도 맞지 않아 자연히 접미사 없음으로 떨어진다.
15
+ _ROLE_SUFFIX_TOKENS = frozenset(
16
+ definition.id for definition in ROLE_DEFINITIONS
17
+ )
9
18
 
10
- def artifact_worker_ids() -> list[str]:
11
- """All analysis worker ids plus the non-analysis report writer slot."""
12
- return [*provider_ids("analyser"), "report-writer"]
13
19
 
20
+ def worker_role_suffix(worker_id: str) -> str:
21
+ """worker_id 에서 provider 뒤에 붙은 역할 접미사. 없으면 빈 문자열.
14
22
 
15
- def artifacts_from_context(
16
- ctx: Mapping[str, Any], *, relative: bool
17
- ) -> dict[str, dict[str, str]]:
18
- """Derive prompt/result/error paths without provider-specific branches."""
23
+ `claude-verifier` → `verifier`, `claude-verifier-2` → `verifier-2`,
24
+ `claude` → `""`, `report-writer` → `""`.
25
+ """
26
+ stem, separator, tail = worker_id.rpartition("-")
27
+ ordinal = ""
28
+ if separator and tail.isdigit():
29
+ ordinal, worker_id = f"-{tail}", stem
30
+ _, separator, role = worker_id.rpartition("-")
31
+ if not separator or role not in _ROLE_SUFFIX_TOKENS:
32
+ return ""
33
+ return f"{role}{ordinal}"
34
+
35
+
36
+ def worker_provider_id(worker_id: str) -> str:
37
+ """역할 접미사가 붙은 worker_id 에서 provider id 를 되돌린다.
38
+
39
+ 한 provider 가 로스터에서 두 슬롯 이상을 차지하면 슬롯마다
40
+ `claude-implementer` · `claude-verifier` 처럼 역할이 붙는다. 그 접미사를 떼면
41
+ provider registry 가 아는 id 로 돌아간다 — 접미사가 없는 id 는 그대로 남는다.
42
+ """
43
+ suffix = worker_role_suffix(worker_id)
44
+ return worker_id[: -(len(suffix) + 1)] if suffix else worker_id
45
+
46
+
47
+ def roster_worker_ids(ctx: Mapping[str, Any]) -> list[str]:
48
+ """이 런이 실제로 여는 worker_id 목록.
49
+
50
+ 같은 provider 가 실행자와 검증자를 겸하면 로스터에 `claude` 와 `claude-2` 가
51
+ 함께 서므로, provider registry 목록으로는 두 번째 슬롯의 경로를 만들 수 없다.
52
+ 로스터를 싣지 않은 ctx(경로 힌트에서 되살린 레거시 컨텍스트)에서는 registry
53
+ 목록으로 돌아간다 — 그 경우 접미사 id 는 애초에 등장하지 않는다.
54
+ """
55
+ roster = [
56
+ part.strip()
57
+ for part in str(ctx.get("RECOMMENDED_ANALYSERS", "") or "").split(",")
58
+ if part.strip()
59
+ ]
60
+ return roster or [*provider_ids("analyser"), "report-writer"]
61
+
62
+
63
+ def worker_artifact_paths(
64
+ ctx: Mapping[str, Any], worker_id: str, *, relative: bool
65
+ ) -> dict[str, str]:
66
+ """worker_id 하나의 prompt/result/errors 경로.
67
+
68
+ 경로는 id 에서 직접 만들어지므로 로스터에 없는 id 로도 부를 수 있다 —
69
+ payload 로만 전달된 워커 행을 되살릴 때 필요하다.
70
+ """
19
71
  prompt_root = _context_root(ctx, "RUN_PROMPTS", relative)
20
72
  result_root = _context_root(ctx, "WORKER_RESULTS", relative)
21
73
  task_type = str(ctx.get("TASK_TYPE_SEGMENT", ""))
22
74
  prompt_suffix = f"-{task_type}-{ctx.get('RUN_PROMPTS_SEQ', '')}"
23
75
  result_suffix = f"-{task_type}-{ctx.get('WORKER_RESULTS_SEQ', '')}"
24
76
  return {
25
- worker_id: {
26
- "promptPath": str(prompt_root / f"{worker_id}-worker-prompt{prompt_suffix}.md"),
27
- "resultPath": str(result_root / f"{worker_id}-worker{result_suffix}.md"),
28
- "errorsSidecarPath": str(
29
- result_root / f"{worker_id}-worker-errors{result_suffix}.json"
30
- ),
31
- }
32
- for worker_id in artifact_worker_ids()
77
+ "promptPath": str(prompt_root / f"{worker_id}-worker-prompt{prompt_suffix}.md"),
78
+ "resultPath": str(result_root / f"{worker_id}-worker{result_suffix}.md"),
79
+ "errorsSidecarPath": str(
80
+ result_root / f"{worker_id}-worker-errors{result_suffix}.json"
81
+ ),
82
+ }
83
+
84
+
85
+ def artifacts_from_context(
86
+ ctx: Mapping[str, Any], *, relative: bool
87
+ ) -> dict[str, dict[str, str]]:
88
+ """이 런의 로스터 worker_id 별 prompt/result/errors 경로."""
89
+ return {
90
+ worker_id: worker_artifact_paths(ctx, worker_id, relative=relative)
91
+ for worker_id in roster_worker_ids(ctx)
33
92
  }
34
93
 
35
94
 
@@ -18,8 +18,30 @@ from okstra_ctl.worker_audit_ledger import (
18
18
  )
19
19
 
20
20
 
21
+ _CLI_EPILOG = r"""Usage:
22
+ okstra worker-audit-check --run-dir <runs/<task-type>/> --task-type <type> \
23
+ --seq <nnn> [--worker <id>]
24
+
25
+ --run-dir the run directory; worker-results/ and prompts/ hang off it
26
+ --worker check only this worker (default: every worker in the run)
27
+
28
+ Emits one JSON object — `{ok, failures[]}` — and exits 2 when failures[] is
29
+ non-empty, 0 otherwise.
30
+
31
+ Runs the Phase 7 audit-sidecar rules now, while the worker session is still
32
+ alive: that every result file carries no `## 0. Reading Confirmation` heading,
33
+ that its audit sidecar exists, and that every backticked `path:line` citation
34
+ has a matching Evidence read row in that sidecar.
35
+
36
+ Call it right after collecting a worker result. The same failure at Phase 7
37
+ leaves only a retroactive edit — which breaks the audit chain — or a failed run.
38
+ """
39
+
40
+
21
41
  def _parser() -> argparse.ArgumentParser:
22
42
  parser = argparse.ArgumentParser(
43
+ epilog=_CLI_EPILOG,
44
+ formatter_class=argparse.RawDescriptionHelpFormatter,
23
45
  prog="okstra worker-audit-check",
24
46
  description="Check one run's worker audit sidecars (read-only).",
25
47
  )
@@ -65,8 +65,29 @@ def _backend_payload(backend_plan, *, dry_run: bool) -> dict[str, Any]:
65
65
  return payload
66
66
 
67
67
 
68
+ _CLI_EPILOG = r"""Usage:
69
+ okstra worker-dispatch --project-root <dir> --run-manifest <path> \
70
+ [--workers <cli-assigned-worker-ids>] [--dry-run] \
71
+ [--idle-timeout-seconds <n>]
72
+
73
+ This command reads a prepared run manifest and dispatches only assignments
74
+ whose persisted runner is 'cli-wrapper'. It verifies each invocation contract
75
+ before execution and uses the persisted provider, model, and registered CLI.
76
+ Native-session assignments remain owned by the active host session.
77
+
78
+ Missing worker prompt files are generated automatically from the immutable run snapshot.
79
+ Existing prompt and metadata files are never overwritten.
80
+ When report-writer completes, this command also runs the idempotent post-report
81
+ steps: token-usage substitution, render-views, spawn-followups, and validate-run.
82
+
83
+ --workspace-root and --okstra-bin are owned by this command.
84
+ """
85
+
86
+
68
87
  def _parser() -> argparse.ArgumentParser:
69
88
  parser = argparse.ArgumentParser(
89
+ epilog=_CLI_EPILOG,
90
+ formatter_class=argparse.RawDescriptionHelpFormatter,
70
91
  prog="okstra worker-dispatch",
71
92
  description="Dispatch verified CLI-backed workers for a prepared run.",
72
93
  )
@@ -404,8 +404,41 @@ def probe_target(team_state_value: str, worker_id: str) -> ProbeTarget:
404
404
  return ProbeTarget(mode, artifact, dispatched_at, result_path)
405
405
 
406
406
 
407
+ _CLI_EPILOG = r"""Usage:
408
+ okstra worker-liveness [--team-state <path> --worker <worker-id>]...
409
+ [--max-idle <seconds>] [--launch-grace <seconds>] [--json]
410
+ okstra worker-liveness --wait [--team-state <path> --worker <worker-id>]...
411
+ [--interval <seconds>] [--timeout <seconds>]
412
+
413
+ --wait polls until every named worker's persisted resultPath lands (exit 0), one
414
+ worker probes unhealthy (exit 1), or --timeout passes (exit 2).
415
+ Defaults: --interval 20, --timeout 2400. Run it as a background
416
+ command and branch on the exit code. Use this instead of writing a
417
+ poll loop: both the "is it done" test (the persisted resultPath, not a
418
+ filename you assemble) and the "is it dead" test live in here, and a
419
+ hand-written loop re-derives them and gets one wrong. A worker row
420
+ without a resultPath is refused rather than waited on by guess.
421
+
422
+ --team-state and --worker select one pending worker. The worker row's
423
+ livenessMode picks the probe: audit-heartbeat reads the in-process
424
+ worker audit sidecar path and reports stalled past the heartbeat
425
+ cadence; wrapper-status reports did-not-launch when no prompt sibling
426
+ .log or .status.json exists past the launch grace. Both graces begin
427
+ at workers[].startedAt, never at an artifact mtime — the audit sidecar
428
+ is reused on re-dispatch, so the previous attempt's last heartbeat is
429
+ not this dispatch's signal.
430
+
431
+ Output: JSON { ok, checkedAt, probes[], unhealthy[] }, plus { outcome, pending[],
432
+ waitedSeconds } under --wait. Exit 1 when any worker is stalled or did not
433
+ launch, so a caller can branch on the exit code. Read-only: it reports, it never
434
+ kills or re-dispatches.
435
+ """
436
+
437
+
407
438
  def main(argv: list[str] | None = None) -> int:
408
439
  parser = argparse.ArgumentParser(
440
+ epilog=_CLI_EPILOG,
441
+ formatter_class=argparse.RawDescriptionHelpFormatter,
409
442
  prog="okstra worker-liveness",
410
443
  description="Report whether pending workers are still alive (read-only).",
411
444
  )
@@ -22,6 +22,7 @@ def analysis_prompt_body(
22
22
  worker_id: str,
23
23
  model: str,
24
24
  plan: PromptPlan,
25
+ approved_plan_path: str = "",
25
26
  ) -> list[str]:
26
27
  """Render the provider-neutral body for an initial analysis audience."""
27
28
  label = analysis_worker_label(worker_id)
@@ -40,7 +41,7 @@ def analysis_prompt_body(
40
41
  f"- Task type: `{_require_string(manifest, 'taskType')}`",
41
42
  "",
42
43
  "## Inputs",
43
- *analysis_input_lines(manifest, active_context, plan),
44
+ *analysis_input_lines(manifest, active_context, plan, approved_plan_path),
44
45
  "",
45
46
  mcp_pointer_line(),
46
47
  "",
@@ -54,10 +55,22 @@ def analysis_input_lines(
54
55
  manifest: Mapping[str, Any],
55
56
  active_context: Mapping[str, Any],
56
57
  plan: PromptPlan,
58
+ approved_plan_path: str = "",
57
59
  ) -> list[str]:
58
60
  if plan.packet_only:
59
61
  packet_path = instruction_path(manifest, active_context, "analysisPacketPath")
60
- return existing_input_lines((("Primary analysis packet", packet_path),))
62
+ # 승인 계획은 `## Inputs` 에 실어야 한다. 워커의 읽기 범위는 이 절과
63
+ # `[Required reading]`·검증 대상 경로만 허용하는데(`**Read scope:**`),
64
+ # 패킷은 Tier 1 을 "원 브리프/승인 계획의 `validation` 세트" 로 정의하고
65
+ # 승인 계획의 requirements·scope 까지 요구한다. 경로가 여기 없으면 범위를
66
+ # 지키는 워커는 그 요구를 만족시킬 방법이 없다 — 실측(2026-08-26,
67
+ # `fontsninja-nlpvibe` final-verification): 범위를 지킨 검증자는 Tier 1 을
68
+ # 무엇으로 볼지 몰라 `tsc --noEmit` 으로 대체했고, 범위를 넘은 검증자만
69
+ # 계약대로의 산출물을 냈다.
70
+ return existing_input_lines((
71
+ ("Primary analysis packet", packet_path),
72
+ ("Approved plan", approved_plan_path),
73
+ ))
61
74
  inputs = [
62
75
  ("Primary analysis packet", instruction_path(manifest, active_context, "analysisPacketPath")),
63
76
  ("Task brief", instruction_path(manifest, active_context, "taskBriefPath")),
@@ -221,6 +221,47 @@ def validate_analysis_prompt_set(prompts: Mapping[str, str]) -> list[str]:
221
221
  return [f"normalized initial analysis prompts differ across workers: {workers}"]
222
222
 
223
223
 
224
+ # reverify 응답 계약 블록. `prompts/lead/convergence.md`
225
+ # §"Required reverify output contract (BLOCKING)" 가 정본이고, 리드는 그 블록을
226
+ # 변종별 응답 형식 뒤에 그대로 덧붙인다.
227
+ #
228
+ # 바이트 단위로 비교하지 않는다 — 손으로 복사하는 블록이라 공백·줄바꿈이 흔들리고,
229
+ # 그 흔들림으로 디스패치를 막으면 규칙이 아니라 서식을 강제하게 된다. 세 항목이
230
+ # 각각 무엇을 요구하는지 식별하는 토큰으로 본다.
231
+ _OUTPUT_CONTRACT_HEADING = "## Output Contract"
232
+ _OUTPUT_CONTRACT_CLAUSES = (
233
+ ("result-path write", ("Result Path", "before returning")),
234
+ ("audit sidecar write", ("Audit sidecar path", "before returning")),
235
+ ("no inline-only response", ("inline",)),
236
+ )
237
+
238
+
239
+ def _validate_output_contract_block(text: str) -> list[str]:
240
+ """응답 계약 블록이 프롬프트에 실려 있는지.
241
+
242
+ 빠지면 워커가 결과를 파일로 쓰지 않고 인라인으로만 답하는 경로가 열린다 —
243
+ 그 run 은 결과 파일이 없는 채로 dispatch 만 성공한 것처럼 보인다.
244
+ """
245
+ if _OUTPUT_CONTRACT_HEADING not in text:
246
+ return [
247
+ f"`{_OUTPUT_CONTRACT_HEADING}` block is required "
248
+ "(convergence.md §\"Required reverify output contract\")"
249
+ ]
250
+ tail = text.split(_OUTPUT_CONTRACT_HEADING, 1)[1]
251
+ missing = [
252
+ label
253
+ for label, tokens in _OUTPUT_CONTRACT_CLAUSES
254
+ if not all(token.lower() in tail.lower() for token in tokens)
255
+ ]
256
+ if missing:
257
+ return [
258
+ "output contract block is missing its "
259
+ + ", ".join(missing)
260
+ + " clause"
261
+ ]
262
+ return []
263
+
264
+
224
265
  def validate_reverify_prompt(
225
266
  text: str,
226
267
  *,
@@ -268,6 +309,7 @@ def validate_reverify_prompt(
268
309
  and boundary_position > first_heading.start()
269
310
  ):
270
311
  errors.append("phase boundary block must precede reverify instructions")
312
+ errors.extend(_validate_output_contract_block(normalized))
271
313
  return errors
272
314
 
273
315
 
@@ -351,7 +393,9 @@ def validate_initial_prompt_records(
351
393
  )
352
394
  if plan.equality_group:
353
395
  group = equality_groups.setdefault(plan.equality_group, {})
354
- group[record.worker_id] = text
396
+ # 최초 프롬프트의 동등성만 비교한다. 같은 역할의 교정 호출은 최초
397
+ # assignment 를 재사용할 수 있지만 지시문은 의도적으로 달라진다.
398
+ group.setdefault(record.worker_id, text)
355
399
  for prompts in equality_groups.values():
356
400
  errors.extend(validate_analysis_prompt_set(prompts))
357
401
  return errors
@@ -365,15 +409,34 @@ def _validate_model_header(text: str, expected_model: str | None) -> list[str]:
365
409
  """
366
410
  if expected_model is None:
367
411
  return []
368
- task_text = text.split("\n\n## Task Instructions\n\n", 1)[-1]
369
- model = _model_value(_header_values(task_text, MODEL_HEADER))
412
+ model = _model_value(_header_values(text, MODEL_HEADER))
370
413
  if model is None:
371
414
  return ["exactly one non-empty **Model:** <label>, <model> header is required"]
372
- if model != expected_model:
415
+ if not _model_header_matches(model, expected_model):
373
416
  return [f"prompt model does not match requested model: {expected_model}"]
374
417
  return []
375
418
 
376
419
 
420
+ def validate_prompt_model_header(text: str, expected_model: str) -> list[str]:
421
+ """`**Model:**` 헤더가 이 디스패치가 실제로 돌릴 모델을 가리키는가.
422
+
423
+ 재료화는 호출자가 넘긴 모델로 판정한다. 그 값이 옳아도 프롬프트 본문의
424
+ 헤더가 다르면 어긋남은 `validate-run` 에서야 드러나는데, 그때는 프롬프트가
425
+ 이미 나가고 다이제스트가 기록된 뒤라 그 run 안에서 고칠 수 없다 — 프롬프트를
426
+ 고치면 다이제스트 위조가 된다. `record-dispatch` 는 워커가 돌기 전 마지막
427
+ 지점이고, 거기서는 **기록된 배정값** 을 쓸 수 있다.
428
+
429
+ 묻는 것은 "적혀 있다면 이 모델인가" 하나다. 헤더가 없는 것은 여기서 볼
430
+ 문제가 아니다 — 어떤 audience 가 그 헤더를 실어야 하는지는 프롬프트 계약이
431
+ audience 를 알고 판정한다. 여기서 부재까지 막으면 헤더를 요구하지 않는
432
+ 디스패치가 전부 막힌다.
433
+ """
434
+ task_text = text.split("\n\n## Task Instructions\n\n", 1)[-1]
435
+ if _model_value(_header_values(task_text, MODEL_HEADER)) is None:
436
+ return []
437
+ return _validate_model_header(text, expected_model)
438
+
439
+
377
440
  def _validate_record_metadata(text: str, record: PromptRecord) -> list[str]:
378
441
  errors = [
379
442
  *_validate_delivery_mode(
@@ -462,13 +525,41 @@ def _header_values(text: str, prefix: str) -> list[str]:
462
525
  ]
463
526
 
464
527
 
528
+ def _model_header_matches(got: str, expected: str) -> bool:
529
+ """표시 alias 와 실행 id 를 같은 요청으로 본다.
530
+
531
+ 헤더는 `opus-5` 이고 잡의 실행 값은 `claude-opus-5` 인 경우가 있다.
532
+ 콤마 형식의 두 번째 칸과 실행 값이 문자열로 다를 뿐이면 거절하지 않는다.
533
+
534
+ `<provider>/<model>` 표기도 받는다. 위저드가 사용자에게 모델을 보여 줄 때
535
+ 쓰는 표기가 정확히 그것(`claude/sonnet`)이라, 지시문에 그대로 옮겨 적는 것이
536
+ 자연스러운 실수가 아니라 자연스러운 선택이다. provider 는 이 헤더의 판정
537
+ 대상이 아니다 — 별도의 `**Provider:**` 헤더가 담당한다. 그래서 앞의 한 칸을
538
+ 떼고 모델끼리 비교한다. 다른 모델이면 떼고 나서도 안 맞으므로 이 관용이
539
+ 불일치를 가리지는 않는다(`claude/opus-5` 대 `sonnet` 은 그대로 실패).
540
+ """
541
+ if got == expected:
542
+ return True
543
+ got_l, expected_l = got.lower(), expected.lower()
544
+ if got_l == expected_l:
545
+ return True
546
+ if expected_l.endswith("-" + got_l) or got_l.endswith("-" + expected_l):
547
+ return True
548
+ if "/" in got_l:
549
+ return _model_header_matches(got_l.rsplit("/", 1)[1], expected_l)
550
+ return False
551
+
552
+
465
553
  def _model_value(values: list[str]) -> str | None:
466
554
  if len(values) != 1:
467
555
  return None
468
- parts = [part.strip() for part in values[0].rsplit(",", 1)]
469
- if len(parts) != 2 or not all(parts):
556
+ raw = values[0].strip()
557
+ if not raw:
470
558
  return None
471
- return parts[1]
559
+ parts = [part.strip() for part in raw.rsplit(",", 1)]
560
+ if len(parts) == 2 and all(parts):
561
+ return parts[1]
562
+ return raw
472
563
 
473
564
 
474
565
  def _resolve_record_plan(
@@ -82,7 +82,7 @@ PLAIN_FILE_WRITE_HEADER = (
82
82
  "brain folder, so an artifact write under `<PROJECT_ROOT>/.okstra/` is "
83
83
  "rejected and the run ends with no result file."
84
84
  )
85
- ANTIGRAVITY_WORKER_ID = "antigravity"
85
+ ANTIGRAVITY_PROVIDER = "antigravity"
86
86
 
87
87
 
88
88
  def worker_prompt_headers(
@@ -141,7 +141,7 @@ def worker_prompt_headers(
141
141
  f"**Errors sidecar path:** {errors_sidecar_path}",
142
142
  READ_SCOPE_HEADER,
143
143
  ])
144
- if worker_id == ANTIGRAVITY_WORKER_ID:
144
+ if _worker_provider(manifest, worker_id) == ANTIGRAVITY_PROVIDER:
145
145
  headers.append(PLAIN_FILE_WRITE_HEADER)
146
146
  if _string_value(manifest.get("taskType")) == "improvement-discovery":
147
147
  headers.append(
@@ -153,6 +153,28 @@ def worker_prompt_headers(
153
153
  return headers
154
154
 
155
155
 
156
+ def _worker_provider(manifest: Mapping[str, Any], worker_id: str) -> str:
157
+ """이 워커를 실행할 공급자.
158
+
159
+ `agy` 의 artifact store 는 공급자의 성질이지 워커 칸 이름의 성질이 아니다.
160
+ 종전에는 `worker_id == "antigravity"` 로 걸렀는데, 워커 id 가 공급자 이름과
161
+ 같은 것은 기본 로스터뿐이다 — 역할 접미사가 붙은 칸(`antigravity-planner`
162
+ 등)은 전부 이 안전장치를 못 받았고, 그 프롬프트를 재검증에 쓰려던 리드는
163
+ 계약(`prompts/lead/convergence.md` §"Required reverify-prompt anchor
164
+ headers")대로 라운드를 중단해야 했다.
165
+
166
+ 로스터 행을 못 찾으면 종전 규칙으로 떨어진다 — 그 자리에선 공급자를 알
167
+ 방법이 없고, 헤더를 빼면 계약이 fail-closed 로 막는다.
168
+ """
169
+ workers = manifest.get("workerAssignments")
170
+ if isinstance(workers, list):
171
+ for row in workers:
172
+ if isinstance(row, Mapping) and row.get("workerId") == worker_id:
173
+ value = row.get("provider")
174
+ return value if isinstance(value, str) else ""
175
+ return worker_id
176
+
177
+
156
178
  def _final_verification_target_headers(
157
179
  active_context: Mapping[str, Any],
158
180
  ) -> list[str]:
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
  from dataclasses import dataclass
5
5
  from typing import Any, Literal, Mapping
6
6
 
7
- from .agent_invocation import AgentAudience
7
+ from .agent.invocation import AgentAudience
8
8
  from .analysis_inputs import ANALYSIS_TASK_TYPES
9
9
 
10
10
 
@@ -90,6 +90,10 @@ class PromptPlan:
90
90
  required_headers: tuple[str, ...]
91
91
  max_body_lines: int | None
92
92
  max_directive_lines: int | None
93
+ # 이 audience 의 `## Inputs` 가 승인 계획을 함께 열거하는가. 승인 계획을
94
+ # 기준으로 판정하라고 요구하는 phase 만 참이다 — 없는 phase 에 실으면
95
+ # 열거만 되고 열 파일이 없는 줄이 된다.
96
+ include_approved_plan: bool = False
93
97
 
94
98
 
95
99
  def resolve_prompt_plan(
@@ -149,6 +153,7 @@ def resolve_prompt_plan(
149
153
  duty_audience=analysis_duty,
150
154
  equality_group=analysis_equality_group,
151
155
  packet_only=True,
156
+ include_approved_plan=True,
152
157
  required_headers=FINAL_VERIFICATION_HEADERS,
153
158
  max_body_lines=96,
154
159
  max_directive_lines=40,
@@ -191,6 +196,7 @@ def _plan(
191
196
  duty_audience: AgentAudience | None = None,
192
197
  equality_group: str | None = None,
193
198
  packet_only: bool = False,
199
+ include_approved_plan: bool = False,
194
200
  allow_coding_preflight: bool = False,
195
201
  required_headers: tuple[str, ...] = (),
196
202
  max_body_lines: int | None = None,
@@ -201,6 +207,7 @@ def _plan(
201
207
  duty_audience=duty_audience or _duty_audience(audience),
202
208
  equality_group=equality_group,
203
209
  packet_only=packet_only,
210
+ include_approved_plan=include_approved_plan,
204
211
  allow_coding_preflight=allow_coding_preflight,
205
212
  required_headers=_worker_facing_headers(audience, required_headers),
206
213
  max_body_lines=max_body_lines,
@@ -261,14 +268,22 @@ def critic_assignment_ref(task_type: str) -> str:
261
268
 
262
269
 
263
270
  def _executor_worker_id(manifest: Mapping[str, Any]) -> str | None:
271
+ """실행자의 **worker_id**. provider 이름이 아니다.
272
+
273
+ 한 provider 가 실행자와 검증자를 겸하면 슬롯마다 역할이 붙어 실행자의 id 는
274
+ `codex-implementer` 가 된다. 여기서 provider(`codex`)를 돌려주면 실행자가
275
+ 자기 자신과 매칭되지 않아 검증자 프롬프트로 실체화되고, 그 사실은
276
+ `agent invocation duty does not match expected audience` 로 디스패치 단계에서야
277
+ 드러난다.
278
+ """
264
279
  team_contract = manifest.get("teamContract")
265
280
  if not isinstance(team_contract, Mapping):
266
281
  return None
267
282
  executor = team_contract.get("executor")
268
283
  if not isinstance(executor, Mapping):
269
284
  return None
270
- provider = executor.get("provider")
271
- return provider.strip() if isinstance(provider, str) and provider.strip() else None
285
+ worker_id = executor.get("workerId")
286
+ return worker_id.strip() if isinstance(worker_id, str) and worker_id.strip() else None
272
287
 
273
288
 
274
289
  def _required_string(payload: Mapping[str, Any], key: str) -> str:
@@ -54,7 +54,8 @@ def build_request(
54
54
  project_root=root,
55
55
  worktree_path=worktree,
56
56
  # Nobody is at the keyboard to answer an approval prompt, so the gate is
57
- # open and the boundary comes from the write scope instead.
57
+ # open. The write scope tells the worker where it belongs; nothing in the
58
+ # CLI stops it leaving, so the run checks that after the fact.
58
59
  policy=ExecutionPolicy(
59
60
  auto_approve=True,
60
61
  write_scope=write_scope(root, worktree, role),
@@ -10,6 +10,7 @@ SIGTERM a healthy worker in the middle of a long reasoning stretch.
10
10
  """
11
11
  from __future__ import annotations
12
12
 
13
+ import dataclasses
13
14
  import json
14
15
  import os
15
16
  import selectors
@@ -21,6 +22,7 @@ from typing import Any, Callable, Mapping
21
22
 
22
23
  from .domain.provider import ServedModelAttestation, ServedModelNormalizer
23
24
  from .domain.worker_exec import (
25
+ NO_RESULT_EXIT_CODE,
24
26
  SERVED_MODEL_MISMATCH_EXIT_CODE,
25
27
  ExecCommand,
26
28
  ExecutionStrategy,
@@ -226,11 +228,9 @@ def _with_served_model_observation(
226
228
  observation.record(observed)
227
229
  return observed
228
230
 
229
- return JsonEvents(
230
- normalise=presentation.normalise,
231
- observe=observe,
232
- join_adjacent_text=presentation.join_adjacent_text,
233
- )
231
+ # 필드를 손으로 옮기지 않는다 — 그렇게 하던 동안 어댑터가 선언한
232
+ # `is_conclusion` 이 여기서 떨어져 나가, grok 의 종결 판정이 무효였다.
233
+ return dataclasses.replace(presentation, observe=observe)
234
234
 
235
235
 
236
236
  class _ServedModelObservation:
@@ -393,11 +393,16 @@ def _pump(
393
393
  strategy.flush(transcript)
394
394
 
395
395
  exit_code = process.wait()
396
- if closing_text is None and isinstance(strategy, JsonEvents):
397
- # JSON 스트림은 결과 이벤트로 끝나야 한다. 텍스트를 흘리는 CLI 에는
398
- # 놓칠 결과 이벤트가 없다.
396
+ if isinstance(strategy, JsonEvents) and not strategy.concluded():
397
+ # JSON 스트림은 그 어휘의 종결 이벤트로 끝나야 한다. 텍스트를 흘리는
398
+ # CLI 에는 놓칠 종결 이벤트가 없다. 무엇이 종결인지는 전략이 안다.
399
399
  transcript.note("no result event in the CLI's output")
400
- elif closing_text is not None:
400
+ if exit_code == 0 and not timed_out:
401
+ # 실측(dev-10341, 한 CLI 4회): 종결 이벤트도 산출물도 없이 exit 0.
402
+ # 그대로 돌려주면 사이드카와 셸 소비자에게는 성공으로 읽힌다.
403
+ # CLI 자신의 비영 코드는 보존한다 — 그쪽이 더 구체적인 사실이다.
404
+ exit_code = NO_RESULT_EXIT_CODE
405
+ if closing_text is not None:
401
406
  # Result 는 format_* 가 줄을 만들지 않는다. 아카이브 결론은 러너가 적는다.
402
407
  # 상한 밖에 둔다 — 잘린 결론은 사후 분석 전체를 잃게 한다.
403
408
  if isinstance(strategy, JsonEvents):
@@ -559,6 +564,8 @@ def _terminate(process: subprocess.Popen[bytes]) -> None:
559
564
  try:
560
565
  os.killpg(process.pid, signal.SIGKILL)
561
566
  except ProcessLookupError:
567
+ # expected-miss: SIGTERM 유예 사이에 그룹이 스스로 끝난 경우다.
568
+ # 죽이려던 대상이 이미 없는 것은 성공이지 실패가 아니다.
562
569
  pass
563
570
  process.wait()
564
571
 
@@ -18,8 +18,23 @@ def _validated_team_state_path(value: str) -> Path:
18
18
  return path
19
19
 
20
20
 
21
+ _CLI_EPILOG = r"""Usage:
22
+ okstra worker-state transition --team-state <path> --worker <worker-id> \
23
+ --status <in-progress|completed|timeout|error|not-run> \
24
+ [--reason <text>] [--model <execution-value>]
25
+
26
+ in-progress records startedAt and clears endedAt. completed, timeout, and error
27
+ record endedAt. timeout, error, and not-run require --reason.
28
+ """
29
+ _CLI_DESCRIPTION = "Atomically transition persisted worker status."
30
+
31
+
21
32
  def _parser() -> argparse.ArgumentParser:
22
- parser = argparse.ArgumentParser(prog="okstra worker-state")
33
+ parser = argparse.ArgumentParser(
34
+ description=_CLI_DESCRIPTION,
35
+ epilog=_CLI_EPILOG,
36
+ formatter_class=argparse.RawDescriptionHelpFormatter,
37
+ prog="okstra worker-state")
23
38
  commands = parser.add_subparsers(dest="command", required=True)
24
39
  transition = commands.add_parser(
25
40
  "transition", help="atomically transition one worker status"
@@ -54,7 +54,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
54
54
  " - top-N ranking (default 8, brief `candidate-cap` overrides within 1..12)\n"
55
55
  " - per-candidate columns Lens / Scope / Severity / Effort / Consensus / Source workers / Recommended next-phase / Expected behavior after / Evidence (path:line)\n"
56
56
  " - Phase 1.5 reflect-back grilling log at `runs/improvement-discovery/<seq>/state/phase-1.5-grilling.md`\n"
57
- " - Phase 5.5 consensus classification (full / partial / contested / worker-unique)"
57
+ " - Phase 5.5 consensus classification (full / partial / contested / worker-unique / unverified)"
58
58
  ),
59
59
  },
60
60
  "project-analysis": {
@@ -173,6 +173,22 @@ def load_phase_forbidden(workspace_root: Path) -> dict[str, str]:
173
173
  return {phase: render_forbidden(items) for phase, items in data.items()}
174
174
 
175
175
 
176
+ def phase_position(task_type: str) -> str:
177
+ """이 phase 가 생애주기 몇 번째인지 — `"5 of 7"`.
178
+
179
+ 리드는 자기가 어느 칸을 도는지만 알고 지도를 못 본다. 다음 phase 이름은
180
+ 이 run 의 리포트가 정하므로 계속 감추지만(`WORKFLOW_NEXT_RECOMMENDED_PHASE`
181
+ 가 빈 문자열인 이유), 위치를 감출 이유는 없다 — 위치를 모르면 중간 단계를
182
+ 도는 run 이 자기가 마지막인 것처럼 결론을 낸다.
183
+
184
+ `PHASE_SEQUENCE` 밖의 사이드트랙 task-type(improvement-discovery 등)은
185
+ 생애주기 칸이 없으므로 빈 문자열이다.
186
+ """
187
+ if task_type not in PHASE_SEQUENCE:
188
+ return ""
189
+ return f"{PHASE_SEQUENCE.index(task_type) + 1} of {len(PHASE_SEQUENCE)}"
190
+
191
+
176
192
  def compute_workflow_state(
177
193
  *,
178
194
  task_type: str,
@@ -219,6 +235,7 @@ def compute_workflow_state(
219
235
  "WORKFLOW_WORK_CATEGORY": resolved_work_category,
220
236
  "WORKFLOW_CURRENT_PHASE": task_type,
221
237
  "WORKFLOW_CURRENT_PHASE_STATE": phase_state,
238
+ "WORKFLOW_PHASE_POSITION": phase_position(task_type),
222
239
  # 값이 아니라 자리표시자다. 실제 포인터는 리포트 라우팅에서 투영돼
223
240
  # 매니페스트의 `workflow.nextRecommendedPhase` 구조체로 저장된다.
224
241
  # 이 ctx 값은 그 구조체로 대체되지 않는다 — 남은 소비자는