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
@@ -40,6 +40,7 @@ from okstra_ctl.final_report_paths import (
40
40
  final_report_data_path,
41
41
  final_report_markdown_path,
42
42
  is_report_record_path,
43
+ sidecar_source_rel,
43
44
  )
44
45
  from okstra_ctl.json_boundary import (
45
46
  JsonBoundaryError,
@@ -55,6 +56,25 @@ from okstra_ctl.ids import slugify_task_segment
55
56
  from okstra_ctl.listing import list_runs, absolute_final_report_path
56
57
  from okstra_ctl.paths import resolve_under_root
57
58
  from okstra_ctl.run_context import dir_flock
59
+ from .user_response_values import ( # noqa: F401 — 재노출
60
+ AnalysisReviewRecord,
61
+ UserResponseError,
62
+ _ANALYSIS_REVIEW_HEADING_RE,
63
+ _ANALYSIS_REVIEW_STATUSES,
64
+ _ANALYSIS_SIDECAR_HEADING_RE,
65
+ _FRONTMATTER_DELIMITER_RE,
66
+ _NEXT_RESPONSE_HEADING_RE,
67
+ _YAML_FRONTMATTER_RE,
68
+ _authoritative_analysis_review_match,
69
+ _field,
70
+ _nearest_frontmatter_value,
71
+ _nearest_sidecar_filename,
72
+ _parsed_created_at,
73
+ _quoted_review_value,
74
+ _sidecar_metadata_value,
75
+ parse_analysis_review,
76
+ parse_user_response_entries,
77
+ )
58
78
  from okstra_ctl.clarification_items import (
59
79
  ClarificationItem,
60
80
  UNRESOLVED_STATUSES,
@@ -64,28 +84,12 @@ from okstra_ctl.clarification_items import (
64
84
  )
65
85
 
66
86
  _PLAN_DECISION_HEADING_RE = re.compile(r"^## PLAN DECISION\s*$", re.MULTILINE)
67
- _NEXT_RESPONSE_HEADING_RE = re.compile(r"^## ", re.MULTILINE)
68
- _ANALYSIS_REVIEW_HEADING_RE = re.compile(r"^## ANALYSIS REVIEW\s*$", re.MULTILINE)
69
87
  _DIRECTION_SELECTION_HEADING_RE = re.compile(
70
88
  r"^## DIRECTION SELECTION\s*$", re.MULTILINE
71
89
  )
72
90
  _REPORT_AUTHORING_HEADING_RE = re.compile(
73
91
  r"^## REPORT AUTHORING\s*$", re.MULTILINE
74
92
  )
75
- _ANALYSIS_SIDECAR_HEADING_RE = re.compile(
76
- r"^## (?P<filename>user-response-[^\n]+\.md)\s*$", re.MULTILINE
77
- )
78
- _YAML_FRONTMATTER_RE = re.compile(
79
- r"\A---[ \t]*\r?\n(?P<body>.*?)(?:\r?\n)---[ \t]*(?:\r?\n|\Z)",
80
- re.DOTALL,
81
- )
82
- _FRONTMATTER_DELIMITER_RE = re.compile(r"^---[ \t]*$", re.MULTILINE)
83
-
84
-
85
- class UserResponseError(ValueError):
86
- """Raised when a user-response sidecar violates its supported format."""
87
-
88
-
89
93
  @dataclass(frozen=True)
90
94
  class PlanDecisionRecord:
91
95
  """sidecar 의 ``## PLAN DECISION`` 블록 + 매칭에 필요한 frontmatter 필드."""
@@ -100,21 +104,6 @@ class PlanDecisionRecord:
100
104
  return self.status == PLAN_DECISION_APPROVED
101
105
 
102
106
 
103
- @dataclass(frozen=True)
104
- class AnalysisReviewRecord:
105
- """Validated user review of an analysis report."""
106
-
107
- status: str
108
- affected_ids: tuple[str, ...]
109
- reason: str
110
- additional_evidence: str
111
- requested_scope_change: str
112
- task_key: str
113
- task_type: str
114
- source_report: str
115
- seq: str
116
-
117
-
118
107
  @dataclass(frozen=True)
119
108
  class DirectionSelectionRecord:
120
109
  status: str
@@ -129,152 +118,12 @@ class DirectionSelectionRecord:
129
118
  seq: str
130
119
 
131
120
 
132
- _ANALYSIS_REVIEW_STATUSES = frozenset({
133
- "accepted",
134
- "revision-requested",
135
- "rejected",
136
- })
137
121
  _ANALYSIS_REPORT_RE = re.compile(
138
122
  r"^final-report-(?P<task_type>project-analysis|feature-analysis|"
139
123
  r"change-impact-analysis)-(?P<seq>\d{3})\.(?:md|data\.json)$"
140
124
  )
141
125
 
142
126
 
143
- def _quoted_review_value(block: str, key: str) -> str:
144
- match = re.search(
145
- rf"^- {re.escape(key)}:\s*\n((?:\s*>.*\n?)+)", block, re.MULTILINE
146
- )
147
- if not match:
148
- return ""
149
- return "\n".join(
150
- re.sub(r"^\s*>\s?", "", line)
151
- for line in match.group(1).splitlines()
152
- ).strip()
153
-
154
-
155
- def _sidecar_metadata_value(sidecar_text: str, key: str) -> str:
156
- frontmatter = _YAML_FRONTMATTER_RE.match(sidecar_text)
157
- if frontmatter is None:
158
- return ""
159
- match = re.search(
160
- rf"^{re.escape(key)}:[ \t]*(\S.*?)[ \t]*$",
161
- frontmatter.group("body"),
162
- re.MULTILINE,
163
- )
164
- return match.group(1) if match else ""
165
-
166
-
167
- def _nearest_frontmatter_value(
168
- sidecar_text: str, key: str, before_offset: int
169
- ) -> str:
170
- delimiters = [
171
- match
172
- for match in _FRONTMATTER_DELIMITER_RE.finditer(sidecar_text)
173
- if match.start() < before_offset
174
- ]
175
- for opening, closing in reversed(list(zip(delimiters, delimiters[1:]))):
176
- body = sidecar_text[opening.end():closing.start()]
177
- match = re.search(
178
- rf"^{re.escape(key)}:[ \t]*(\S.*?)[ \t]*$",
179
- body,
180
- re.MULTILINE,
181
- )
182
- if match:
183
- return match.group(1)
184
- return _sidecar_metadata_value(sidecar_text, key)
185
-
186
-
187
- def _parsed_created_at(value: str) -> dt.datetime | None:
188
- if re.fullmatch(
189
- r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z",
190
- value,
191
- ) is None:
192
- return None
193
- try:
194
- parsed = dt.datetime.fromisoformat(value[:-1] + "+00:00")
195
- except ValueError:
196
- return None
197
- if parsed.tzinfo is None:
198
- return None
199
- return parsed.astimezone(dt.timezone.utc)
200
-
201
-
202
- def _nearest_sidecar_filename(sidecar_text: str, before_offset: int) -> str:
203
- headings = [
204
- match
205
- for match in _ANALYSIS_SIDECAR_HEADING_RE.finditer(sidecar_text)
206
- if match.start() < before_offset
207
- ]
208
- return headings[-1].group("filename") if headings else ""
209
-
210
-
211
- def _authoritative_analysis_review_match(
212
- sidecar_text: str, matches: list[re.Match[str]]
213
- ) -> re.Match[str]:
214
- candidates: list[tuple[dt.datetime, str, int, re.Match[str]]] = []
215
- for match in matches:
216
- created_at = _parsed_created_at(
217
- _nearest_frontmatter_value(sidecar_text, "created-at", match.start())
218
- )
219
- if created_at is None:
220
- continue
221
- candidates.append((
222
- created_at,
223
- _nearest_sidecar_filename(sidecar_text, match.start()),
224
- match.start(),
225
- match,
226
- ))
227
- if not candidates:
228
- raise UserResponseError(
229
- "ANALYSIS REVIEW requires a valid canonical created-at"
230
- )
231
- return max(candidates, key=lambda candidate: candidate[:3])[3]
232
-
233
-
234
- def parse_analysis_review(sidecar_text: str) -> AnalysisReviewRecord | None:
235
- """Parse the bounded ``## ANALYSIS REVIEW`` block from a sidecar.
236
-
237
- A sidecar without this optional block is not a review. Once the block is
238
- present, status and the fields that make a rejection actionable are
239
- validated fail-closed.
240
- """
241
- matches = list(_ANALYSIS_REVIEW_HEADING_RE.finditer(sidecar_text))
242
- if not matches:
243
- return None
244
- match = _authoritative_analysis_review_match(sidecar_text, matches)
245
- block = sidecar_text[match.end():]
246
- next_heading = _NEXT_RESPONSE_HEADING_RE.search(block)
247
- if next_heading:
248
- block = block[:next_heading.start()]
249
- status = _field(block, "Status")
250
- if status not in _ANALYSIS_REVIEW_STATUSES:
251
- raise UserResponseError("ANALYSIS REVIEW Status is invalid")
252
- affected = _field(block, "Affected-IDs") or ""
253
- affected_ids = tuple(item.strip() for item in affected.split(",") if item.strip())
254
- reason = _quoted_review_value(block, "Reason")
255
- if status in {"revision-requested", "rejected"} and (not affected_ids or not reason):
256
- raise UserResponseError(
257
- f"ANALYSIS REVIEW {status} requires Affected-IDs and Reason"
258
- )
259
- return AnalysisReviewRecord(
260
- status=status,
261
- affected_ids=affected_ids,
262
- reason=reason,
263
- additional_evidence=_quoted_review_value(block, "Additional-Evidence"),
264
- requested_scope_change=_quoted_review_value(block, "Requested-Scope-Change"),
265
- task_key=_nearest_frontmatter_value(
266
- sidecar_text, "task-key", match.start()
267
- ),
268
- task_type=_nearest_frontmatter_value(
269
- sidecar_text, "task-type", match.start()
270
- ),
271
- source_report=_nearest_frontmatter_value(
272
- sidecar_text, "source-report", match.start()
273
- ),
274
- seq=_nearest_frontmatter_value(sidecar_text, "seq", match.start()),
275
- )
276
-
277
-
278
127
  def _matching_analysis_review_sidecars(
279
128
  report_path: Path,
280
129
  sidecar_name: re.Pattern[str],
@@ -474,16 +323,6 @@ def parse_report_authoring(sidecar_text: str) -> UserReportAuthoring | None:
474
323
  return UserReportAuthoring(status=status, reason=reason)
475
324
 
476
325
 
477
- _RESPONSE_HEADING_RE = re.compile(r"^## (?P<id>[A-Za-z][A-Za-z0-9]*-\d+)\s*$", re.MULTILINE)
478
-
479
-
480
- def _field(block: str, key: str) -> Optional[str]:
481
- m = re.search(
482
- rf"^- {re.escape(key)}:[ \t]*(\S.*?)[ \t]*$", block, re.MULTILINE
483
- )
484
- return m.group(1) if m else None
485
-
486
-
487
326
  def _direction_identity_fields(block: str) -> tuple[str, str]:
488
327
  match = re.search(
489
328
  r"^- Option-ID:[ \t]*(?P<option_id>[^\r\n]*)\r?\n"
@@ -497,15 +336,6 @@ def _direction_identity_fields(block: str) -> tuple[str, str]:
497
336
  return match.group("option_id"), match.group("option_name")
498
337
 
499
338
 
500
- def _value(block: str) -> str:
501
- # Value 는 "- Value:" 다음 줄들의 " > " 인용 블록.
502
- m = re.search(r"^- Value:\s*\n((?:\s*>.*\n?)+)", block, re.MULTILINE)
503
- if not m:
504
- return ""
505
- lines = [re.sub(r"^\s*>\s?", "", ln) for ln in m.group(1).splitlines()]
506
- return "\n".join(lines).strip()
507
-
508
-
509
339
  def parse_direction_selection(
510
340
  sidecar_text: str,
511
341
  ) -> DirectionSelectionRecord | None:
@@ -550,26 +380,6 @@ def parse_direction_selection(
550
380
  )
551
381
 
552
382
 
553
- def parse_user_response_entries(sidecar_text: str) -> list[UserResponseEntry]:
554
- """Reverse of ``serialize_user_response`` for the per-response ``## C-*``
555
- blocks. The ``## PLAN DECISION`` block is skipped (read separately by
556
- ``parse_plan_decision``)."""
557
- entries: list[UserResponseEntry] = []
558
- matches = list(_RESPONSE_HEADING_RE.finditer(sidecar_text))
559
- for i, m in enumerate(matches):
560
- end = matches[i + 1].start() if i + 1 < len(matches) else len(sidecar_text)
561
- block = sidecar_text[m.end():end]
562
- rationale = _field(block, "Rationale")
563
- entries.append(UserResponseEntry(
564
- response_id=m.group("id"),
565
- kind=_field(block, "Kind") or "",
566
- value=_value(block),
567
- rationale=rationale,
568
- disposition=_field(block, "Disposition") or "answer",
569
- ))
570
- return entries
571
-
572
-
573
383
  _SEQ_FROM_REPORT_RE = re.compile(
574
384
  r"final-report-.+-(\w+)\.(?:md|data\.json)$"
575
385
  )
@@ -633,9 +443,6 @@ def _load_report_record(
633
443
  return record
634
444
 
635
445
 
636
- def _relative_from_runs(path: Path) -> str:
637
- indices = [index for index, part in enumerate(path.parts) if part == "runs"]
638
- return Path(*path.parts[indices[-1]:]).as_posix() if indices else path.name
639
446
 
640
447
 
641
448
  def _record_identity(record: Mapping[str, Any]) -> tuple[str, str]:
@@ -678,7 +485,7 @@ def resolve_report_context(
678
485
  raise UserResponseError("report filename has no run sequence")
679
486
  run_root = resolved.parent.parent
680
487
  data_path = final_report_data_path(resolved) if record else None
681
- source_data = _relative_from_runs(data_path) if data_path else ""
488
+ source_data = sidecar_source_rel(data_path) if data_path else ""
682
489
  task_type_root = run_root.parent if run_root.name.startswith("stage-") else run_root
683
490
  task_root = task_type_root.parent.parent
684
491
  project_root = task_root.parents[3] if len(task_root.parents) >= 4 else task_root
@@ -689,7 +496,7 @@ def resolve_report_context(
689
496
  task_key=task_key,
690
497
  task_type=task_type,
691
498
  seq=seq,
692
- source_report=_relative_from_runs(markdown),
499
+ source_report=sidecar_source_rel(markdown),
693
500
  source_data=source_data,
694
501
  source_data_sha256=(
695
502
  hashlib.sha256(data_path.read_bytes()).hexdigest() if data_path else ""
@@ -764,9 +571,14 @@ def _validate_owned_report_context(
764
571
  raise UserResponseError("report resolves outside the selected project")
765
572
  _reject_symlinks_below(project_root, data_path, "report")
766
573
  markdown = _lexical_absolute(final_report_markdown_path(data_path))
574
+ # 읽기 사본(`.md`)은 on-demand 로만 렌더된다. 타입드 트랜잭션은 바로 아래에서
575
+ # 버전 있는 레코드를 요구하고, 그 경로는 `.md` 본문을 한 번도 읽지 않는다
576
+ # (schema-v1 만 `markdown_path` 를 읽으며, 그때 그 경로는 리포트 자신이다).
577
+ # 존재를 요구하면 `.md` 를 렌더한 적 없는 리포트의 응답 채널이 통째로 닫힌다.
578
+ # 경로는 sidecar 의 `source-report` 신원 문자열로만 쓰이므로 계산은 유지한다.
767
579
  _reject_symlinks_below(project_root, markdown, "report reading copy")
768
- if not data_path.is_file() or not markdown.is_file():
769
- raise UserResponseError("report record and reading copy must be regular files")
580
+ if not data_path.is_file():
581
+ raise UserResponseError("report record must be a regular file")
770
582
  record = _load_report_record(data_path, validate_schema=True)
771
583
  if record is None:
772
584
  raise UserResponseError("typed transactions require a versioned report record")
@@ -2283,8 +2095,33 @@ def _add_list_arguments(parser: argparse.ArgumentParser) -> None:
2283
2095
  parser.add_argument("--limit", type=int, default=3)
2284
2096
 
2285
2097
 
2098
+ _CLI_EPILOG = r"""Usage:
2099
+ okstra user-response list --home <dir> --project <id> [--limit <n>]
2100
+ okstra user-response list-view --home <dir> --project <id> [--limit <n>]
2101
+ okstra user-response show --report <md>
2102
+ okstra user-response show-view --report <md|data.json> --project-root <dir>
2103
+ okstra user-response begin --report <md|data.json> --task-key <key>
2104
+ okstra user-response answer --transaction <id> --clarification-id <C-NNN> \
2105
+ --kind <kind> --option-number <N>
2106
+ okstra user-response answer --transaction <id> --clarification-id <C-NNN> \
2107
+ --kind <kind> --disposition <value> --value-file <md> [--rationale-file <md>]
2108
+ okstra user-response plan-decision --transaction <id> --status <value> \
2109
+ [--implementation-option <name>] [--reason-file <md>]
2110
+ okstra user-response legacy-report-authoring --transaction <id> \
2111
+ --status <approved|denied> --reason-file <md>
2112
+ okstra user-response finalize --transaction <id>
2113
+
2114
+ Exit codes: 0 ok / 1 오류
2115
+ """
2116
+ _CLI_DESCRIPTION = "\uc5f4\ub9b0 clarification\uc744 \uace0\uc815 \ud14d\uc2a4\ud2b8\ub85c \uc77d\uace0 sidecar \ud2b8\ub79c\uc7ad\uc158 \uae30\ub85d."
2117
+
2118
+
2286
2119
  def _build_parser() -> argparse.ArgumentParser:
2287
- parser = argparse.ArgumentParser(prog="okstra user-response")
2120
+ parser = argparse.ArgumentParser(
2121
+ description=_CLI_DESCRIPTION,
2122
+ epilog=_CLI_EPILOG,
2123
+ formatter_class=argparse.RawDescriptionHelpFormatter,
2124
+ prog="okstra user-response")
2288
2125
  sub = parser.add_subparsers(dest="cmd", required=True)
2289
2126
  _add_list_arguments(sub.add_parser("list"))
2290
2127
  _add_list_arguments(sub.add_parser("list-view"))
@@ -0,0 +1,242 @@
1
+ """사용자 응답 사이드카의 값 타입과 파싱.
2
+
3
+ `user_response`(2,383줄) 와 `report_views`(1,000줄+) 에 흩어져 있던
4
+ 자족적인 값·파싱 단위다. 어떤 사이드카를 분석
5
+ 리포트에 붙일지 고르는 `clarification_items._sidecars_for_attachment` 가
6
+ `parse_analysis_review` 하나 때문에 `user_response` 전체를 함수 본문에서 지연
7
+ import 해야 했다 — `user_response` 는 반대로 `clarification_items` 를 최상위에서
8
+ 끌어오기 때문이다.
9
+
10
+ 이 모듈은 표준 라이브러리 외에 아무것도 import 하지 않는다. `user_response` 가
11
+ 모든 이름을 재노출하므로 기존 호출부는 바뀌지 않는다.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import datetime as dt
16
+ import re
17
+ from dataclasses import dataclass
18
+ from typing import Optional
19
+
20
+
21
+ _NEXT_RESPONSE_HEADING_RE = re.compile(r"^## ", re.MULTILINE)
22
+
23
+ _ANALYSIS_REVIEW_HEADING_RE = re.compile(r"^## ANALYSIS REVIEW\s*$", re.MULTILINE)
24
+
25
+ _ANALYSIS_SIDECAR_HEADING_RE = re.compile(
26
+ r"^## (?P<filename>user-response-[^\n]+\.md)\s*$", re.MULTILINE
27
+ )
28
+
29
+ _YAML_FRONTMATTER_RE = re.compile(
30
+ r"\A---[ \t]*\r?\n(?P<body>.*?)(?:\r?\n)---[ \t]*(?:\r?\n|\Z)",
31
+ re.DOTALL,
32
+ )
33
+
34
+ _FRONTMATTER_DELIMITER_RE = re.compile(r"^---[ \t]*$", re.MULTILINE)
35
+
36
+ class UserResponseError(ValueError):
37
+ """Raised when a user-response sidecar violates its supported format."""
38
+
39
+ @dataclass(frozen=True)
40
+ class AnalysisReviewRecord:
41
+ """Validated user review of an analysis report."""
42
+
43
+ status: str
44
+ affected_ids: tuple[str, ...]
45
+ reason: str
46
+ additional_evidence: str
47
+ requested_scope_change: str
48
+ task_key: str
49
+ task_type: str
50
+ source_report: str
51
+ seq: str
52
+
53
+ _ANALYSIS_REVIEW_STATUSES = frozenset({
54
+ "accepted",
55
+ "revision-requested",
56
+ "rejected",
57
+ })
58
+
59
+ def _quoted_review_value(block: str, key: str) -> str:
60
+ match = re.search(
61
+ rf"^- {re.escape(key)}:\s*\n((?:\s*>.*\n?)+)", block, re.MULTILINE
62
+ )
63
+ if not match:
64
+ return ""
65
+ return "\n".join(
66
+ re.sub(r"^\s*>\s?", "", line)
67
+ for line in match.group(1).splitlines()
68
+ ).strip()
69
+
70
+ def _sidecar_metadata_value(sidecar_text: str, key: str) -> str:
71
+ frontmatter = _YAML_FRONTMATTER_RE.match(sidecar_text)
72
+ if frontmatter is None:
73
+ return ""
74
+ match = re.search(
75
+ rf"^{re.escape(key)}:[ \t]*(\S.*?)[ \t]*$",
76
+ frontmatter.group("body"),
77
+ re.MULTILINE,
78
+ )
79
+ return match.group(1) if match else ""
80
+
81
+ def _nearest_frontmatter_value(
82
+ sidecar_text: str, key: str, before_offset: int
83
+ ) -> str:
84
+ delimiters = [
85
+ match
86
+ for match in _FRONTMATTER_DELIMITER_RE.finditer(sidecar_text)
87
+ if match.start() < before_offset
88
+ ]
89
+ for opening, closing in reversed(list(zip(delimiters, delimiters[1:]))):
90
+ body = sidecar_text[opening.end():closing.start()]
91
+ match = re.search(
92
+ rf"^{re.escape(key)}:[ \t]*(\S.*?)[ \t]*$",
93
+ body,
94
+ re.MULTILINE,
95
+ )
96
+ if match:
97
+ return match.group(1)
98
+ return _sidecar_metadata_value(sidecar_text, key)
99
+
100
+ def _parsed_created_at(value: str) -> dt.datetime | None:
101
+ if re.fullmatch(
102
+ r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z",
103
+ value,
104
+ ) is None:
105
+ return None
106
+ try:
107
+ parsed = dt.datetime.fromisoformat(value[:-1] + "+00:00")
108
+ except ValueError:
109
+ return None
110
+ if parsed.tzinfo is None:
111
+ return None
112
+ return parsed.astimezone(dt.timezone.utc)
113
+
114
+ def _nearest_sidecar_filename(sidecar_text: str, before_offset: int) -> str:
115
+ headings = [
116
+ match
117
+ for match in _ANALYSIS_SIDECAR_HEADING_RE.finditer(sidecar_text)
118
+ if match.start() < before_offset
119
+ ]
120
+ return headings[-1].group("filename") if headings else ""
121
+
122
+ def _authoritative_analysis_review_match(
123
+ sidecar_text: str, matches: list[re.Match[str]]
124
+ ) -> re.Match[str]:
125
+ candidates: list[tuple[dt.datetime, str, int, re.Match[str]]] = []
126
+ for match in matches:
127
+ created_at = _parsed_created_at(
128
+ _nearest_frontmatter_value(sidecar_text, "created-at", match.start())
129
+ )
130
+ if created_at is None:
131
+ continue
132
+ candidates.append((
133
+ created_at,
134
+ _nearest_sidecar_filename(sidecar_text, match.start()),
135
+ match.start(),
136
+ match,
137
+ ))
138
+ if not candidates:
139
+ raise UserResponseError(
140
+ "ANALYSIS REVIEW requires a valid canonical created-at"
141
+ )
142
+ return max(candidates, key=lambda candidate: candidate[:3])[3]
143
+
144
+ def parse_analysis_review(sidecar_text: str) -> AnalysisReviewRecord | None:
145
+ """Parse the bounded ``## ANALYSIS REVIEW`` block from a sidecar.
146
+
147
+ A sidecar without this optional block is not a review. Once the block is
148
+ present, status and the fields that make a rejection actionable are
149
+ validated fail-closed.
150
+ """
151
+ matches = list(_ANALYSIS_REVIEW_HEADING_RE.finditer(sidecar_text))
152
+ if not matches:
153
+ return None
154
+ match = _authoritative_analysis_review_match(sidecar_text, matches)
155
+ block = sidecar_text[match.end():]
156
+ next_heading = _NEXT_RESPONSE_HEADING_RE.search(block)
157
+ if next_heading:
158
+ block = block[:next_heading.start()]
159
+ status = _field(block, "Status")
160
+ if status not in _ANALYSIS_REVIEW_STATUSES:
161
+ raise UserResponseError("ANALYSIS REVIEW Status is invalid")
162
+ affected = _field(block, "Affected-IDs") or ""
163
+ affected_ids = tuple(item.strip() for item in affected.split(",") if item.strip())
164
+ reason = _quoted_review_value(block, "Reason")
165
+ if status in {"revision-requested", "rejected"} and (not affected_ids or not reason):
166
+ raise UserResponseError(
167
+ f"ANALYSIS REVIEW {status} requires Affected-IDs and Reason"
168
+ )
169
+ return AnalysisReviewRecord(
170
+ status=status,
171
+ affected_ids=affected_ids,
172
+ reason=reason,
173
+ additional_evidence=_quoted_review_value(block, "Additional-Evidence"),
174
+ requested_scope_change=_quoted_review_value(block, "Requested-Scope-Change"),
175
+ task_key=_nearest_frontmatter_value(
176
+ sidecar_text, "task-key", match.start()
177
+ ),
178
+ task_type=_nearest_frontmatter_value(
179
+ sidecar_text, "task-type", match.start()
180
+ ),
181
+ source_report=_nearest_frontmatter_value(
182
+ sidecar_text, "source-report", match.start()
183
+ ),
184
+ seq=_nearest_frontmatter_value(sidecar_text, "seq", match.start()),
185
+ )
186
+
187
+ def _field(block: str, key: str) -> Optional[str]:
188
+ m = re.search(
189
+ rf"^- {re.escape(key)}:[ \t]*(\S.*?)[ \t]*$", block, re.MULTILINE
190
+ )
191
+ return m.group(1) if m else None
192
+
193
+
194
+ @dataclass(frozen=True)
195
+ class UserResponseEntry:
196
+ response_id: str
197
+ kind: str
198
+ value: str
199
+ rationale: Optional[str] = None
200
+ disposition: str = "answer"
201
+
202
+ @dataclass(frozen=True)
203
+ class UserPlanDecision:
204
+ """HTML 계획 결정 위젯의 Export 결과.
205
+
206
+ ``status`` 가 승인이 아니면 ``reason`` 이 필수다 — 사유 없는 반려는 다음
207
+ run 이 무엇을 고쳐야 하는지 알 수 없어 되돌아올 수밖에 없다.
208
+ ``implementation_option`` 이 빈 문자열이면 라인을 생략한다 (소비 측은
209
+ Recommended Option 폴백)."""
210
+ status: str
211
+ implementation_option: str = ""
212
+ reason: str = ""
213
+
214
+
215
+ _RESPONSE_HEADING_RE = re.compile(r"^## (?P<id>[A-Za-z][A-Za-z0-9]*-\d+)\s*$", re.MULTILINE)
216
+
217
+ def _value(block: str) -> str:
218
+ # Value 는 "- Value:" 다음 줄들의 " > " 인용 블록.
219
+ m = re.search(r"^- Value:\s*\n((?:\s*>.*\n?)+)", block, re.MULTILINE)
220
+ if not m:
221
+ return ""
222
+ lines = [re.sub(r"^\s*>\s?", "", ln) for ln in m.group(1).splitlines()]
223
+ return "\n".join(lines).strip()
224
+
225
+ def parse_user_response_entries(sidecar_text: str) -> list[UserResponseEntry]:
226
+ """Reverse of ``serialize_user_response`` for the per-response ``## C-*``
227
+ blocks. The ``## PLAN DECISION`` block is skipped (read separately by
228
+ ``parse_plan_decision``)."""
229
+ entries: list[UserResponseEntry] = []
230
+ matches = list(_RESPONSE_HEADING_RE.finditer(sidecar_text))
231
+ for i, m in enumerate(matches):
232
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(sidecar_text)
233
+ block = sidecar_text[m.end():end]
234
+ rationale = _field(block, "Rationale")
235
+ entries.append(UserResponseEntry(
236
+ response_id=m.group("id"),
237
+ kind=_field(block, "Kind") or "",
238
+ value=_value(block),
239
+ rationale=rationale,
240
+ disposition=_field(block, "Disposition") or "answer",
241
+ ))
242
+ return entries
@@ -0,0 +1,3 @@
1
+ """실행 준비 시 고정하는 검증 계약 버전."""
2
+
3
+ CURRENT_VALIDATION_CONTRACT_VERSION = 1
@@ -106,10 +106,33 @@ def parse_finding_votes(text: str, *, adversarial: bool) -> dict[str, FindingVot
106
106
  an unfamiliar token silently read as a different verdict.
107
107
  """
108
108
  vocabulary = ADVERSARIAL_VERDICTS if adversarial else COLLABORATIVE_VERDICTS
109
- return {
110
- finding_id: _finding_vote(finding_id, fields, vocabulary)
111
- for finding_id, fields in _scan_blocks(text).items()
112
- }
109
+ return _collect_blocks(
110
+ _scan_blocks(text),
111
+ lambda item_id, fields: _finding_vote(item_id, fields, vocabulary),
112
+ )
113
+
114
+
115
+ def _collect_blocks(blocks: dict[str, dict[str, str]], parse) -> dict:
116
+ """모든 블록을 읽고 결함을 한 번에 보고한다.
117
+
118
+ 첫 결함에서 멈추면 15건 중 13건이 같은 결함인 응답도 한 건씩만 드러나,
119
+ 교정 디스패치를 그 수만큼 반복하게 된다(2026-09-02 실측). 한 응답의 결함은
120
+ 서로 독립이므로 전부 모아 하나의 오류로 낸다.
121
+ """
122
+ parsed: dict = {}
123
+ errors: list[str] = []
124
+ for item_id, fields in blocks.items():
125
+ try:
126
+ parsed[item_id] = parse(item_id, fields)
127
+ except VerdictBlockError as exc:
128
+ errors.append(str(exc))
129
+ if errors:
130
+ if len(errors) == 1:
131
+ raise VerdictBlockError(errors[0])
132
+ raise VerdictBlockError(
133
+ f"{len(errors)} of {len(blocks)} blocks are malformed: " + "; ".join(errors)
134
+ )
135
+ return parsed
113
136
 
114
137
 
115
138
  def _finding_vote(
@@ -155,10 +178,7 @@ class VerdictBlock:
155
178
 
156
179
  def parse_verdict_blocks(text: str) -> dict[str, VerdictBlock]:
157
180
  """Every `### <item-id>` plan-body verdict block in *text*, keyed by item id."""
158
- return {
159
- item_id: _block(item_id, fields)
160
- for item_id, fields in _scan_blocks(text).items()
161
- }
181
+ return _collect_blocks(_scan_blocks(text), _block)
162
182
 
163
183
 
164
184
  def _verdict_token(item_id: str, raw: str) -> tuple[str, str]: