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
@@ -29,7 +29,7 @@ from pathlib import Path
29
29
  from typing import Any, Callable, Mapping, Sequence
30
30
 
31
31
  from . import cmux
32
- from .agent_invocation import (
32
+ from .agent.invocation import (
33
33
  AgentInvocationError,
34
34
  AgentModelAssignment,
35
35
  InvocationMetadataIdentity,
@@ -39,7 +39,11 @@ from .agent_invocation import (
39
39
  verify_agent_invocation,
40
40
  )
41
41
  from .domain.role import RoleCatalogError, role_for_duty
42
- from .execution_identity import Attempt, Invocation
42
+ from .execution_identity import ( # noqa: F401 — build_dispatch_id 재노출
43
+ Attempt,
44
+ Invocation,
45
+ build_dispatch_id,
46
+ )
43
47
  from .execution_manifest import (
44
48
  finish_attempt_mutation,
45
49
  read_execution_manifest,
@@ -52,6 +56,7 @@ from .worker_prompt_body import REPORT_WRITER_WORKER_ID
52
56
  from .worker_prompt_contract import (
53
57
  PromptRecord,
54
58
  validate_initial_prompt_records,
59
+ validate_prompt_model_header,
55
60
  validate_reverify_prompt,
56
61
  )
57
62
  from .worker_runner import LIVE, QUIET
@@ -65,6 +70,7 @@ from .wrapper_status import (
65
70
  from .write_policy import (
66
71
  build_invocation_write_contract,
67
72
  planned_paths_from_run_manifest,
73
+ task_root_from_run_manifest,
68
74
  write_policy_from_payload,
69
75
  )
70
76
  from .convergence_store import write_json_atomic
@@ -188,6 +194,7 @@ class WorkerJob:
188
194
  execution_label: str = ""
189
195
  duty_id: str = ""
190
196
  invocation_ref: str = ""
197
+ result_aliases: tuple[Path, ...] = ()
191
198
  attempt: int = 0
192
199
 
193
200
  @property
@@ -352,13 +359,6 @@ def resolve_required_path(
352
359
  return resolve_project_path(project_root, require_string(manifest, key))
353
360
 
354
361
 
355
- def worker_prompt_path(manifest: Mapping[str, Any], worker_id: str) -> str:
356
- paths = manifest.get("workerPromptPathByWorkerId")
357
- if not isinstance(paths, Mapping):
358
- raise DispatchError("run manifest has no workerPromptPathByWorkerId object")
359
- return require_string(paths, worker_id)
360
-
361
-
362
362
  def worktree_path(
363
363
  manifest: Mapping[str, Any], active_context: Mapping[str, Any]
364
364
  ) -> str:
@@ -505,6 +505,35 @@ _AGENT_ENFORCEMENT_MODES = frozenset({
505
505
  })
506
506
 
507
507
 
508
+ def _require_prompt_model_matches_assignment(
509
+ metadata_path: Path, assignment: Any
510
+ ) -> None:
511
+ """디스패치 직전, 프롬프트가 이 배정의 모델을 가리키는지 확인한다.
512
+
513
+ 메타데이터는 프롬프트 옆에 `<prompt>.meta.json` 으로 놓인다. 읽을 수 없으면
514
+ 조용히 통과한다 — 이 검사는 어긋남을 앞당겨 잡는 것이지, 프롬프트를 못 읽는
515
+ 사정을 새 차단 사유로 만드는 것이 아니다. 그 사정은 이미 다른 검사가 본다.
516
+ """
517
+ prompt_path = metadata_path.with_name(
518
+ metadata_path.name.removesuffix(".meta.json")
519
+ )
520
+ if prompt_path == metadata_path or not prompt_path.is_file():
521
+ return
522
+ expected = str(getattr(assignment, "model_execution_value", "") or "").strip()
523
+ if not expected:
524
+ return
525
+ try:
526
+ text = prompt_path.read_text(encoding="utf-8")
527
+ except OSError:
528
+ return
529
+ errors = validate_prompt_model_header(text, expected)
530
+ if errors:
531
+ raise DispatchError(
532
+ f"agent dispatch prompt does not name this assignment's model: "
533
+ + "; ".join(errors)
534
+ )
535
+
536
+
508
537
  def record_verified_agent_dispatch(
509
538
  *,
510
539
  project_root: Path,
@@ -577,6 +606,7 @@ def record_verified_agent_dispatch(
577
606
  )
578
607
  if errors:
579
608
  raise DispatchError("agent invocation verification failed: " + "; ".join(errors))
609
+ _require_prompt_model_matches_assignment(metadata_path, assignment)
580
610
  if (
581
611
  enforcement_mode == "host-native-spec-link-gate"
582
612
  and assignment.runner != "native-session"
@@ -691,9 +721,6 @@ def record_verified_agent_dispatch(
691
721
  dispatch_kind=dispatch_kind,
692
722
  round=_execution_dispatch_round(dispatch_kind),
693
723
  input_digest=prompt_digest,
694
- write_policy=write_policy.to_payload(),
695
- write_policy_digest=write_policy.digest,
696
- write_enforcement=write_enforcement.to_payload(),
697
724
  ),
698
725
  Attempt(
699
726
  invocation_ref=execution_identity.invocation_ref,
@@ -705,6 +732,9 @@ def record_verified_agent_dispatch(
705
732
  error_path=None,
706
733
  change_summary={},
707
734
  evidence_seal_ref=None,
735
+ write_policy=write_policy.to_payload(),
736
+ write_policy_digest=write_policy.digest,
737
+ write_enforcement=write_enforcement.to_payload(),
708
738
  ),
709
739
  task_key=require_string(manifest, "taskKey"),
710
740
  )
@@ -833,6 +863,7 @@ def _agent_write_contract(
833
863
  project_root=project_root,
834
864
  worktree=worktree,
835
865
  artifact_paths=artifacts,
866
+ task_root=task_root_from_run_manifest(project_root, authority),
836
867
  maximum_precision=maximum_precision,
837
868
  planned_paths=planned[0],
838
869
  planned_paths_declared=planned[1],
@@ -1017,6 +1048,9 @@ def link_agent_dispatch_result(
1017
1048
  f"agent result link requires exactly one dispatch record: {dispatch_id}"
1018
1049
  )
1019
1050
  dispatch = dispatches[0]
1051
+ _require_recorded_result_path(
1052
+ project_root, dispatch, resolved_result, result_relative
1053
+ )
1020
1054
  link: dict[str, Any] = {
1021
1055
  "dispatchId": dispatch_id,
1022
1056
  "resultPath": result_relative,
@@ -1081,20 +1115,21 @@ def _finish_native_agent_attempt(
1081
1115
  if identity is None:
1082
1116
  return
1083
1117
  execution = read_execution_manifest(manifest_path)
1084
- invocation = next(
1118
+ attempt = next(
1085
1119
  (
1086
- row for row in execution.invocations
1120
+ row for row in execution.attempts
1087
1121
  if row.invocation_ref == identity["invocation_ref"]
1122
+ and row.attempt == identity["attempt"]
1088
1123
  ),
1089
1124
  None,
1090
1125
  )
1091
- if invocation is None:
1092
- raise DispatchError("agent result has no canonical invocation policy")
1093
- if dispatch.get("writePolicyDigest") != invocation.write_policy_digest:
1126
+ if attempt is None:
1127
+ raise DispatchError("agent result has no canonical attempt policy")
1128
+ if dispatch.get("writePolicyDigest") != attempt.write_policy_digest:
1094
1129
  raise DispatchError("agent dispatch write policy digest changed")
1095
- if dispatch.get("writeEnforcement") != invocation.write_enforcement:
1130
+ if dispatch.get("writeEnforcement") != attempt.write_enforcement:
1096
1131
  raise DispatchError("agent dispatch write enforcement changed")
1097
- policy = write_policy_from_payload(invocation.write_policy)
1132
+ policy = write_policy_from_payload(attempt.write_policy)
1098
1133
  status, summary = _native_mutation_result(
1099
1134
  project_root=project_root,
1100
1135
  dispatch=dispatch,
@@ -1290,6 +1325,127 @@ def reject_agent_dispatch_result(
1290
1325
  return dict(row)
1291
1326
 
1292
1327
 
1328
+ def _require_recorded_result_path(
1329
+ project_root: Path,
1330
+ dispatch: Mapping[str, Any],
1331
+ resolved_result: Path,
1332
+ result_relative: str,
1333
+ ) -> None:
1334
+ """링크되는 결과가 이 디스패치가 실제로 기다린 산출물인가.
1335
+
1336
+ 비교 대상을 여기서 다시 계산하지 않는다. `record-dispatch` 가 이미
1337
+ `completionPaths` 로 그 목록을 적어 두었고(리포트 작성자는 narrative 와
1338
+ 포인터 기록 둘), 그것이 이 디스패치가 완료로 인정하는 경로의 정본이다.
1339
+
1340
+ 이 대조가 없으면 잘못된 경로를 링크해도 그 자리에서는 통과하고, 조립이
1341
+ `required input is missing` 으로 죽을 때에야 드러난다 — 그때는 워커가 이미
1342
+ 돌고 난 뒤다. 실측(2026-08-26): 리드가 지어낸 seq(`-002.md`)를 넘겼고
1343
+ 매니페스트의 선언은 `-004.md` 였다. 여기서 막으면 워커를 다시 돌릴 필요 없이
1344
+ 같은 명령을 옳은 인자로 다시 부르면 된다.
1345
+
1346
+ `completionPaths` 를 기록하지 않은 행은 판정하지 않는다 — 목록이 없으면
1347
+ 무엇이 정당한지 알 수 없고, 모르면서 막는 것은 추측이다.
1348
+ """
1349
+ recorded = dispatch.get("completionPaths")
1350
+ if not isinstance(recorded, list) or not recorded:
1351
+ return
1352
+ allowed = {
1353
+ resolve_project_path(project_root, str(value)).resolve()
1354
+ for value in recorded
1355
+ if isinstance(value, str) and value.strip()
1356
+ }
1357
+ if not allowed or resolved_result in allowed:
1358
+ return
1359
+ expected = ", ".join(sorted(_relative_project_path(project_root, path) for path in allowed))
1360
+ raise DispatchError(
1361
+ f"agent result link is not one of this dispatch's completion paths: "
1362
+ f"linked {result_relative}, expected one of {expected}"
1363
+ )
1364
+
1365
+
1366
+ def abandon_agent_dispatch_attempt(
1367
+ *,
1368
+ project_root: Path,
1369
+ run_manifest_path: Path,
1370
+ invocation_ref: str,
1371
+ reason: str,
1372
+ ) -> dict[str, Any]:
1373
+ """Close a started attempt whose worker died without producing a result.
1374
+
1375
+ Every other path that closes an attempt needs something the dead worker
1376
+ never made: `link-result` needs its result file, and
1377
+ `_abandon_unstarted_attempt` only runs when the dispatch itself failed
1378
+ before the process existed. A worker that started and then died — its host
1379
+ killed, its pane closed, an import error before any log — leaves the attempt
1380
+ `started` forever, and `_validate_next_attempt` then refuses every retry with
1381
+ `previous attempt is not terminal`. The run has to be re-rendered from
1382
+ scratch to make progress.
1383
+
1384
+ **Only a `source-readonly` attempt may be abandoned.** The manifest accepts a
1385
+ following attempt only after `failed-no-mutation`, so closing an attempt is
1386
+ also an assertion that nothing was written. For a read-only worker that
1387
+ assertion is true by policy — it never held permission to write — so no
1388
+ guess is involved. For a `project-mutation` attempt it would be a guess, and
1389
+ a wrong one lets the retry run on top of a half-mutated tree, which is the
1390
+ invariant this manifest exists to protect. That case is refused here and
1391
+ belongs to the mutation audit.
1392
+ """
1393
+ if not reason.strip():
1394
+ raise DispatchError(
1395
+ "an abandonment reason is required — the ledger has to say why an "
1396
+ "attempt was closed without a result"
1397
+ )
1398
+ project_root = project_root.resolve()
1399
+ manifest_path = resolve_project_path(
1400
+ project_root, str(run_manifest_path)
1401
+ ).resolve(strict=True)
1402
+ manifest = load_json_object(manifest_path, "run manifest")
1403
+ execution = read_execution_manifest(manifest_path)
1404
+ attempts = [
1405
+ row for row in execution.attempts if row.invocation_ref == invocation_ref
1406
+ ]
1407
+ if not attempts:
1408
+ raise DispatchError(f"no attempt for invocation {invocation_ref}")
1409
+ row = max(attempts, key=lambda item: item.attempt)
1410
+ if row.finished_at is not None:
1411
+ raise DispatchError(
1412
+ f"attempt {invocation_ref} #{row.attempt} is already terminal "
1413
+ f"({row.status})"
1414
+ )
1415
+ mode = _attempt_source_mode(row)
1416
+ if mode != "source-readonly":
1417
+ raise DispatchError(
1418
+ f"attempt {invocation_ref} #{row.attempt} runs under sourcePolicy "
1419
+ f"`{mode}` — only a source-readonly attempt may be abandoned, "
1420
+ "because closing one asserts that nothing was written. Resolve a "
1421
+ "mutating attempt through its mutation audit."
1422
+ )
1423
+ finish_attempt_mutation(
1424
+ manifest_path,
1425
+ invocation_ref=invocation_ref,
1426
+ attempt=row.attempt,
1427
+ finished_at=utc_now(),
1428
+ status="failed-no-mutation",
1429
+ result_path=None,
1430
+ error_path=None,
1431
+ change_summary={"abandonedReason": reason.strip()},
1432
+ task_key=require_string(manifest, "taskKey"),
1433
+ )
1434
+ return {
1435
+ "invocationRef": invocation_ref,
1436
+ "attempt": row.attempt,
1437
+ "status": "failed-no-mutation",
1438
+ "reason": reason.strip(),
1439
+ }
1440
+
1441
+
1442
+ def _attempt_source_mode(row: Any) -> str:
1443
+ policy = getattr(row, "write_policy", None)
1444
+ source = policy.get("sourcePolicy") if isinstance(policy, Mapping) else None
1445
+ mode = source.get("mode") if isinstance(source, Mapping) else None
1446
+ return str(mode or "unknown")
1447
+
1448
+
1293
1449
  def _relative_project_path(project_root: Path, path: Path) -> str:
1294
1450
  try:
1295
1451
  return path.resolve(strict=False).relative_to(project_root).as_posix()
@@ -1307,6 +1463,55 @@ def worker_state(team_state: Mapping[str, Any], worker_id: str) -> Mapping[str,
1307
1463
  raise DispatchError(f"team-state has no workerId={worker_id}")
1308
1464
 
1309
1465
 
1466
+ def _task_project_root(team_state_path: Path) -> Path | None:
1467
+ """team-state 경로에서 프로젝트 루트를 되짚는다 (`.okstra` 를 기준점으로).
1468
+
1469
+ stage run 은 `runs/<phase>/stage-N/state/` 로 한 층이 더 깊으므로 부모를
1470
+ 세는 방식은 빗나간다. 기준점을 못 찾으면 None — 예상 밖 배치에서 판정을
1471
+ 포기하는 쪽이, 실행을 통째로 막는 쪽보다 낫다. 최종 검증이 같은 모순을
1472
+ 다시 잡는다.
1473
+ """
1474
+ for parent in team_state_path.resolve().parents:
1475
+ if parent.name == ".okstra":
1476
+ return parent.parent
1477
+ return None
1478
+
1479
+
1480
+ def _reject_completed_without_result(
1481
+ team_state_path: Path, worker_id: str, worker: Mapping[str, Any]
1482
+ ) -> None:
1483
+ """산출물이 없는 역할을 완료로 적지 못하게 한다.
1484
+
1485
+ okstra 는 디스패치 결과를 스스로 기록한다. 그 원장이 두 시도 모두 `error`
1486
+ 라고 적은 역할이 명부에는 `completed` 로 남을 수 있었다 — 명부 상태가
1487
+ 원장에서 파생되지 않기 때문이다. 모순은 phase 끝 `validate-run` 이
1488
+ "completed but worker result file is missing" 으로 잡지만, 그때는 워커를
1489
+ 다시 돌릴 수 없어 실행 전체가 막힌다.
1490
+
1491
+ 조인 대신 명부 자신의 `resultPath` 를 본다. 그 값이 없는 역할(critic scope
1492
+ 등)은 판정 대상이 아니다.
1493
+
1494
+ 실측(2026-08-27, `fontsninja-v3-site` `dev-10341`): 명부 5건 중 `resultPath`
1495
+ 를 가진 4건에서 3건은 파일이 있었고, grok-planner 만 없었다.
1496
+ """
1497
+ relative = str(worker.get("resultPath") or "").strip()
1498
+ if not relative:
1499
+ return
1500
+ candidate = Path(relative)
1501
+ if not candidate.is_absolute():
1502
+ project_root = _task_project_root(team_state_path)
1503
+ if project_root is None:
1504
+ return
1505
+ candidate = project_root / candidate
1506
+ if candidate.is_file():
1507
+ return
1508
+ raise DispatchError(
1509
+ f"worker {worker_id} cannot be recorded completed: its result "
1510
+ f"{relative} does not exist. Record the terminal status the dispatch "
1511
+ "actually reached (`error` / `timeout` / `not-run`) with a reason."
1512
+ )
1513
+
1514
+
1310
1515
  def transition_worker_status(
1311
1516
  team_state_path: Path,
1312
1517
  worker_id: str,
@@ -1332,6 +1537,10 @@ def transition_worker_status(
1332
1537
  raise DispatchError(f"team-state workers must be an array: {team_state_path}")
1333
1538
  for worker in workers:
1334
1539
  if isinstance(worker, dict) and worker.get("workerId") == worker_id:
1540
+ if status == "completed":
1541
+ _reject_completed_without_result(
1542
+ team_state_path, worker_id, worker
1543
+ )
1335
1544
  worker["status"] = status
1336
1545
  worker["reason"] = reason
1337
1546
  if status == "in-progress":
@@ -1400,25 +1609,6 @@ def _has_recorded_team_create(team_state: Mapping[str, Any]) -> bool:
1400
1609
 
1401
1610
  # --- job facts ----------------------------------------------------------------
1402
1611
 
1403
- def build_dispatch_id(invocation_id: str, attempt: int) -> str:
1404
- """The only place Python assembles a dispatch id.
1405
-
1406
- Two generators drifted: this module pinned the attempt to 1 while
1407
- `dispatch_core` used the real one, so a re-dispatch took the first attempt's
1408
- id, team-state kept the same row twice, and `validate-run` failed that run
1409
- with `agent dispatch ID is duplicated`.
1410
-
1411
- Prose mirrors of this shape live outside Python, where no guard reaches
1412
- them: `prompts/lead/convergence.md`, `prompts/lead/report-writer.md`,
1413
- `prompts/lead/plan-body-verification.md` and the five
1414
- `adapters/hosts/*/relay.md` each spell an `--dispatch-id` argument out for
1415
- the lead. They agree with this function only because
1416
- `record_verified_agent_dispatch` records the first attempt and no other;
1417
- changing that convention has to change them too.
1418
- """
1419
- return f"{invocation_id}:attempt-{attempt}"
1420
-
1421
-
1422
1612
  def dispatch_mode(jobs: Sequence[WorkerJob]) -> str:
1423
1613
  backends = {job.backend for job in jobs}
1424
1614
  if len(backends) == 1:
@@ -1427,7 +1617,18 @@ def dispatch_mode(jobs: Sequence[WorkerJob]) -> str:
1427
1617
 
1428
1618
 
1429
1619
  def missing_completion_paths(job: WorkerJob) -> tuple[Path, ...]:
1430
- return tuple(path for path in job.completion_paths if not path.is_file())
1620
+ missing: list[Path] = []
1621
+ for path in job.completion_paths:
1622
+ if path.is_file():
1623
+ continue
1624
+ # reports seq 와 workerResults seq 가 갈라지면 워커는 다른 쪽
1625
+ # 파일명으로 쓴다. 둘 중 하나가 있으면 산출물은 있는 것이다.
1626
+ if path in {job.result_path, job.worker_result_path} and any(
1627
+ alias.is_file() for alias in job.result_aliases
1628
+ ):
1629
+ continue
1630
+ missing.append(path)
1631
+ return tuple(missing)
1431
1632
 
1432
1633
 
1433
1634
  def dispatch_session_id(provider: str) -> str:
@@ -1534,6 +1735,32 @@ def dispatch_completion_paths(
1534
1735
  return (data_json, worker_result_path)
1535
1736
 
1536
1737
 
1738
+ def _plan_verify_result_aliases(
1739
+ worker_result_path: Path, manifest: Mapping[str, Any],
1740
+ ) -> tuple[Path, ...]:
1741
+ """reports seq 파일의 workerResults seq 짝.
1742
+
1743
+ plan-verify 결과 파일명만 해당한다. 다른 산출물의 seq 를 바꾸면 안 된다.
1744
+ """
1745
+ if "-plan-verify-r" not in worker_result_path.name:
1746
+ return ()
1747
+ sequences = manifest.get("runSequencesByCategory")
1748
+ if not isinstance(sequences, Mapping):
1749
+ return ()
1750
+ reports = str(sequences.get("reports") or "").strip()
1751
+ workers = str(sequences.get("workerResults") or "").strip()
1752
+ if not reports or not workers or reports == workers:
1753
+ return ()
1754
+ name = worker_result_path.name
1755
+ if name.endswith(f"-{reports}.md"):
1756
+ other = name[: -len(f"-{reports}.md")] + f"-{workers}.md"
1757
+ elif name.endswith(f"-{workers}.md"):
1758
+ other = name[: -len(f"-{workers}.md")] + f"-{reports}.md"
1759
+ else:
1760
+ return ()
1761
+ return (worker_result_path.with_name(other),)
1762
+
1763
+
1537
1764
  def validate_initial_prompts(
1538
1765
  manifest: Mapping[str, Any], jobs: Sequence[WorkerJob]
1539
1766
  ) -> None:
@@ -1937,6 +2164,7 @@ def _worker_job_from_file(
1937
2164
  completion_paths = dispatch_completion_paths(
1938
2165
  worker_id, worker_result_path, manifest, project_root
1939
2166
  )
2167
+ result_aliases = _plan_verify_result_aliases(worker_result_path, manifest)
1940
2168
  digests = item.get("digests")
1941
2169
  digest_values = digests if isinstance(digests, Mapping) else {}
1942
2170
  host_model_value = item.get("hostModelValue")
@@ -1985,6 +2213,7 @@ def _worker_job_from_file(
1985
2213
  prompt_digest=string_value(digest_values.get("promptDigest")),
1986
2214
  host_model_value=host_model_value,
1987
2215
  enforcement_mode=string_value(item.get("enforcementMode")),
2216
+ result_aliases=result_aliases,
1988
2217
  **(identity or {}),
1989
2218
  )
1990
2219
 
@@ -0,0 +1,48 @@
1
+ """`okstra doctor` 가 부르는 진단 프로젝션.
2
+
3
+ 진단 로직 자체는 `okstra_ctl.doctor` 에 있다. 이 모듈은 그것을 CLI 경계로
4
+ 노출하기만 한다 — 원래 `src/commands/lifecycle/doctor.mts` 의 문자열 리터럴
5
+ 안에 있던 두 스니펫을 옮긴 것이다(docs/coding-rules.md R1 위반).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+
12
+ from .doctor import model_pool_diagnostics, phase_diagnostics
13
+
14
+
15
+ def main(argv: list[str] | None = None) -> int:
16
+ parser = argparse.ArgumentParser(prog="okstra doctor")
17
+ sub = parser.add_subparsers(dest="op", required=True)
18
+
19
+ pool = sub.add_parser("model-pool")
20
+ pool.add_argument("--host-runtime", default="claude-code")
21
+ pool.add_argument("--cwd", required=True)
22
+
23
+ phase = sub.add_parser("phase")
24
+ phase.add_argument("--phase", required=True)
25
+ phase.add_argument("--cwd", required=True)
26
+ phase.add_argument("--workspace-root", required=True)
27
+ # `--home` 은 사용자 홈이다. 에이전트 설치 루트가 `<home>/.claude/agents` 라서
28
+ # okstra 홈(~/.okstra)을 넘기면 모든 worker-agent 점검이 없는 경로를 짚는다.
29
+ phase.add_argument("--home", required=True)
30
+ phase.add_argument("--host-runtime", default="")
31
+
32
+ args = parser.parse_args(argv)
33
+ if args.op == "model-pool":
34
+ payload = model_pool_diagnostics(host_runtime=args.host_runtime, cwd=args.cwd)
35
+ else:
36
+ payload = phase_diagnostics(
37
+ args.phase,
38
+ cwd=args.cwd,
39
+ workspace_root=args.workspace_root,
40
+ home=args.home,
41
+ host_runtime=args.host_runtime,
42
+ )
43
+ print(json.dumps(payload, ensure_ascii=False))
44
+ return 0
45
+
46
+
47
+ if __name__ == "__main__":
48
+ raise SystemExit(main())
@@ -12,9 +12,12 @@ from pathlib import Path
12
12
  from typing import Literal, Protocol, runtime_checkable
13
13
 
14
14
  from .worker_presentation import Presentation
15
- from ..write_policy import WriteEnforcement, WritePolicy
15
+ from .write_policy import WriteEnforcement, WritePolicy
16
16
 
17
17
  SERVED_MODEL_MISMATCH_EXIT_CODE = 78
18
+ # JSON 이벤트 스트림이 result 이벤트 없이 깨끗하게(exit 0) 끝난 run.
19
+ # CLI 가 성공을 주장했지만 답을 내지 않았다 — sysexits 의 EX_PROTOCOL.
20
+ NO_RESULT_EXIT_CODE = 76
18
21
 
19
22
 
20
23
  @dataclass(frozen=True)
@@ -22,10 +25,12 @@ class ExecutionPolicy:
22
25
  """How a non-interactive worker is allowed to act.
23
26
 
24
27
  Identical for every worker: nobody is at the keyboard to answer an approval
25
- prompt, so the gate has to be open and the boundary has to come from a
26
- sandbox instead. Declared once here rather than per provider, because a
27
- policy that lives in one adapter file per provider drifts into one policy
28
- per provider.
28
+ prompt, so the gate has to be open. No provider CLI enforces a sandbox
29
+ boundary any more, so `write_scope` is what the worker is told to write, not
30
+ what it is prevented from leaving — `write_policy` and `write_enforcement`
31
+ are what the run checks after the fact. Declared once here rather than per
32
+ provider, because a policy that lives in one adapter file per provider
33
+ drifts into one policy per provider.
29
34
  """
30
35
 
31
36
  auto_approve: bool
@@ -117,11 +117,24 @@ class JsonEvents:
117
117
  # 않으면 pane 이 한 글자 한 줄이 된다. 기본은 끄고, 그런 CLI 의 어댑터가 켠다 —
118
118
  # 어느 CLI 가 그런지는 이 계층이 알 필요도, 알아서도 안 되는 사실이다.
119
119
  join_adjacent_text: bool = False
120
+ # 이 어휘에서 "정상 종결" 을 뜻하는 원시 이벤트. 기본은 Result 이벤트지만,
121
+ # 어떤 CLI 의 어휘에는 Result 가 없고 별도의 종결 이벤트가 있다 — 어댑터가
122
+ # 자기 어휘의 표지를 넘긴다. 어느 CLI 가 그런지는 이 계층이 알 필요도,
123
+ # 알아서도 안 되는 사실이다. 판정 없이는 그런 CLI 의 건강한 run 전부가
124
+ # 결과 부재로 강등된다.
125
+ is_conclusion: Callable[[Mapping[str, Any]], bool] | None = None
120
126
  _pending_text: list[str] = field(default_factory=list, repr=False, compare=False)
127
+ _saw_conclusion: list[bool] = field(
128
+ default_factory=list, repr=False, compare=False
129
+ )
121
130
 
122
131
  def merges_stderr(self) -> bool:
123
132
  return True
124
133
 
134
+ def concluded(self) -> bool:
135
+ """스트림이 이 어휘의 종결 표지로 끝났는가."""
136
+ return bool(self._saw_conclusion)
137
+
125
138
  def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]:
126
139
  def emit(line: str) -> str | None:
127
140
  stripped = line.strip()
@@ -138,6 +151,8 @@ class JsonEvents:
138
151
  if not isinstance(event, dict):
139
152
  return None
140
153
  self.observe(event)
154
+ if self.is_conclusion is not None and self.is_conclusion(event):
155
+ self._saw_conclusion.append(True)
141
156
  closing: str | None = None
142
157
  for entry in self.normalise(event):
143
158
  closing = self._write_entry(writer, entry) or closing
@@ -164,7 +179,12 @@ class JsonEvents:
164
179
  screen=format_live(entry),
165
180
  archive=format_log(entry),
166
181
  )
167
- return final_text(entry)
182
+ closing = final_text(entry)
183
+ if closing is not None:
184
+ # Result 이벤트는 어휘와 무관한 종결이다. 빈 결과 텍스트도 종결이므로
185
+ # closing_text 의 진릿값과 따로 기록한다.
186
+ self._saw_conclusion.append(True)
187
+ return closing
168
188
 
169
189
  def _publish_complete_lines(self, writer: TranscriptWriter) -> None:
170
190
  combined = "".join(self._pending_text)