okstra 0.186.7 → 0.188.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 (546) 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 +107 -40
  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 +39 -38
  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 +47 -22
  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/adapters/cmux.md +3 -3
  100. package/runtime/prompts/lead/convergence.md +99 -88
  101. package/runtime/prompts/lead/okstra-lead-contract.md +63 -12
  102. package/runtime/prompts/lead/plan-body-verification.md +65 -46
  103. package/runtime/prompts/lead/report-writer.md +35 -5
  104. package/runtime/prompts/lead/team-contract.md +4 -1
  105. package/runtime/prompts/profiles/_clarification-recommendation.md +2 -1
  106. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  107. package/runtime/prompts/profiles/_common-contract.md +3 -3
  108. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  109. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  110. package/runtime/prompts/profiles/_implementation-executor.md +3 -1
  111. package/runtime/prompts/profiles/_implementation-verifier.md +16 -2
  112. package/runtime/prompts/profiles/error-analysis.md +4 -3
  113. package/runtime/prompts/profiles/final-verification.md +2 -2
  114. package/runtime/prompts/profiles/implementation-planning.md +22 -17
  115. package/runtime/prompts/profiles/implementation.md +3 -2
  116. package/runtime/prompts/profiles/requirements-discovery.md +2 -1
  117. package/runtime/prompts/wizard/prompts.ko.json +29 -8
  118. package/runtime/python/okstra_ctl/__init__.py +6 -27
  119. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -5
  120. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +21 -2
  121. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +3 -12
  122. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +63 -22
  123. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +20 -21
  124. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +37 -20
  125. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +86 -14
  126. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +12 -12
  127. package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +4 -7
  128. package/runtime/python/okstra_ctl/agent/__init__.py +1 -0
  129. package/runtime/python/okstra_ctl/{agent_activity.py → agent/activity.py} +16 -2
  130. package/runtime/python/okstra_ctl/{agent_invocation.py → agent/invocation.py} +34 -5
  131. package/runtime/python/okstra_ctl/agent/prompt_cli/__init__.py +19 -0
  132. package/runtime/python/okstra_ctl/agent/prompt_cli/__main__.py +11 -0
  133. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +228 -0
  134. package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +155 -0
  135. package/runtime/python/okstra_ctl/agent/prompt_cli/emit.py +66 -0
  136. package/runtime/python/okstra_ctl/agent/prompt_cli/inputs.py +138 -0
  137. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +605 -0
  138. package/runtime/python/okstra_ctl/agent/prompt_cli/results.py +197 -0
  139. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +107 -0
  140. package/runtime/python/okstra_ctl/analysis_packet.py +47 -4
  141. package/runtime/python/okstra_ctl/approval_decisions.py +287 -10
  142. package/runtime/python/okstra_ctl/assignment_resolver.py +0 -12
  143. package/runtime/python/okstra_ctl/attempt_evidence.py +12 -24
  144. package/runtime/python/okstra_ctl/blocking_checks.py +251 -0
  145. package/runtime/python/okstra_ctl/brief_frontmatter.py +0 -7
  146. package/runtime/python/okstra_ctl/clarification_items/__init__.py +102 -0
  147. package/runtime/python/okstra_ctl/clarification_items/carry.py +224 -0
  148. package/runtime/python/okstra_ctl/clarification_items/dispositions.py +135 -0
  149. package/runtime/python/okstra_ctl/clarification_items/parsing.py +253 -0
  150. package/runtime/python/okstra_ctl/clarification_items/rows.py +115 -0
  151. package/runtime/python/okstra_ctl/clarification_items/scan.py +215 -0
  152. package/runtime/python/okstra_ctl/clarification_items/sidecars.py +211 -0
  153. package/runtime/python/okstra_ctl/cmux.py +38 -31
  154. package/runtime/python/okstra_ctl/code_review_target.py +163 -1
  155. package/runtime/python/okstra_ctl/conformance.py +23 -0
  156. package/runtime/python/okstra_ctl/container.py +38 -421
  157. package/runtime/python/okstra_ctl/context_cost.py +32 -4
  158. package/runtime/python/okstra_ctl/contract_graph_cli.py +7 -0
  159. package/runtime/python/okstra_ctl/convergence.py +107 -16
  160. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +345 -0
  161. package/runtime/python/okstra_ctl/convergence_engine.py +382 -70
  162. package/runtime/python/okstra_ctl/convergence_store.py +5 -29
  163. package/runtime/python/okstra_ctl/design_prep.py +108 -12
  164. package/runtime/python/okstra_ctl/design_snapshot.py +11 -1
  165. package/runtime/python/okstra_ctl/design_surfaces.py +29 -2
  166. package/runtime/python/okstra_ctl/dispatch_core.py +236 -49
  167. package/runtime/python/okstra_ctl/dispatch_state.py +417 -41
  168. package/runtime/python/okstra_ctl/doctor_cli.py +48 -0
  169. package/runtime/python/okstra_ctl/domain/worker_exec.py +10 -5
  170. package/runtime/python/okstra_ctl/domain/worker_presentation.py +28 -1
  171. package/runtime/python/okstra_ctl/domain/worker_stream.py +52 -21
  172. package/runtime/python/okstra_ctl/domain/write_policy.py +219 -0
  173. package/runtime/python/okstra_ctl/entrypoints/hosts.py +9 -2
  174. package/runtime/python/okstra_ctl/error_log_core.py +1 -1
  175. package/runtime/python/okstra_ctl/error_report.py +14 -0
  176. package/runtime/python/okstra_ctl/error_zip.py +18 -2
  177. package/runtime/python/okstra_ctl/exact_coverage.py +5 -0
  178. package/runtime/python/okstra_ctl/execution_identity.py +100 -7
  179. package/runtime/python/okstra_ctl/execution_manifest.py +67 -20
  180. package/runtime/python/okstra_ctl/execution_mutation_audit.py +90 -8
  181. package/runtime/python/okstra_ctl/final_report_paths.py +17 -0
  182. package/runtime/python/okstra_ctl/final_report_schema.py +319 -17
  183. package/runtime/python/okstra_ctl/git_reconcile.py +22 -2
  184. package/runtime/python/okstra_ctl/handoff.py +24 -1
  185. package/runtime/python/okstra_ctl/ids.py +6 -16
  186. package/runtime/python/okstra_ctl/implementation_direction.py +142 -33
  187. package/runtime/python/okstra_ctl/implementation_options.py +32 -13
  188. package/runtime/python/okstra_ctl/implementation_outcome.py +5 -2
  189. package/runtime/python/okstra_ctl/improvement_lenses.py +0 -14
  190. package/runtime/python/okstra_ctl/incremental_carry.py +69 -8
  191. package/runtime/python/okstra_ctl/incremental_scope.py +173 -12
  192. package/runtime/python/okstra_ctl/index.py +2 -2
  193. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +48 -8
  194. package/runtime/python/okstra_ctl/interactive_cli.py +223 -0
  195. package/runtime/python/okstra_ctl/json_boundary.py +10 -0
  196. package/runtime/python/okstra_ctl/listing.py +0 -91
  197. package/runtime/python/okstra_ctl/locks.py +5 -19
  198. package/runtime/python/okstra_ctl/log_report.py +14 -0
  199. package/runtime/python/okstra_ctl/manager_cli.py +22 -3
  200. package/runtime/python/okstra_ctl/manager_launch.py +4 -8
  201. package/runtime/python/okstra_ctl/material.py +2 -2
  202. package/runtime/python/okstra_ctl/migrate.py +23 -1
  203. package/runtime/python/okstra_ctl/model_cli.py +19 -5
  204. package/runtime/python/okstra_ctl/model_discovery.py +46 -47
  205. package/runtime/python/okstra_ctl/model_io/__init__.py +1 -0
  206. package/runtime/python/okstra_ctl/model_io/lines.py +163 -0
  207. package/runtime/python/okstra_ctl/model_io/references.py +370 -0
  208. package/runtime/python/okstra_ctl/model_io/renderers.py +498 -0
  209. package/runtime/python/okstra_ctl/model_io_cli.py +30 -945
  210. package/runtime/python/okstra_ctl/model_pool.py +10 -6
  211. package/runtime/python/okstra_ctl/models.py +8 -8
  212. package/runtime/python/okstra_ctl/mutation_recovery.py +5 -63
  213. package/runtime/python/okstra_ctl/next_phase.py +121 -69
  214. package/runtime/python/okstra_ctl/pane_reclaim.py +23 -0
  215. package/runtime/python/okstra_ctl/pane_title.py +11 -8
  216. package/runtime/python/okstra_ctl/path_hints.py +36 -11
  217. package/runtime/python/okstra_ctl/paths.py +76 -8
  218. package/runtime/python/okstra_ctl/plan_items.py +132 -16
  219. package/runtime/python/okstra_ctl/plan_items_cli.py +486 -36
  220. package/runtime/python/okstra_ctl/plan_run_root.py +1 -1
  221. package/runtime/python/okstra_ctl/plan_validate_cli.py +51 -0
  222. package/runtime/python/okstra_ctl/plan_verify_cli.py +80 -0
  223. package/runtime/python/okstra_ctl/prepare_error.py +20 -0
  224. package/runtime/python/okstra_ctl/prior_planning.py +157 -0
  225. package/runtime/python/okstra_ctl/profile_show.py +18 -1
  226. package/runtime/python/okstra_ctl/project_setup_cli.py +140 -0
  227. package/runtime/python/okstra_ctl/recap.py +22 -1
  228. package/runtime/python/okstra_ctl/reconcile.py +84 -0
  229. package/runtime/python/okstra_ctl/registry/host_registry.py +18 -1
  230. package/runtime/python/okstra_ctl/render.py +98 -32
  231. package/runtime/python/okstra_ctl/render_final_report.py +4 -8
  232. package/runtime/python/okstra_ctl/report_assembly.py +297 -7
  233. package/runtime/python/okstra_ctl/report_contract.py +30 -7
  234. package/runtime/python/okstra_ctl/report_finalize.py +213 -16
  235. package/runtime/python/okstra_ctl/report_html/common.py +2 -2
  236. package/runtime/python/okstra_ctl/report_html/render.py +8 -12
  237. package/runtime/python/okstra_ctl/report_language.py +8 -4
  238. package/runtime/python/okstra_ctl/report_narrative.py +220 -34
  239. package/runtime/python/okstra_ctl/report_projections.py +8 -2
  240. package/runtime/python/okstra_ctl/report_synthesis_packet.py +393 -1
  241. package/runtime/python/okstra_ctl/report_translation.py +2 -35
  242. package/runtime/python/okstra_ctl/report_view_artifacts.py +42 -2
  243. package/runtime/python/okstra_ctl/report_views.py +4 -22
  244. package/runtime/python/okstra_ctl/resolve_task_key.py +13 -0
  245. package/runtime/python/okstra_ctl/rollup.py +13 -0
  246. package/runtime/python/okstra_ctl/run.py +639 -138
  247. package/runtime/python/okstra_ctl/run_audit.py +13 -0
  248. package/runtime/python/okstra_ctl/run_index_row.py +0 -12
  249. package/runtime/python/okstra_ctl/seeding.py +21 -11
  250. package/runtime/python/okstra_ctl/sequence.py +1 -1
  251. package/runtime/python/okstra_ctl/session.py +64 -14
  252. package/runtime/python/okstra_ctl/set_work_status.py +18 -0
  253. package/runtime/python/okstra_ctl/stage_integrate.py +15 -1
  254. package/runtime/python/okstra_ctl/stage_ledger.py +1 -1
  255. package/runtime/python/okstra_ctl/stage_map.py +57 -11
  256. package/runtime/python/okstra_ctl/stage_map_cli.py +83 -0
  257. package/runtime/python/okstra_ctl/stage_map_view.py +65 -0
  258. package/runtime/python/okstra_ctl/stage_targets.py +3 -12
  259. package/runtime/python/okstra_ctl/task_list_cli.py +138 -0
  260. package/runtime/python/okstra_ctl/task_show_cli.py +66 -0
  261. package/runtime/python/okstra_ctl/task_target.py +4 -16
  262. package/runtime/python/okstra_ctl/team.py +49 -2
  263. package/runtime/python/okstra_ctl/time_report.py +17 -2
  264. package/runtime/python/okstra_ctl/usage_report.py +11 -0
  265. package/runtime/python/okstra_ctl/user_response.py +55 -218
  266. package/runtime/python/okstra_ctl/user_response_values.py +242 -0
  267. package/runtime/python/okstra_ctl/validation_contract.py +3 -0
  268. package/runtime/python/okstra_ctl/verdict_blocks.py +28 -8
  269. package/runtime/python/okstra_ctl/verification_target.py +68 -0
  270. package/runtime/python/okstra_ctl/wizard.py +457 -92
  271. package/runtime/python/okstra_ctl/worker_artifacts.py +75 -16
  272. package/runtime/python/okstra_ctl/worker_audit_check.py +22 -0
  273. package/runtime/python/okstra_ctl/worker_dispatch.py +21 -0
  274. package/runtime/python/okstra_ctl/worker_liveness.py +33 -0
  275. package/runtime/python/okstra_ctl/worker_prompt_body.py +15 -2
  276. package/runtime/python/okstra_ctl/worker_prompt_contract.py +98 -7
  277. package/runtime/python/okstra_ctl/worker_prompt_headers.py +24 -2
  278. package/runtime/python/okstra_ctl/worker_prompt_policy.py +18 -3
  279. package/runtime/python/okstra_ctl/worker_request.py +2 -1
  280. package/runtime/python/okstra_ctl/worker_runner.py +58 -17
  281. package/runtime/python/okstra_ctl/worker_state.py +16 -1
  282. package/runtime/python/okstra_ctl/workflow.py +18 -1
  283. package/runtime/python/okstra_ctl/worktree/__init__.py +92 -0
  284. package/runtime/python/okstra_ctl/worktree/cleanliness.py +89 -0
  285. package/runtime/python/okstra_ctl/worktree/decisions.py +127 -0
  286. package/runtime/python/okstra_ctl/worktree/git_ops.py +165 -0
  287. package/runtime/python/okstra_ctl/worktree/linking.py +249 -0
  288. package/runtime/python/okstra_ctl/worktree/naming.py +91 -0
  289. package/runtime/python/okstra_ctl/worktree/provision.py +385 -0
  290. package/runtime/python/okstra_ctl/worktree/sync_config.py +181 -0
  291. package/runtime/python/okstra_ctl/worktree_cli.py +75 -0
  292. package/runtime/python/okstra_ctl/worktree_lookup_cli.py +39 -0
  293. package/runtime/python/okstra_ctl/worktree_registry.py +7 -0
  294. package/runtime/python/okstra_ctl/worktree_status_cli.py +53 -0
  295. package/runtime/python/okstra_ctl/write_policy.py +90 -213
  296. package/runtime/python/okstra_project/__init__.py +0 -4
  297. package/runtime/python/okstra_project/dirs.py +2 -2
  298. package/runtime/python/okstra_project/phase_pointer.py +84 -0
  299. package/runtime/python/okstra_project/slug.py +24 -0
  300. package/runtime/python/okstra_project/state.py +22 -230
  301. package/runtime/python/okstra_token_usage/antigravity.py +50 -12
  302. package/runtime/python/okstra_token_usage/claude.py +9 -1
  303. package/runtime/python/okstra_token_usage/cli.py +3 -1
  304. package/runtime/python/okstra_token_usage/collect.py +171 -36
  305. package/runtime/python/okstra_token_usage/cursor.py +2 -0
  306. package/runtime/python/okstra_token_usage/grok.py +1 -5
  307. package/runtime/python/okstra_token_usage/paths.py +26 -0
  308. package/runtime/python/okstra_token_usage/pricing.py +23 -8
  309. package/runtime/schemas/convergence-critic-results-v1.0.schema.json +5 -0
  310. package/runtime/schemas/execution-manifest-v2.schema.json +10 -10
  311. package/runtime/schemas/final-report-v2.0.schema.json +9 -26
  312. package/runtime/schemas/final-report-v3.0.schema.json +173 -41
  313. package/runtime/schemas/report-narrative-v3.0.schema.json +3 -2
  314. package/runtime/skills/okstra-brief-gen/SKILL.md +35 -2
  315. package/runtime/skills/okstra-container-build/SKILL.md +16 -47
  316. package/runtime/skills/okstra-inspect/facets/history.md +1 -1
  317. package/runtime/skills/okstra-inspect/facets/status.md +7 -6
  318. package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
  319. package/runtime/skills/okstra-run/SKILL.md +7 -5
  320. package/runtime/templates/report-writer-prompt-preamble.md +3 -1
  321. package/runtime/templates/reports/brief.template.md +6 -2
  322. package/runtime/templates/reports/error-analysis-input.template.md +2 -0
  323. package/runtime/templates/reports/final-verification-input.template.md +2 -0
  324. package/runtime/templates/reports/implementation-input.template.md +7 -1
  325. package/runtime/templates/reports/implementation-planning-input.template.md +4 -0
  326. package/runtime/templates/reports/quick-input.template.md +2 -0
  327. package/runtime/templates/reports/release-handoff-input.template.md +6 -1
  328. package/runtime/templates/reports/report.js +20 -3
  329. package/runtime/templates/reports/schedule.template.md +2 -0
  330. package/runtime/templates/reports/settings.template.json +0 -10
  331. package/runtime/templates/reports/task-brief.template.md +3 -0
  332. package/runtime/templates/reports/user-response.template.md +7 -3
  333. package/runtime/validators/checks/fixtures-01.py +57 -0
  334. package/runtime/validators/checks/fixtures-02.py +565 -0
  335. package/runtime/validators/checks/runners-01.py +108 -0
  336. package/runtime/validators/checks/validate-assets-01.py +60 -0
  337. package/runtime/validators/checks/validate-prompt-metadata-01.py +261 -0
  338. package/runtime/validators/checks/validate-tasks-01.py +46 -0
  339. package/runtime/validators/checks/validate-tasks-02.py +85 -0
  340. package/runtime/validators/checks/validate-tasks-03.py +61 -0
  341. package/runtime/validators/checks/validate-tasks-04.py +118 -0
  342. package/runtime/validators/forbidden_actions.py +73 -3
  343. package/runtime/validators/lib/common.sh +5 -0
  344. package/runtime/validators/lib/fixtures.sh +7 -591
  345. package/runtime/validators/lib/paths.sh +13 -0
  346. package/runtime/validators/lib/runners.sh +6 -104
  347. package/runtime/validators/lib/summary.sh +1 -1
  348. package/runtime/validators/lib/validate-assets.sh +6 -56
  349. package/runtime/validators/lib/validate-prompt-metadata.sh +6 -257
  350. package/runtime/validators/lib/validate-tasks.sh +9 -294
  351. package/runtime/validators/validate-implementation-plan-stages.py +4 -4
  352. package/runtime/validators/validate-run.py +1363 -2643
  353. package/runtime/validators/validate-workflow.sh +56 -16
  354. package/runtime/validators/validate_improvement_report.py +2 -1
  355. package/runtime/validators/validate_session_conformance.py +295 -49
  356. package/dist/commands/execute/agent-prompt.d.mts +0 -1
  357. package/dist/commands/execute/agent-prompt.mjs +0 -24
  358. package/dist/commands/execute/agent-prompt.mjs.map +0 -1
  359. package/dist/commands/execute/codex-dispatch.d.mts +0 -3
  360. package/dist/commands/execute/codex-dispatch.mjs +0 -6
  361. package/dist/commands/execute/codex-dispatch.mjs.map +0 -1
  362. package/dist/commands/execute/codex-run.d.mts +0 -3
  363. package/dist/commands/execute/codex-run.mjs +0 -62
  364. package/dist/commands/execute/codex-run.mjs.map +0 -1
  365. package/dist/commands/execute/convergence.d.mts +0 -1
  366. package/dist/commands/execute/convergence.mjs +0 -38
  367. package/dist/commands/execute/convergence.mjs.map +0 -1
  368. package/dist/commands/execute/error-log.d.mts +0 -1
  369. package/dist/commands/execute/error-log.mjs +0 -18
  370. package/dist/commands/execute/error-log.mjs.map +0 -1
  371. package/dist/commands/execute/git-reconcile.d.mts +0 -1
  372. package/dist/commands/execute/git-reconcile.mjs +0 -30
  373. package/dist/commands/execute/git-reconcile.mjs.map +0 -1
  374. package/dist/commands/execute/handoff.d.mts +0 -1
  375. package/dist/commands/execute/handoff.mjs +0 -31
  376. package/dist/commands/execute/handoff.mjs.map +0 -1
  377. package/dist/commands/execute/incremental-carry.d.mts +0 -1
  378. package/dist/commands/execute/incremental-carry.mjs +0 -20
  379. package/dist/commands/execute/incremental-carry.mjs.map +0 -1
  380. package/dist/commands/execute/incremental-scope.d.mts +0 -1
  381. package/dist/commands/execute/incremental-scope.mjs +0 -29
  382. package/dist/commands/execute/incremental-scope.mjs.map +0 -1
  383. package/dist/commands/execute/integrate-stages.d.mts +0 -1
  384. package/dist/commands/execute/integrate-stages.mjs +0 -24
  385. package/dist/commands/execute/integrate-stages.mjs.map +0 -1
  386. package/dist/commands/execute/pane-title.d.mts +0 -1
  387. package/dist/commands/execute/pane-title.mjs +0 -20
  388. package/dist/commands/execute/pane-title.mjs.map +0 -1
  389. package/dist/commands/execute/plan-items.d.mts +0 -1
  390. package/dist/commands/execute/plan-items.mjs +0 -9
  391. package/dist/commands/execute/plan-items.mjs.map +0 -1
  392. package/dist/commands/execute/plan-validate.d.mts +0 -1
  393. package/dist/commands/execute/plan-validate.mjs +0 -68
  394. package/dist/commands/execute/plan-validate.mjs.map +0 -1
  395. package/dist/commands/execute/plan-verify.d.mts +0 -1
  396. package/dist/commands/execute/plan-verify.mjs +0 -43
  397. package/dist/commands/execute/plan-verify.mjs.map +0 -1
  398. package/dist/commands/execute/spawn-followups.d.mts +0 -1
  399. package/dist/commands/execute/spawn-followups.mjs +0 -22
  400. package/dist/commands/execute/spawn-followups.mjs.map +0 -1
  401. package/dist/commands/execute/team.d.mts +0 -3
  402. package/dist/commands/execute/team.mjs +0 -66
  403. package/dist/commands/execute/team.mjs.map +0 -1
  404. package/dist/commands/execute/token-usage.d.mts +0 -1
  405. package/dist/commands/execute/token-usage.mjs +0 -19
  406. package/dist/commands/execute/token-usage.mjs.map +0 -1
  407. package/dist/commands/execute/worker-audit-check.d.mts +0 -1
  408. package/dist/commands/execute/worker-audit-check.mjs +0 -34
  409. package/dist/commands/execute/worker-audit-check.mjs.map +0 -1
  410. package/dist/commands/execute/worker-dispatch.d.mts +0 -7
  411. package/dist/commands/execute/worker-dispatch.mjs +0 -64
  412. package/dist/commands/execute/worker-dispatch.mjs.map +0 -1
  413. package/dist/commands/execute/worker-state.d.mts +0 -1
  414. package/dist/commands/execute/worker-state.mjs +0 -28
  415. package/dist/commands/execute/worker-state.mjs.map +0 -1
  416. package/dist/commands/execute/worktree-lookup.d.mts +0 -1
  417. package/dist/commands/execute/worktree-lookup.mjs +0 -92
  418. package/dist/commands/execute/worktree-lookup.mjs.map +0 -1
  419. package/dist/commands/execute/worktree-status.d.mts +0 -1
  420. package/dist/commands/execute/worktree-status.mjs +0 -121
  421. package/dist/commands/execute/worktree-status.mjs.map +0 -1
  422. package/dist/commands/inspect/code-review.d.mts +0 -1
  423. package/dist/commands/inspect/code-review.mjs +0 -32
  424. package/dist/commands/inspect/code-review.mjs.map +0 -1
  425. package/dist/commands/inspect/container.d.mts +0 -1
  426. package/dist/commands/inspect/container.mjs +0 -25
  427. package/dist/commands/inspect/container.mjs.map +0 -1
  428. package/dist/commands/inspect/context-cost.d.mts +0 -1
  429. package/dist/commands/inspect/context-cost.mjs +0 -25
  430. package/dist/commands/inspect/context-cost.mjs.map +0 -1
  431. package/dist/commands/inspect/design-prep.d.mts +0 -1
  432. package/dist/commands/inspect/design-prep.mjs +0 -22
  433. package/dist/commands/inspect/design-prep.mjs.map +0 -1
  434. package/dist/commands/inspect/error-report.d.mts +0 -1
  435. package/dist/commands/inspect/error-report.mjs +0 -25
  436. package/dist/commands/inspect/error-report.mjs.map +0 -1
  437. package/dist/commands/inspect/error-zip.d.mts +0 -1
  438. package/dist/commands/inspect/error-zip.mjs +0 -24
  439. package/dist/commands/inspect/error-zip.mjs.map +0 -1
  440. package/dist/commands/inspect/log-report.d.mts +0 -1
  441. package/dist/commands/inspect/log-report.mjs +0 -26
  442. package/dist/commands/inspect/log-report.mjs.map +0 -1
  443. package/dist/commands/inspect/model-io.d.mts +0 -1
  444. package/dist/commands/inspect/model-io.mjs +0 -25
  445. package/dist/commands/inspect/model-io.mjs.map +0 -1
  446. package/dist/commands/inspect/profile-show.d.mts +0 -1
  447. package/dist/commands/inspect/profile-show.mjs +0 -28
  448. package/dist/commands/inspect/profile-show.mjs.map +0 -1
  449. package/dist/commands/inspect/recap.d.mts +0 -1
  450. package/dist/commands/inspect/recap.mjs +0 -30
  451. package/dist/commands/inspect/recap.mjs.map +0 -1
  452. package/dist/commands/inspect/resolve-task-key.d.mts +0 -1
  453. package/dist/commands/inspect/resolve-task-key.mjs +0 -24
  454. package/dist/commands/inspect/resolve-task-key.mjs.map +0 -1
  455. package/dist/commands/inspect/rollup.d.mts +0 -1
  456. package/dist/commands/inspect/rollup.mjs +0 -25
  457. package/dist/commands/inspect/rollup.mjs.map +0 -1
  458. package/dist/commands/inspect/run-audit.d.mts +0 -1
  459. package/dist/commands/inspect/run-audit.mjs +0 -25
  460. package/dist/commands/inspect/run-audit.mjs.map +0 -1
  461. package/dist/commands/inspect/set-work-status.d.mts +0 -1
  462. package/dist/commands/inspect/set-work-status.mjs +0 -29
  463. package/dist/commands/inspect/set-work-status.mjs.map +0 -1
  464. package/dist/commands/inspect/stage-map.d.mts +0 -1
  465. package/dist/commands/inspect/stage-map.mjs +0 -131
  466. package/dist/commands/inspect/stage-map.mjs.map +0 -1
  467. package/dist/commands/inspect/task-list.d.mts +0 -1
  468. package/dist/commands/inspect/task-list.mjs +0 -149
  469. package/dist/commands/inspect/task-list.mjs.map +0 -1
  470. package/dist/commands/inspect/task-show.d.mts +0 -1
  471. package/dist/commands/inspect/task-show.mjs +0 -108
  472. package/dist/commands/inspect/task-show.mjs.map +0 -1
  473. package/dist/commands/inspect/time-report.d.mts +0 -1
  474. package/dist/commands/inspect/time-report.mjs +0 -24
  475. package/dist/commands/inspect/time-report.mjs.map +0 -1
  476. package/dist/commands/inspect/usage-report.d.mts +0 -1
  477. package/dist/commands/inspect/usage-report.mjs +0 -23
  478. package/dist/commands/inspect/usage-report.mjs.map +0 -1
  479. package/dist/commands/inspect/user-response.d.mts +0 -1
  480. package/dist/commands/inspect/user-response.mjs +0 -35
  481. package/dist/commands/inspect/user-response.mjs.map +0 -1
  482. package/dist/commands/inspect/worker-liveness.d.mts +0 -1
  483. package/dist/commands/inspect/worker-liveness.mjs +0 -45
  484. package/dist/commands/inspect/worker-liveness.mjs.map +0 -1
  485. package/dist/commands/lifecycle/contract-check.d.mts +0 -1
  486. package/dist/commands/lifecycle/contract-check.mjs +0 -18
  487. package/dist/commands/lifecycle/contract-check.mjs.map +0 -1
  488. package/dist/commands/lifecycle/migrate.d.mts +0 -1
  489. package/dist/commands/lifecycle/migrate.mjs +0 -30
  490. package/dist/commands/lifecycle/migrate.mjs.map +0 -1
  491. package/dist/commands/lifecycle/model.d.mts +0 -1
  492. package/dist/commands/lifecycle/model.mjs +0 -22
  493. package/dist/commands/lifecycle/model.mjs.map +0 -1
  494. package/dist/commands/manager.d.mts +0 -3
  495. package/dist/commands/manager.mjs +0 -50
  496. package/dist/commands/manager.mjs.map +0 -1
  497. package/dist/commands/report/agent-activity.d.mts +0 -1
  498. package/dist/commands/report/agent-activity.mjs +0 -20
  499. package/dist/commands/report/agent-activity.mjs.map +0 -1
  500. package/dist/commands/report/approval-decision.d.mts +0 -1
  501. package/dist/commands/report/approval-decision.mjs +0 -21
  502. package/dist/commands/report/approval-decision.mjs.map +0 -1
  503. package/dist/commands/report/design-snapshot.d.mts +0 -1
  504. package/dist/commands/report/design-snapshot.mjs +0 -19
  505. package/dist/commands/report/design-snapshot.mjs.map +0 -1
  506. package/dist/commands/report/finalize.d.mts +0 -4
  507. package/dist/commands/report/finalize.mjs +0 -64
  508. package/dist/commands/report/finalize.mjs.map +0 -1
  509. package/dist/commands/report/inject-report-index.d.mts +0 -1
  510. package/dist/commands/report/inject-report-index.mjs +0 -21
  511. package/dist/commands/report/inject-report-index.mjs.map +0 -1
  512. package/dist/commands/report/render-final-report.d.mts +0 -1
  513. package/dist/commands/report/render-final-report.mjs +0 -23
  514. package/dist/commands/report/render-final-report.mjs.map +0 -1
  515. package/dist/commands/report/render-views.d.mts +0 -1
  516. package/dist/commands/report/render-views.mjs +0 -27
  517. package/dist/commands/report/render-views.mjs.map +0 -1
  518. package/dist/commands/report/translate.d.mts +0 -1
  519. package/dist/commands/report/translate.mjs +0 -33
  520. package/dist/commands/report/translate.mjs.map +0 -1
  521. package/runtime/bin/lib/okstra/tmux-pane.sh +0 -40
  522. package/runtime/bin/lib/okstra-ctl/cmd-batch.sh +0 -59
  523. package/runtime/bin/lib/okstra-ctl/cmd-list.sh +0 -35
  524. package/runtime/bin/lib/okstra-ctl/cmd-open.sh +0 -36
  525. package/runtime/bin/lib/okstra-ctl/cmd-projects.sh +0 -26
  526. package/runtime/bin/lib/okstra-ctl/cmd-reconcile.sh +0 -29
  527. package/runtime/bin/lib/okstra-ctl/cmd-reindex.sh +0 -38
  528. package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +0 -345
  529. package/runtime/bin/lib/okstra-ctl/cmd-show.sh +0 -27
  530. package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +0 -92
  531. package/runtime/bin/lib/okstra-ctl/main.sh +0 -41
  532. package/runtime/bin/lib/okstra-ctl/prepare.sh +0 -31
  533. package/runtime/bin/lib/okstra-ctl/usage.sh +0 -23
  534. package/runtime/bin/okstra-central.sh +0 -152
  535. package/runtime/bin/okstra-incremental-carry.py +0 -10
  536. package/runtime/bin/okstra-incremental-scope.py +0 -10
  537. package/runtime/bin/okstra-team-reconcile.sh +0 -36
  538. package/runtime/python/okstra_ctl/agent_prompt_cli.py +0 -1156
  539. package/runtime/python/okstra_ctl/batch.py +0 -60
  540. package/runtime/python/okstra_ctl/clarification_items.py +0 -1050
  541. package/runtime/python/okstra_ctl/container_registry.py +0 -72
  542. package/runtime/python/okstra_ctl/improvement_assignment.py +0 -61
  543. package/runtime/python/okstra_ctl/resolver.py +0 -54
  544. package/runtime/python/okstra_ctl/team_reconcile.py +0 -275
  545. package/runtime/python/okstra_ctl/tmux.py +0 -134
  546. package/runtime/python/okstra_ctl/worktree.py +0 -1099
@@ -26,10 +26,10 @@ import uuid
26
26
  from dataclasses import dataclass
27
27
  from datetime import datetime, timezone
28
28
  from pathlib import Path
29
- from typing import Any, Callable, Mapping, Sequence
29
+ from typing import Any, Callable, Iterable, 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],
@@ -954,6 +985,25 @@ def _project_or_absolute(project_root: Path, value: str) -> Path:
954
985
  return path if path.is_absolute() else project_root / path
955
986
 
956
987
 
988
+ def prompt_anchor_values(
989
+ prompt_path: Path, labels: Iterable[str],
990
+ ) -> dict[str, str]:
991
+ """`**<label>:** <value>` 앵커 줄을 라벨별로 읽는다. 첫 번째 값이 이긴다.
992
+
993
+ 앵커는 materialize 가 문서 머리에 쓰고, 본문(리드 지시문)이 같은 라벨을
994
+ 예시로 되풀이할 수 있다. `_prompt_write_paths` 는 마지막 값을 취하는데
995
+ 그것은 write-policy 의 허용 집합을 넓힐 뿐이지만, 여기서 읽는 값은 워커가
996
+ 실제로 쓰는 경로의 판정 기준이므로 머리의 것이어야 한다.
997
+ """
998
+ values: dict[str, str] = {}
999
+ for line in prompt_path.read_text(encoding="utf-8").splitlines():
1000
+ for label in labels:
1001
+ prefix = f"**{label}:** "
1002
+ if label not in values and line.startswith(prefix):
1003
+ values[label] = line.removeprefix(prefix).strip()
1004
+ return values
1005
+
1006
+
957
1007
  def _execution_dispatch_round(dispatch_kind: str) -> int:
958
1008
  prefix = "reverify-r"
959
1009
  if not dispatch_kind.startswith(prefix):
@@ -1017,6 +1067,9 @@ def link_agent_dispatch_result(
1017
1067
  f"agent result link requires exactly one dispatch record: {dispatch_id}"
1018
1068
  )
1019
1069
  dispatch = dispatches[0]
1070
+ _require_recorded_result_path(
1071
+ project_root, dispatch, resolved_result, result_relative
1072
+ )
1020
1073
  link: dict[str, Any] = {
1021
1074
  "dispatchId": dispatch_id,
1022
1075
  "resultPath": result_relative,
@@ -1081,20 +1134,21 @@ def _finish_native_agent_attempt(
1081
1134
  if identity is None:
1082
1135
  return
1083
1136
  execution = read_execution_manifest(manifest_path)
1084
- invocation = next(
1137
+ attempt = next(
1085
1138
  (
1086
- row for row in execution.invocations
1139
+ row for row in execution.attempts
1087
1140
  if row.invocation_ref == identity["invocation_ref"]
1141
+ and row.attempt == identity["attempt"]
1088
1142
  ),
1089
1143
  None,
1090
1144
  )
1091
- if invocation is None:
1092
- raise DispatchError("agent result has no canonical invocation policy")
1093
- if dispatch.get("writePolicyDigest") != invocation.write_policy_digest:
1145
+ if attempt is None:
1146
+ raise DispatchError("agent result has no canonical attempt policy")
1147
+ if dispatch.get("writePolicyDigest") != attempt.write_policy_digest:
1094
1148
  raise DispatchError("agent dispatch write policy digest changed")
1095
- if dispatch.get("writeEnforcement") != invocation.write_enforcement:
1149
+ if dispatch.get("writeEnforcement") != attempt.write_enforcement:
1096
1150
  raise DispatchError("agent dispatch write enforcement changed")
1097
- policy = write_policy_from_payload(invocation.write_policy)
1151
+ policy = write_policy_from_payload(attempt.write_policy)
1098
1152
  status, summary = _native_mutation_result(
1099
1153
  project_root=project_root,
1100
1154
  dispatch=dispatch,
@@ -1290,6 +1344,127 @@ def reject_agent_dispatch_result(
1290
1344
  return dict(row)
1291
1345
 
1292
1346
 
1347
+ def _require_recorded_result_path(
1348
+ project_root: Path,
1349
+ dispatch: Mapping[str, Any],
1350
+ resolved_result: Path,
1351
+ result_relative: str,
1352
+ ) -> None:
1353
+ """링크되는 결과가 이 디스패치가 실제로 기다린 산출물인가.
1354
+
1355
+ 비교 대상을 여기서 다시 계산하지 않는다. `record-dispatch` 가 이미
1356
+ `completionPaths` 로 그 목록을 적어 두었고(리포트 작성자는 narrative 와
1357
+ 포인터 기록 둘), 그것이 이 디스패치가 완료로 인정하는 경로의 정본이다.
1358
+
1359
+ 이 대조가 없으면 잘못된 경로를 링크해도 그 자리에서는 통과하고, 조립이
1360
+ `required input is missing` 으로 죽을 때에야 드러난다 — 그때는 워커가 이미
1361
+ 돌고 난 뒤다. 실측(2026-08-26): 리드가 지어낸 seq(`-002.md`)를 넘겼고
1362
+ 매니페스트의 선언은 `-004.md` 였다. 여기서 막으면 워커를 다시 돌릴 필요 없이
1363
+ 같은 명령을 옳은 인자로 다시 부르면 된다.
1364
+
1365
+ `completionPaths` 를 기록하지 않은 행은 판정하지 않는다 — 목록이 없으면
1366
+ 무엇이 정당한지 알 수 없고, 모르면서 막는 것은 추측이다.
1367
+ """
1368
+ recorded = dispatch.get("completionPaths")
1369
+ if not isinstance(recorded, list) or not recorded:
1370
+ return
1371
+ allowed = {
1372
+ resolve_project_path(project_root, str(value)).resolve()
1373
+ for value in recorded
1374
+ if isinstance(value, str) and value.strip()
1375
+ }
1376
+ if not allowed or resolved_result in allowed:
1377
+ return
1378
+ expected = ", ".join(sorted(_relative_project_path(project_root, path) for path in allowed))
1379
+ raise DispatchError(
1380
+ f"agent result link is not one of this dispatch's completion paths: "
1381
+ f"linked {result_relative}, expected one of {expected}"
1382
+ )
1383
+
1384
+
1385
+ def abandon_agent_dispatch_attempt(
1386
+ *,
1387
+ project_root: Path,
1388
+ run_manifest_path: Path,
1389
+ invocation_ref: str,
1390
+ reason: str,
1391
+ ) -> dict[str, Any]:
1392
+ """Close a started attempt whose worker died without producing a result.
1393
+
1394
+ Every other path that closes an attempt needs something the dead worker
1395
+ never made: `link-result` needs its result file, and
1396
+ `_abandon_unstarted_attempt` only runs when the dispatch itself failed
1397
+ before the process existed. A worker that started and then died — its host
1398
+ killed, its pane closed, an import error before any log — leaves the attempt
1399
+ `started` forever, and `_validate_next_attempt` then refuses every retry with
1400
+ `previous attempt is not terminal`. The run has to be re-rendered from
1401
+ scratch to make progress.
1402
+
1403
+ **Only a `source-readonly` attempt may be abandoned.** The manifest accepts a
1404
+ following attempt only after `failed-no-mutation`, so closing an attempt is
1405
+ also an assertion that nothing was written. For a read-only worker that
1406
+ assertion is true by policy — it never held permission to write — so no
1407
+ guess is involved. For a `project-mutation` attempt it would be a guess, and
1408
+ a wrong one lets the retry run on top of a half-mutated tree, which is the
1409
+ invariant this manifest exists to protect. That case is refused here and
1410
+ belongs to the mutation audit.
1411
+ """
1412
+ if not reason.strip():
1413
+ raise DispatchError(
1414
+ "an abandonment reason is required — the ledger has to say why an "
1415
+ "attempt was closed without a result"
1416
+ )
1417
+ project_root = project_root.resolve()
1418
+ manifest_path = resolve_project_path(
1419
+ project_root, str(run_manifest_path)
1420
+ ).resolve(strict=True)
1421
+ manifest = load_json_object(manifest_path, "run manifest")
1422
+ execution = read_execution_manifest(manifest_path)
1423
+ attempts = [
1424
+ row for row in execution.attempts if row.invocation_ref == invocation_ref
1425
+ ]
1426
+ if not attempts:
1427
+ raise DispatchError(f"no attempt for invocation {invocation_ref}")
1428
+ row = max(attempts, key=lambda item: item.attempt)
1429
+ if row.finished_at is not None:
1430
+ raise DispatchError(
1431
+ f"attempt {invocation_ref} #{row.attempt} is already terminal "
1432
+ f"({row.status})"
1433
+ )
1434
+ mode = _attempt_source_mode(row)
1435
+ if mode != "source-readonly":
1436
+ raise DispatchError(
1437
+ f"attempt {invocation_ref} #{row.attempt} runs under sourcePolicy "
1438
+ f"`{mode}` — only a source-readonly attempt may be abandoned, "
1439
+ "because closing one asserts that nothing was written. Resolve a "
1440
+ "mutating attempt through its mutation audit."
1441
+ )
1442
+ finish_attempt_mutation(
1443
+ manifest_path,
1444
+ invocation_ref=invocation_ref,
1445
+ attempt=row.attempt,
1446
+ finished_at=utc_now(),
1447
+ status="failed-no-mutation",
1448
+ result_path=None,
1449
+ error_path=None,
1450
+ change_summary={"abandonedReason": reason.strip()},
1451
+ task_key=require_string(manifest, "taskKey"),
1452
+ )
1453
+ return {
1454
+ "invocationRef": invocation_ref,
1455
+ "attempt": row.attempt,
1456
+ "status": "failed-no-mutation",
1457
+ "reason": reason.strip(),
1458
+ }
1459
+
1460
+
1461
+ def _attempt_source_mode(row: Any) -> str:
1462
+ policy = getattr(row, "write_policy", None)
1463
+ source = policy.get("sourcePolicy") if isinstance(policy, Mapping) else None
1464
+ mode = source.get("mode") if isinstance(source, Mapping) else None
1465
+ return str(mode or "unknown")
1466
+
1467
+
1293
1468
  def _relative_project_path(project_root: Path, path: Path) -> str:
1294
1469
  try:
1295
1470
  return path.resolve(strict=False).relative_to(project_root).as_posix()
@@ -1307,6 +1482,63 @@ def worker_state(team_state: Mapping[str, Any], worker_id: str) -> Mapping[str,
1307
1482
  raise DispatchError(f"team-state has no workerId={worker_id}")
1308
1483
 
1309
1484
 
1485
+ def _task_project_root(team_state_path: Path) -> Path | None:
1486
+ """team-state 경로에서 프로젝트 루트를 되짚는다 (`.okstra` 를 기준점으로).
1487
+
1488
+ stage run 은 `runs/<phase>/stage-N/state/` 로 한 층이 더 깊으므로 부모를
1489
+ 세는 방식은 빗나간다. 기준점을 못 찾으면 None — 예상 밖 배치에서 판정을
1490
+ 포기하는 쪽이, 실행을 통째로 막는 쪽보다 낫다. 최종 검증이 같은 모순을
1491
+ 다시 잡는다.
1492
+ """
1493
+ for parent in team_state_path.resolve().parents:
1494
+ if parent.name == ".okstra":
1495
+ return parent.parent
1496
+ return None
1497
+
1498
+
1499
+ class CompletedWithoutResultError(DispatchError):
1500
+ """명부의 `resultPath` 파일 없이 `completed` 로 적으려 했다.
1501
+
1502
+ `_finish_attempt` 가 이 거절을 데이터로 돌려세우기 위해 구분한다 — 디스패치
1503
+ 자신의 산출물은 있는데 명부가 가리키는 파일이 다른 경우다.
1504
+ """
1505
+
1506
+
1507
+ def _reject_completed_without_result(
1508
+ team_state_path: Path, worker_id: str, worker: Mapping[str, Any]
1509
+ ) -> None:
1510
+ """산출물이 없는 역할을 완료로 적지 못하게 한다.
1511
+
1512
+ okstra 는 디스패치 결과를 스스로 기록한다. 그 원장이 두 시도 모두 `error`
1513
+ 라고 적은 역할이 명부에는 `completed` 로 남을 수 있었다 — 명부 상태가
1514
+ 원장에서 파생되지 않기 때문이다. 모순은 phase 끝 `validate-run` 이
1515
+ "completed but worker result file is missing" 으로 잡지만, 그때는 워커를
1516
+ 다시 돌릴 수 없어 실행 전체가 막힌다.
1517
+
1518
+ 조인 대신 명부 자신의 `resultPath` 를 본다. 그 값이 없는 역할(critic scope
1519
+ 등)은 판정 대상이 아니다.
1520
+
1521
+ 실측(2026-08-27, `fontsninja-v3-site` `dev-10341`): 명부 5건 중 `resultPath`
1522
+ 를 가진 4건에서 3건은 파일이 있었고, grok-planner 만 없었다.
1523
+ """
1524
+ relative = str(worker.get("resultPath") or "").strip()
1525
+ if not relative:
1526
+ return
1527
+ candidate = Path(relative)
1528
+ if not candidate.is_absolute():
1529
+ project_root = _task_project_root(team_state_path)
1530
+ if project_root is None:
1531
+ return
1532
+ candidate = project_root / candidate
1533
+ if candidate.is_file():
1534
+ return
1535
+ raise CompletedWithoutResultError(
1536
+ f"worker {worker_id} cannot be recorded completed: its result "
1537
+ f"{relative} does not exist. Record the terminal status the dispatch "
1538
+ "actually reached (`error` / `timeout` / `not-run`) with a reason."
1539
+ )
1540
+
1541
+
1310
1542
  def transition_worker_status(
1311
1543
  team_state_path: Path,
1312
1544
  worker_id: str,
@@ -1332,6 +1564,10 @@ def transition_worker_status(
1332
1564
  raise DispatchError(f"team-state workers must be an array: {team_state_path}")
1333
1565
  for worker in workers:
1334
1566
  if isinstance(worker, dict) and worker.get("workerId") == worker_id:
1567
+ if status == "completed":
1568
+ _reject_completed_without_result(
1569
+ team_state_path, worker_id, worker
1570
+ )
1335
1571
  worker["status"] = status
1336
1572
  worker["reason"] = reason
1337
1573
  if status == "in-progress":
@@ -1400,25 +1636,6 @@ def _has_recorded_team_create(team_state: Mapping[str, Any]) -> bool:
1400
1636
 
1401
1637
  # --- job facts ----------------------------------------------------------------
1402
1638
 
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
1639
  def dispatch_mode(jobs: Sequence[WorkerJob]) -> str:
1423
1640
  backends = {job.backend for job in jobs}
1424
1641
  if len(backends) == 1:
@@ -1427,7 +1644,18 @@ def dispatch_mode(jobs: Sequence[WorkerJob]) -> str:
1427
1644
 
1428
1645
 
1429
1646
  def missing_completion_paths(job: WorkerJob) -> tuple[Path, ...]:
1430
- return tuple(path for path in job.completion_paths if not path.is_file())
1647
+ missing: list[Path] = []
1648
+ for path in job.completion_paths:
1649
+ if path.is_file():
1650
+ continue
1651
+ # reports seq 와 workerResults seq 가 갈라지면 워커는 다른 쪽
1652
+ # 파일명으로 쓴다. 둘 중 하나가 있으면 산출물은 있는 것이다.
1653
+ if path in {job.result_path, job.worker_result_path} and any(
1654
+ alias.is_file() for alias in job.result_aliases
1655
+ ):
1656
+ continue
1657
+ missing.append(path)
1658
+ return tuple(missing)
1431
1659
 
1432
1660
 
1433
1661
  def dispatch_session_id(provider: str) -> str:
@@ -1475,7 +1703,7 @@ def worker_session_ids(
1475
1703
  for record in dispatches if isinstance(dispatches, list) else []:
1476
1704
  if not isinstance(record, Mapping):
1477
1705
  continue
1478
- if worker_id is not None and record.get("workerId") != worker_id:
1706
+ if worker_id is not None and _dispatch_worker_key(record) != worker_id:
1479
1707
  continue
1480
1708
  session_id = str(record.get("sessionId") or "").strip()
1481
1709
  if session_id and session_id not in session_ids:
@@ -1483,6 +1711,27 @@ def worker_session_ids(
1483
1711
  return session_ids
1484
1712
 
1485
1713
 
1714
+ def _dispatch_worker_key(record: Mapping[str, Any]) -> str:
1715
+ """The `workers[]` row a dispatch record belongs to.
1716
+
1717
+ A v1 record names it as `workerId`. A v2 record carries execution identity
1718
+ instead and no `workerId` (`_dispatch_record`), so its row is the one the
1719
+ roster keys off the assignment ref's last segment — `critic/scope` is the
1720
+ row `scope`, `initial/claude-analyser` the row `claude-analyser` — the same
1721
+ projection `v2_worker_state_key` makes for the jobs file. Matching on
1722
+ `workerId` alone found no v2 record, so a pane worker's session id was never
1723
+ read and every v2 pane worker stayed `unavailable` (observed 2026-09-02,
1724
+ dev-10626 r04: the critic's session was on disk with 79 usage records).
1725
+ """
1726
+ worker_id = str(record.get("workerId") or "").strip()
1727
+ if worker_id:
1728
+ return worker_id
1729
+ assignment_ref = record.get("assignmentRef")
1730
+ if not isinstance(assignment_ref, str):
1731
+ return ""
1732
+ return assignment_ref.rsplit("/", 1)[-1].strip()
1733
+
1734
+
1486
1735
  def dispatch_result_path(
1487
1736
  worker_id: str,
1488
1737
  worker_result_path: Path,
@@ -1534,6 +1783,32 @@ def dispatch_completion_paths(
1534
1783
  return (data_json, worker_result_path)
1535
1784
 
1536
1785
 
1786
+ def _plan_verify_result_aliases(
1787
+ worker_result_path: Path, manifest: Mapping[str, Any],
1788
+ ) -> tuple[Path, ...]:
1789
+ """reports seq 파일의 workerResults seq 짝.
1790
+
1791
+ plan-verify 결과 파일명만 해당한다. 다른 산출물의 seq 를 바꾸면 안 된다.
1792
+ """
1793
+ if "-plan-verify-r" not in worker_result_path.name:
1794
+ return ()
1795
+ sequences = manifest.get("runSequencesByCategory")
1796
+ if not isinstance(sequences, Mapping):
1797
+ return ()
1798
+ reports = str(sequences.get("reports") or "").strip()
1799
+ workers = str(sequences.get("workerResults") or "").strip()
1800
+ if not reports or not workers or reports == workers:
1801
+ return ()
1802
+ name = worker_result_path.name
1803
+ if name.endswith(f"-{reports}.md"):
1804
+ other = name[: -len(f"-{reports}.md")] + f"-{workers}.md"
1805
+ elif name.endswith(f"-{workers}.md"):
1806
+ other = name[: -len(f"-{workers}.md")] + f"-{reports}.md"
1807
+ else:
1808
+ return ()
1809
+ return (worker_result_path.with_name(other),)
1810
+
1811
+
1537
1812
  def validate_initial_prompts(
1538
1813
  manifest: Mapping[str, Any], jobs: Sequence[WorkerJob]
1539
1814
  ) -> None:
@@ -1893,10 +2168,82 @@ def worker_jobs_from_file(
1893
2168
  for item in workers
1894
2169
  if isinstance(item, Mapping)
1895
2170
  ]
2171
+ _validate_jobs_file_prompt_anchors(jobs)
1896
2172
  _validate_v2_jobs_file_authority(manifest, jobs)
1897
2173
  return jobs
1898
2174
 
1899
2175
 
2176
+ def _normalized_path(path: Path) -> Path:
2177
+ return Path(os.path.normpath(path))
2178
+
2179
+
2180
+ def _validate_jobs_file_prompt_anchors(jobs: Sequence[WorkerJob]) -> None:
2181
+ """jobs-file 의 결과 경로가 프롬프트 앵커와 같은 파일인지 본다.
2182
+
2183
+ 워커는 프롬프트의 `**Result Path:**` 에 쓰고(포인터를 가진 report-writer 는
2184
+ `**Worker Result Path:**` 에 포인터를), 수집기는 jobs-file 의
2185
+ `workerResultPath` 를 기다린다. 두 값은 리드가 따로 적으므로 어긋날 수 있고,
2186
+ 어긋나면 워커가 결과를 다 써도 `required worker artifact was not produced`
2187
+ 로 끝나 같은 프롬프트가 한 번 더 돈다. 실측(2026-09-02, fontsninja-v3-site
2188
+ dev-10626-1 error-analysis r04): 검증 디스패치 9건 중 8건이 이 형태였고
2189
+ 재시도 6건이 같은 이유로 실패했다.
2190
+
2191
+ report-writer 는 한 가지를 더 본다 — 프롬프트의 `**Result Path:**`(서술문)가
2192
+ `dispatch_result_path` 가 정한 조립 입력과 같은가. 다르면 서술문은 써지지만
2193
+ `report-finalize` 가 읽는 자리에는 아무것도 없다.
2194
+
2195
+ 프롬프트 파일이 아직 없는 잡은 건너뛴다. 실제 디스패치는 같은 파일을
2196
+ write-policy 검사(`_prompt_write_paths`)가 반드시 읽으므로 빠져나갈 자리가
2197
+ 없고, 단위 테스트의 빈 경로만 여기서 면제된다.
2198
+ """
2199
+ errors: list[str] = []
2200
+ for job in jobs:
2201
+ if not job.prompt_path.is_file():
2202
+ continue
2203
+ anchors = prompt_anchor_values(
2204
+ job.prompt_path, ("Result Path", "Worker Result Path"),
2205
+ )
2206
+ label = (
2207
+ "Worker Result Path"
2208
+ if anchors.get("Worker Result Path")
2209
+ else "Result Path"
2210
+ )
2211
+ declared = anchors.get(label)
2212
+ if not declared:
2213
+ continue
2214
+ declared_path = _normalized_path(
2215
+ _project_or_absolute(job.project_root, declared)
2216
+ )
2217
+ accepted = {
2218
+ _normalized_path(path)
2219
+ for path in (job.worker_result_path, *job.result_aliases)
2220
+ }
2221
+ if declared_path not in accepted:
2222
+ errors.append(
2223
+ f"{job.worker_id}: workerResultPath {job.worker_result_path} "
2224
+ f"differs from the prompt's **{label}:** {declared_path}"
2225
+ )
2226
+ narrative = anchors.get("Result Path")
2227
+ if (
2228
+ label == "Worker Result Path"
2229
+ and narrative
2230
+ and _normalized_path(job.result_path)
2231
+ != _normalized_path(job.worker_result_path)
2232
+ and _normalized_path(_project_or_absolute(job.project_root, narrative))
2233
+ != _normalized_path(job.result_path)
2234
+ ):
2235
+ errors.append(
2236
+ f"{job.worker_id}: the prompt's **Result Path:** {narrative} is "
2237
+ f"not the result this run assembles from ({job.result_path})"
2238
+ )
2239
+ if errors:
2240
+ raise DispatchError(
2241
+ "jobs file result paths differ from the prompt anchors — the worker "
2242
+ "writes where the prompt says, so the jobs file must name that path: "
2243
+ + "; ".join(errors)
2244
+ )
2245
+
2246
+
1900
2247
  def _worker_job_from_file(
1901
2248
  project_root: Path,
1902
2249
  item: Mapping[str, Any],
@@ -1909,6 +2256,14 @@ def _worker_job_from_file(
1909
2256
  resolve_wrapper: Callable[[str], Path],
1910
2257
  default_provider: Callable[[str], str],
1911
2258
  ) -> WorkerJob:
2259
+ # 빠진 필드를 한꺼번에 댄다. 하나씩 거절하던 동안 세 워커짜리 파일 하나를
2260
+ # 통과시키는 데 dry-run 세 번이 들었다(2026-09-02 실측).
2261
+ missing = _missing_required_strings(item)
2262
+ if missing:
2263
+ raise DispatchError(
2264
+ "jobs file worker is missing required string fields: "
2265
+ + ", ".join(missing)
2266
+ )
1912
2267
  identity = worker_execution_identity(item)
1913
2268
  worker_id = (
1914
2269
  require_string(item, "workerId")
@@ -1937,6 +2292,7 @@ def _worker_job_from_file(
1937
2292
  completion_paths = dispatch_completion_paths(
1938
2293
  worker_id, worker_result_path, manifest, project_root
1939
2294
  )
2295
+ result_aliases = _plan_verify_result_aliases(worker_result_path, manifest)
1940
2296
  digests = item.get("digests")
1941
2297
  digest_values = digests if isinstance(digests, Mapping) else {}
1942
2298
  host_model_value = item.get("hostModelValue")
@@ -1985,10 +2341,30 @@ def _worker_job_from_file(
1985
2341
  prompt_digest=string_value(digest_values.get("promptDigest")),
1986
2342
  host_model_value=host_model_value,
1987
2343
  enforcement_mode=string_value(item.get("enforcementMode")),
2344
+ result_aliases=result_aliases,
1988
2345
  **(identity or {}),
1989
2346
  )
1990
2347
 
1991
2348
 
2349
+ _V1_JOB_REQUIRED_STRINGS = (
2350
+ "workerId", "promptPath", "workerResultPath", "modelExecutionValue", "role",
2351
+ )
2352
+ _V2_JOB_REQUIRED_STRINGS = (
2353
+ "provider", "assignmentRef", "participantRef", "roleExecutionRef",
2354
+ "executionLabel", "dutyId", "invocationRef", "promptPath",
2355
+ "workerResultPath", "modelExecutionValue", "role",
2356
+ )
2357
+
2358
+
2359
+ def _missing_required_strings(item: Mapping[str, Any]) -> list[str]:
2360
+ keys = (
2361
+ _V2_JOB_REQUIRED_STRINGS
2362
+ if item.get("schemaVersion") == "2.0"
2363
+ else _V1_JOB_REQUIRED_STRINGS
2364
+ )
2365
+ return [key for key in keys if not string_value(item.get(key))]
2366
+
2367
+
1992
2368
  def worker_execution_identity(
1993
2369
  item: Mapping[str, Any],
1994
2370
  ) -> dict[str, Any] | None: