okstra 0.186.7 → 0.187.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (541) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.d.mts +88 -1
  3. package/dist/cli-registry.mjs +68 -111
  4. package/dist/cli-registry.mjs.map +1 -1
  5. package/dist/commands/execute/render-bundle.mjs +0 -1
  6. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  7. package/dist/commands/execute/run.mjs +8 -3
  8. package/dist/commands/execute/run.mjs.map +1 -1
  9. package/dist/commands/lifecycle/check-project.mjs +1 -14
  10. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  11. package/dist/commands/lifecycle/config.mjs +38 -40
  12. package/dist/commands/lifecycle/config.mjs.map +1 -1
  13. package/dist/commands/lifecycle/doctor.d.mts +22 -7
  14. package/dist/commands/lifecycle/doctor.mjs +77 -49
  15. package/dist/commands/lifecycle/doctor.mjs.map +1 -1
  16. package/dist/commands/lifecycle/install.d.mts +12 -10
  17. package/dist/commands/lifecycle/install.mjs +104 -39
  18. package/dist/commands/lifecycle/install.mjs.map +1 -1
  19. package/dist/commands/lifecycle/paths.mjs +8 -3
  20. package/dist/commands/lifecycle/paths.mjs.map +1 -1
  21. package/dist/commands/lifecycle/preflight.mjs +2 -1
  22. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  23. package/dist/commands/lifecycle/setup.mjs +22 -36
  24. package/dist/commands/lifecycle/setup.mjs.map +1 -1
  25. package/dist/commands/lifecycle/uninstall.d.mts +4 -2
  26. package/dist/commands/lifecycle/uninstall.mjs +59 -15
  27. package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
  28. package/dist/commands/memory/memory.mjs +7 -1
  29. package/dist/commands/memory/memory.mjs.map +1 -1
  30. package/dist/lib/helper-scripts.d.mts +1 -1
  31. package/dist/lib/helper-scripts.mjs +10 -18
  32. package/dist/lib/helper-scripts.mjs.map +1 -1
  33. package/dist/lib/host-config.d.mts +72 -0
  34. package/dist/lib/host-config.mjs +404 -0
  35. package/dist/lib/host-config.mjs.map +1 -0
  36. package/dist/lib/host-registry-client.d.mts +2 -2
  37. package/dist/lib/host-registry-client.mjs +0 -3
  38. package/dist/lib/host-registry-client.mjs.map +1 -1
  39. package/dist/lib/install-assets.d.mts +1 -0
  40. package/dist/lib/install-assets.mjs +4 -0
  41. package/dist/lib/install-assets.mjs.map +1 -1
  42. package/dist/lib/proc.d.mts +2 -0
  43. package/dist/lib/proc.mjs +12 -0
  44. package/dist/lib/proc.mjs.map +1 -1
  45. package/dist/lib/python-command.d.mts +2 -0
  46. package/dist/lib/python-command.mjs +52 -0
  47. package/dist/lib/python-command.mjs.map +1 -0
  48. package/dist/lib/python-helper.d.mts +2 -5
  49. package/dist/lib/python-helper.mjs +3 -52
  50. package/dist/lib/python-helper.mjs.map +1 -1
  51. package/dist/lib/runtime-payload.d.mts +23 -0
  52. package/dist/lib/runtime-payload.mjs +57 -0
  53. package/dist/lib/runtime-payload.mjs.map +1 -0
  54. package/dist/lib/types.d.mts +24 -13
  55. package/docs/architecture/storage-model.md +21 -5
  56. package/docs/architecture.md +37 -36
  57. package/docs/cli.md +55 -76
  58. package/docs/coding-rules.md +295 -0
  59. package/docs/container.md +10 -36
  60. package/docs/contributor-change-matrix.md +1 -1
  61. package/docs/for-ai/skills/okstra-code-review.md +0 -1
  62. package/docs/for-ai/skills/okstra-container-build.md +8 -41
  63. package/docs/for-ai/skills/okstra-inspect.md +4 -4
  64. package/docs/for-ai/skills/okstra-manager.md +2 -2
  65. package/docs/for-ai/skills/okstra-rollup.md +0 -1
  66. package/docs/for-ai/skills/okstra-run.md +3 -3
  67. package/docs/for-ai/skills/okstra-user-response.md +0 -1
  68. package/docs/performance-improvement-plan-v2.md +1 -1
  69. package/docs/project-structure-overview.md +62 -62
  70. package/docs/task-process/README.md +3 -3
  71. package/docs/task-process/common-flow.md +1 -1
  72. package/docs/task-process/error-analysis.md +3 -3
  73. package/docs/task-process/implementation-planning.md +3 -1
  74. package/docs/task-process/release-handoff.md +1 -1
  75. package/package.json +3 -2
  76. package/runtime/BUILD.json +2 -2
  77. package/runtime/agents/workers/claude-worker.md +6 -11
  78. package/runtime/agents/workers/report-writer-worker.md +1 -1
  79. package/runtime/bin/lib/okstra/cli.sh +4 -0
  80. package/runtime/bin/lib/okstra/globals.sh +2 -0
  81. package/runtime/bin/lib/okstra/interactive.sh +26 -173
  82. package/runtime/bin/lib/okstra/project-resolver.sh +23 -61
  83. package/runtime/bin/lib/okstra/usage.sh +3 -3
  84. package/runtime/bin/okstra-compact-reminder.sh +1 -5
  85. package/runtime/bin/okstra-error-log.py +19 -2
  86. package/runtime/bin/okstra-import-check.py +26 -0
  87. package/runtime/bin/okstra-inject-report-index.py +5 -4
  88. package/runtime/bin/okstra-provider-exec.py +22 -17
  89. package/runtime/bin/okstra-render-final-report.py +16 -6
  90. package/runtime/bin/okstra-render-report-views.py +25 -18
  91. package/runtime/bin/okstra-report-translate.py +30 -18
  92. package/runtime/bin/okstra-spawn-followups.py +4 -0
  93. package/runtime/bin/okstra-token-usage.py +3 -0
  94. package/runtime/bin/okstra.sh +4 -2
  95. package/runtime/bin/okstra_bootstrap.py +58 -0
  96. package/runtime/prompts/coding-preflight/overview.md +1 -1
  97. package/runtime/prompts/duties/planning-worker.md +1 -1
  98. package/runtime/prompts/launch.template.md +35 -9
  99. package/runtime/prompts/lead/convergence.md +96 -87
  100. package/runtime/prompts/lead/okstra-lead-contract.md +63 -12
  101. package/runtime/prompts/lead/plan-body-verification.md +65 -46
  102. package/runtime/prompts/lead/report-writer.md +34 -4
  103. package/runtime/prompts/lead/team-contract.md +4 -1
  104. package/runtime/prompts/profiles/_clarification-recommendation.md +2 -1
  105. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  106. package/runtime/prompts/profiles/_common-contract.md +3 -3
  107. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  108. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  109. package/runtime/prompts/profiles/_implementation-executor.md +3 -1
  110. package/runtime/prompts/profiles/_implementation-verifier.md +16 -2
  111. package/runtime/prompts/profiles/error-analysis.md +4 -3
  112. package/runtime/prompts/profiles/final-verification.md +2 -2
  113. package/runtime/prompts/profiles/implementation-planning.md +22 -17
  114. package/runtime/prompts/profiles/implementation.md +3 -2
  115. package/runtime/prompts/profiles/requirements-discovery.md +2 -1
  116. package/runtime/prompts/wizard/prompts.ko.json +29 -8
  117. package/runtime/python/okstra_ctl/__init__.py +6 -27
  118. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -5
  119. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +21 -2
  120. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +3 -12
  121. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +40 -22
  122. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +14 -21
  123. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +37 -20
  124. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +86 -14
  125. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +12 -12
  126. package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +4 -7
  127. package/runtime/python/okstra_ctl/agent/__init__.py +1 -0
  128. package/runtime/python/okstra_ctl/{agent_activity.py → agent/activity.py} +12 -1
  129. package/runtime/python/okstra_ctl/{agent_invocation.py → agent/invocation.py} +34 -5
  130. package/runtime/python/okstra_ctl/agent/prompt_cli/__init__.py +19 -0
  131. package/runtime/python/okstra_ctl/agent/prompt_cli/__main__.py +11 -0
  132. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +215 -0
  133. package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +155 -0
  134. package/runtime/python/okstra_ctl/agent/prompt_cli/emit.py +66 -0
  135. package/runtime/python/okstra_ctl/agent/prompt_cli/inputs.py +138 -0
  136. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +408 -0
  137. package/runtime/python/okstra_ctl/agent/prompt_cli/results.py +197 -0
  138. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +107 -0
  139. package/runtime/python/okstra_ctl/analysis_packet.py +47 -4
  140. package/runtime/python/okstra_ctl/approval_decisions.py +260 -8
  141. package/runtime/python/okstra_ctl/assignment_resolver.py +0 -12
  142. package/runtime/python/okstra_ctl/attempt_evidence.py +12 -24
  143. package/runtime/python/okstra_ctl/blocking_checks.py +251 -0
  144. package/runtime/python/okstra_ctl/brief_frontmatter.py +0 -7
  145. package/runtime/python/okstra_ctl/clarification_items/__init__.py +102 -0
  146. package/runtime/python/okstra_ctl/clarification_items/carry.py +224 -0
  147. package/runtime/python/okstra_ctl/clarification_items/dispositions.py +135 -0
  148. package/runtime/python/okstra_ctl/clarification_items/parsing.py +253 -0
  149. package/runtime/python/okstra_ctl/clarification_items/rows.py +115 -0
  150. package/runtime/python/okstra_ctl/clarification_items/scan.py +215 -0
  151. package/runtime/python/okstra_ctl/clarification_items/sidecars.py +211 -0
  152. package/runtime/python/okstra_ctl/cmux.py +38 -31
  153. package/runtime/python/okstra_ctl/code_review_target.py +163 -1
  154. package/runtime/python/okstra_ctl/conformance.py +23 -0
  155. package/runtime/python/okstra_ctl/container.py +38 -421
  156. package/runtime/python/okstra_ctl/context_cost.py +16 -2
  157. package/runtime/python/okstra_ctl/contract_graph_cli.py +7 -0
  158. package/runtime/python/okstra_ctl/convergence.py +101 -15
  159. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +345 -0
  160. package/runtime/python/okstra_ctl/convergence_engine.py +382 -70
  161. package/runtime/python/okstra_ctl/convergence_store.py +5 -29
  162. package/runtime/python/okstra_ctl/design_prep.py +108 -12
  163. package/runtime/python/okstra_ctl/design_snapshot.py +11 -1
  164. package/runtime/python/okstra_ctl/design_surfaces.py +29 -2
  165. package/runtime/python/okstra_ctl/dispatch_core.py +102 -25
  166. package/runtime/python/okstra_ctl/dispatch_state.py +268 -39
  167. package/runtime/python/okstra_ctl/doctor_cli.py +48 -0
  168. package/runtime/python/okstra_ctl/domain/worker_exec.py +10 -5
  169. package/runtime/python/okstra_ctl/domain/worker_presentation.py +21 -1
  170. package/runtime/python/okstra_ctl/domain/worker_stream.py +52 -21
  171. package/runtime/python/okstra_ctl/domain/write_policy.py +219 -0
  172. package/runtime/python/okstra_ctl/entrypoints/hosts.py +9 -2
  173. package/runtime/python/okstra_ctl/error_log_core.py +1 -1
  174. package/runtime/python/okstra_ctl/error_report.py +14 -0
  175. package/runtime/python/okstra_ctl/error_zip.py +18 -2
  176. package/runtime/python/okstra_ctl/execution_identity.py +100 -7
  177. package/runtime/python/okstra_ctl/execution_manifest.py +67 -20
  178. package/runtime/python/okstra_ctl/execution_mutation_audit.py +90 -8
  179. package/runtime/python/okstra_ctl/final_report_paths.py +17 -0
  180. package/runtime/python/okstra_ctl/final_report_schema.py +90 -16
  181. package/runtime/python/okstra_ctl/git_reconcile.py +22 -2
  182. package/runtime/python/okstra_ctl/handoff.py +24 -1
  183. package/runtime/python/okstra_ctl/ids.py +6 -16
  184. package/runtime/python/okstra_ctl/implementation_direction.py +142 -33
  185. package/runtime/python/okstra_ctl/implementation_options.py +25 -11
  186. package/runtime/python/okstra_ctl/implementation_outcome.py +5 -2
  187. package/runtime/python/okstra_ctl/improvement_lenses.py +0 -14
  188. package/runtime/python/okstra_ctl/incremental_carry.py +69 -8
  189. package/runtime/python/okstra_ctl/incremental_scope.py +173 -12
  190. package/runtime/python/okstra_ctl/index.py +2 -2
  191. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +48 -8
  192. package/runtime/python/okstra_ctl/interactive_cli.py +223 -0
  193. package/runtime/python/okstra_ctl/json_boundary.py +10 -0
  194. package/runtime/python/okstra_ctl/listing.py +0 -91
  195. package/runtime/python/okstra_ctl/locks.py +5 -19
  196. package/runtime/python/okstra_ctl/log_report.py +14 -0
  197. package/runtime/python/okstra_ctl/manager_cli.py +22 -3
  198. package/runtime/python/okstra_ctl/manager_launch.py +4 -8
  199. package/runtime/python/okstra_ctl/material.py +2 -2
  200. package/runtime/python/okstra_ctl/migrate.py +23 -1
  201. package/runtime/python/okstra_ctl/model_cli.py +19 -5
  202. package/runtime/python/okstra_ctl/model_discovery.py +46 -47
  203. package/runtime/python/okstra_ctl/model_io/__init__.py +1 -0
  204. package/runtime/python/okstra_ctl/model_io/lines.py +163 -0
  205. package/runtime/python/okstra_ctl/model_io/references.py +370 -0
  206. package/runtime/python/okstra_ctl/model_io/renderers.py +498 -0
  207. package/runtime/python/okstra_ctl/model_io_cli.py +30 -945
  208. package/runtime/python/okstra_ctl/model_pool.py +10 -6
  209. package/runtime/python/okstra_ctl/models.py +8 -8
  210. package/runtime/python/okstra_ctl/mutation_recovery.py +5 -63
  211. package/runtime/python/okstra_ctl/next_phase.py +121 -69
  212. package/runtime/python/okstra_ctl/pane_reclaim.py +23 -0
  213. package/runtime/python/okstra_ctl/pane_title.py +11 -8
  214. package/runtime/python/okstra_ctl/path_hints.py +36 -11
  215. package/runtime/python/okstra_ctl/paths.py +76 -8
  216. package/runtime/python/okstra_ctl/plan_items.py +132 -16
  217. package/runtime/python/okstra_ctl/plan_items_cli.py +486 -36
  218. package/runtime/python/okstra_ctl/plan_run_root.py +1 -1
  219. package/runtime/python/okstra_ctl/plan_validate_cli.py +51 -0
  220. package/runtime/python/okstra_ctl/plan_verify_cli.py +80 -0
  221. package/runtime/python/okstra_ctl/prepare_error.py +20 -0
  222. package/runtime/python/okstra_ctl/prior_planning.py +157 -0
  223. package/runtime/python/okstra_ctl/profile_show.py +18 -1
  224. package/runtime/python/okstra_ctl/project_setup_cli.py +140 -0
  225. package/runtime/python/okstra_ctl/recap.py +22 -1
  226. package/runtime/python/okstra_ctl/reconcile.py +84 -0
  227. package/runtime/python/okstra_ctl/registry/host_registry.py +18 -1
  228. package/runtime/python/okstra_ctl/render.py +98 -32
  229. package/runtime/python/okstra_ctl/render_final_report.py +3 -7
  230. package/runtime/python/okstra_ctl/report_assembly.py +297 -7
  231. package/runtime/python/okstra_ctl/report_contract.py +30 -7
  232. package/runtime/python/okstra_ctl/report_finalize.py +206 -15
  233. package/runtime/python/okstra_ctl/report_html/common.py +2 -2
  234. package/runtime/python/okstra_ctl/report_html/render.py +8 -12
  235. package/runtime/python/okstra_ctl/report_language.py +8 -4
  236. package/runtime/python/okstra_ctl/report_narrative.py +90 -3
  237. package/runtime/python/okstra_ctl/report_projections.py +8 -2
  238. package/runtime/python/okstra_ctl/report_synthesis_packet.py +258 -1
  239. package/runtime/python/okstra_ctl/report_translation.py +2 -35
  240. package/runtime/python/okstra_ctl/report_views.py +4 -22
  241. package/runtime/python/okstra_ctl/resolve_task_key.py +13 -0
  242. package/runtime/python/okstra_ctl/rollup.py +13 -0
  243. package/runtime/python/okstra_ctl/run.py +639 -138
  244. package/runtime/python/okstra_ctl/run_audit.py +13 -0
  245. package/runtime/python/okstra_ctl/run_index_row.py +0 -12
  246. package/runtime/python/okstra_ctl/seeding.py +21 -11
  247. package/runtime/python/okstra_ctl/sequence.py +1 -1
  248. package/runtime/python/okstra_ctl/session.py +64 -14
  249. package/runtime/python/okstra_ctl/set_work_status.py +18 -0
  250. package/runtime/python/okstra_ctl/stage_integrate.py +15 -1
  251. package/runtime/python/okstra_ctl/stage_ledger.py +1 -1
  252. package/runtime/python/okstra_ctl/stage_map.py +57 -11
  253. package/runtime/python/okstra_ctl/stage_map_cli.py +83 -0
  254. package/runtime/python/okstra_ctl/stage_map_view.py +65 -0
  255. package/runtime/python/okstra_ctl/stage_targets.py +3 -12
  256. package/runtime/python/okstra_ctl/task_list_cli.py +138 -0
  257. package/runtime/python/okstra_ctl/task_show_cli.py +66 -0
  258. package/runtime/python/okstra_ctl/task_target.py +4 -16
  259. package/runtime/python/okstra_ctl/team.py +49 -2
  260. package/runtime/python/okstra_ctl/time_report.py +17 -2
  261. package/runtime/python/okstra_ctl/usage_report.py +11 -0
  262. package/runtime/python/okstra_ctl/user_response.py +55 -218
  263. package/runtime/python/okstra_ctl/user_response_values.py +242 -0
  264. package/runtime/python/okstra_ctl/validation_contract.py +3 -0
  265. package/runtime/python/okstra_ctl/verification_target.py +68 -0
  266. package/runtime/python/okstra_ctl/wizard.py +457 -92
  267. package/runtime/python/okstra_ctl/worker_artifacts.py +75 -16
  268. package/runtime/python/okstra_ctl/worker_audit_check.py +22 -0
  269. package/runtime/python/okstra_ctl/worker_dispatch.py +21 -0
  270. package/runtime/python/okstra_ctl/worker_liveness.py +33 -0
  271. package/runtime/python/okstra_ctl/worker_prompt_body.py +15 -2
  272. package/runtime/python/okstra_ctl/worker_prompt_contract.py +98 -7
  273. package/runtime/python/okstra_ctl/worker_prompt_headers.py +24 -2
  274. package/runtime/python/okstra_ctl/worker_prompt_policy.py +18 -3
  275. package/runtime/python/okstra_ctl/worker_request.py +2 -1
  276. package/runtime/python/okstra_ctl/worker_runner.py +16 -9
  277. package/runtime/python/okstra_ctl/worker_state.py +16 -1
  278. package/runtime/python/okstra_ctl/workflow.py +18 -1
  279. package/runtime/python/okstra_ctl/worktree/__init__.py +92 -0
  280. package/runtime/python/okstra_ctl/worktree/cleanliness.py +89 -0
  281. package/runtime/python/okstra_ctl/worktree/decisions.py +127 -0
  282. package/runtime/python/okstra_ctl/worktree/git_ops.py +165 -0
  283. package/runtime/python/okstra_ctl/worktree/linking.py +249 -0
  284. package/runtime/python/okstra_ctl/worktree/naming.py +91 -0
  285. package/runtime/python/okstra_ctl/worktree/provision.py +385 -0
  286. package/runtime/python/okstra_ctl/worktree/sync_config.py +181 -0
  287. package/runtime/python/okstra_ctl/worktree_cli.py +75 -0
  288. package/runtime/python/okstra_ctl/worktree_lookup_cli.py +39 -0
  289. package/runtime/python/okstra_ctl/worktree_registry.py +7 -0
  290. package/runtime/python/okstra_ctl/worktree_status_cli.py +53 -0
  291. package/runtime/python/okstra_ctl/write_policy.py +90 -213
  292. package/runtime/python/okstra_project/__init__.py +0 -4
  293. package/runtime/python/okstra_project/dirs.py +2 -2
  294. package/runtime/python/okstra_project/phase_pointer.py +84 -0
  295. package/runtime/python/okstra_project/slug.py +24 -0
  296. package/runtime/python/okstra_project/state.py +22 -230
  297. package/runtime/python/okstra_token_usage/claude.py +9 -1
  298. package/runtime/python/okstra_token_usage/cli.py +3 -1
  299. package/runtime/python/okstra_token_usage/collect.py +71 -10
  300. package/runtime/python/okstra_token_usage/cursor.py +2 -0
  301. package/runtime/python/okstra_token_usage/grok.py +1 -5
  302. package/runtime/python/okstra_token_usage/paths.py +26 -0
  303. package/runtime/python/okstra_token_usage/pricing.py +19 -7
  304. package/runtime/schemas/convergence-critic-results-v1.0.schema.json +5 -0
  305. package/runtime/schemas/execution-manifest-v2.schema.json +10 -10
  306. package/runtime/schemas/final-report-v2.0.schema.json +9 -26
  307. package/runtime/schemas/final-report-v3.0.schema.json +173 -41
  308. package/runtime/schemas/report-narrative-v3.0.schema.json +3 -2
  309. package/runtime/skills/okstra-brief-gen/SKILL.md +35 -2
  310. package/runtime/skills/okstra-container-build/SKILL.md +16 -47
  311. package/runtime/skills/okstra-inspect/facets/history.md +1 -1
  312. package/runtime/skills/okstra-inspect/facets/status.md +7 -6
  313. package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
  314. package/runtime/skills/okstra-run/SKILL.md +7 -5
  315. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  316. package/runtime/templates/reports/brief.template.md +6 -2
  317. package/runtime/templates/reports/error-analysis-input.template.md +2 -0
  318. package/runtime/templates/reports/final-verification-input.template.md +2 -0
  319. package/runtime/templates/reports/implementation-input.template.md +7 -1
  320. package/runtime/templates/reports/implementation-planning-input.template.md +4 -0
  321. package/runtime/templates/reports/quick-input.template.md +2 -0
  322. package/runtime/templates/reports/release-handoff-input.template.md +6 -1
  323. package/runtime/templates/reports/report.js +20 -3
  324. package/runtime/templates/reports/schedule.template.md +2 -0
  325. package/runtime/templates/reports/settings.template.json +0 -10
  326. package/runtime/templates/reports/task-brief.template.md +3 -0
  327. package/runtime/templates/reports/user-response.template.md +7 -3
  328. package/runtime/validators/checks/fixtures-01.py +57 -0
  329. package/runtime/validators/checks/fixtures-02.py +565 -0
  330. package/runtime/validators/checks/runners-01.py +108 -0
  331. package/runtime/validators/checks/validate-assets-01.py +60 -0
  332. package/runtime/validators/checks/validate-prompt-metadata-01.py +261 -0
  333. package/runtime/validators/checks/validate-tasks-01.py +46 -0
  334. package/runtime/validators/checks/validate-tasks-02.py +85 -0
  335. package/runtime/validators/checks/validate-tasks-03.py +61 -0
  336. package/runtime/validators/checks/validate-tasks-04.py +118 -0
  337. package/runtime/validators/forbidden_actions.py +73 -3
  338. package/runtime/validators/lib/common.sh +5 -0
  339. package/runtime/validators/lib/fixtures.sh +7 -591
  340. package/runtime/validators/lib/paths.sh +13 -0
  341. package/runtime/validators/lib/runners.sh +6 -104
  342. package/runtime/validators/lib/summary.sh +1 -1
  343. package/runtime/validators/lib/validate-assets.sh +6 -56
  344. package/runtime/validators/lib/validate-prompt-metadata.sh +6 -257
  345. package/runtime/validators/lib/validate-tasks.sh +9 -294
  346. package/runtime/validators/validate-implementation-plan-stages.py +4 -4
  347. package/runtime/validators/validate-run.py +1369 -2654
  348. package/runtime/validators/validate-workflow.sh +56 -16
  349. package/runtime/validators/validate_improvement_report.py +2 -1
  350. package/runtime/validators/validate_session_conformance.py +295 -49
  351. package/dist/commands/execute/agent-prompt.d.mts +0 -1
  352. package/dist/commands/execute/agent-prompt.mjs +0 -24
  353. package/dist/commands/execute/agent-prompt.mjs.map +0 -1
  354. package/dist/commands/execute/codex-dispatch.d.mts +0 -3
  355. package/dist/commands/execute/codex-dispatch.mjs +0 -6
  356. package/dist/commands/execute/codex-dispatch.mjs.map +0 -1
  357. package/dist/commands/execute/codex-run.d.mts +0 -3
  358. package/dist/commands/execute/codex-run.mjs +0 -62
  359. package/dist/commands/execute/codex-run.mjs.map +0 -1
  360. package/dist/commands/execute/convergence.d.mts +0 -1
  361. package/dist/commands/execute/convergence.mjs +0 -38
  362. package/dist/commands/execute/convergence.mjs.map +0 -1
  363. package/dist/commands/execute/error-log.d.mts +0 -1
  364. package/dist/commands/execute/error-log.mjs +0 -18
  365. package/dist/commands/execute/error-log.mjs.map +0 -1
  366. package/dist/commands/execute/git-reconcile.d.mts +0 -1
  367. package/dist/commands/execute/git-reconcile.mjs +0 -30
  368. package/dist/commands/execute/git-reconcile.mjs.map +0 -1
  369. package/dist/commands/execute/handoff.d.mts +0 -1
  370. package/dist/commands/execute/handoff.mjs +0 -31
  371. package/dist/commands/execute/handoff.mjs.map +0 -1
  372. package/dist/commands/execute/incremental-carry.d.mts +0 -1
  373. package/dist/commands/execute/incremental-carry.mjs +0 -20
  374. package/dist/commands/execute/incremental-carry.mjs.map +0 -1
  375. package/dist/commands/execute/incremental-scope.d.mts +0 -1
  376. package/dist/commands/execute/incremental-scope.mjs +0 -29
  377. package/dist/commands/execute/incremental-scope.mjs.map +0 -1
  378. package/dist/commands/execute/integrate-stages.d.mts +0 -1
  379. package/dist/commands/execute/integrate-stages.mjs +0 -24
  380. package/dist/commands/execute/integrate-stages.mjs.map +0 -1
  381. package/dist/commands/execute/pane-title.d.mts +0 -1
  382. package/dist/commands/execute/pane-title.mjs +0 -20
  383. package/dist/commands/execute/pane-title.mjs.map +0 -1
  384. package/dist/commands/execute/plan-items.d.mts +0 -1
  385. package/dist/commands/execute/plan-items.mjs +0 -9
  386. package/dist/commands/execute/plan-items.mjs.map +0 -1
  387. package/dist/commands/execute/plan-validate.d.mts +0 -1
  388. package/dist/commands/execute/plan-validate.mjs +0 -68
  389. package/dist/commands/execute/plan-validate.mjs.map +0 -1
  390. package/dist/commands/execute/plan-verify.d.mts +0 -1
  391. package/dist/commands/execute/plan-verify.mjs +0 -43
  392. package/dist/commands/execute/plan-verify.mjs.map +0 -1
  393. package/dist/commands/execute/spawn-followups.d.mts +0 -1
  394. package/dist/commands/execute/spawn-followups.mjs +0 -22
  395. package/dist/commands/execute/spawn-followups.mjs.map +0 -1
  396. package/dist/commands/execute/team.d.mts +0 -3
  397. package/dist/commands/execute/team.mjs +0 -66
  398. package/dist/commands/execute/team.mjs.map +0 -1
  399. package/dist/commands/execute/token-usage.d.mts +0 -1
  400. package/dist/commands/execute/token-usage.mjs +0 -19
  401. package/dist/commands/execute/token-usage.mjs.map +0 -1
  402. package/dist/commands/execute/worker-audit-check.d.mts +0 -1
  403. package/dist/commands/execute/worker-audit-check.mjs +0 -34
  404. package/dist/commands/execute/worker-audit-check.mjs.map +0 -1
  405. package/dist/commands/execute/worker-dispatch.d.mts +0 -7
  406. package/dist/commands/execute/worker-dispatch.mjs +0 -64
  407. package/dist/commands/execute/worker-dispatch.mjs.map +0 -1
  408. package/dist/commands/execute/worker-state.d.mts +0 -1
  409. package/dist/commands/execute/worker-state.mjs +0 -28
  410. package/dist/commands/execute/worker-state.mjs.map +0 -1
  411. package/dist/commands/execute/worktree-lookup.d.mts +0 -1
  412. package/dist/commands/execute/worktree-lookup.mjs +0 -92
  413. package/dist/commands/execute/worktree-lookup.mjs.map +0 -1
  414. package/dist/commands/execute/worktree-status.d.mts +0 -1
  415. package/dist/commands/execute/worktree-status.mjs +0 -121
  416. package/dist/commands/execute/worktree-status.mjs.map +0 -1
  417. package/dist/commands/inspect/code-review.d.mts +0 -1
  418. package/dist/commands/inspect/code-review.mjs +0 -32
  419. package/dist/commands/inspect/code-review.mjs.map +0 -1
  420. package/dist/commands/inspect/container.d.mts +0 -1
  421. package/dist/commands/inspect/container.mjs +0 -25
  422. package/dist/commands/inspect/container.mjs.map +0 -1
  423. package/dist/commands/inspect/context-cost.d.mts +0 -1
  424. package/dist/commands/inspect/context-cost.mjs +0 -25
  425. package/dist/commands/inspect/context-cost.mjs.map +0 -1
  426. package/dist/commands/inspect/design-prep.d.mts +0 -1
  427. package/dist/commands/inspect/design-prep.mjs +0 -22
  428. package/dist/commands/inspect/design-prep.mjs.map +0 -1
  429. package/dist/commands/inspect/error-report.d.mts +0 -1
  430. package/dist/commands/inspect/error-report.mjs +0 -25
  431. package/dist/commands/inspect/error-report.mjs.map +0 -1
  432. package/dist/commands/inspect/error-zip.d.mts +0 -1
  433. package/dist/commands/inspect/error-zip.mjs +0 -24
  434. package/dist/commands/inspect/error-zip.mjs.map +0 -1
  435. package/dist/commands/inspect/log-report.d.mts +0 -1
  436. package/dist/commands/inspect/log-report.mjs +0 -26
  437. package/dist/commands/inspect/log-report.mjs.map +0 -1
  438. package/dist/commands/inspect/model-io.d.mts +0 -1
  439. package/dist/commands/inspect/model-io.mjs +0 -25
  440. package/dist/commands/inspect/model-io.mjs.map +0 -1
  441. package/dist/commands/inspect/profile-show.d.mts +0 -1
  442. package/dist/commands/inspect/profile-show.mjs +0 -28
  443. package/dist/commands/inspect/profile-show.mjs.map +0 -1
  444. package/dist/commands/inspect/recap.d.mts +0 -1
  445. package/dist/commands/inspect/recap.mjs +0 -30
  446. package/dist/commands/inspect/recap.mjs.map +0 -1
  447. package/dist/commands/inspect/resolve-task-key.d.mts +0 -1
  448. package/dist/commands/inspect/resolve-task-key.mjs +0 -24
  449. package/dist/commands/inspect/resolve-task-key.mjs.map +0 -1
  450. package/dist/commands/inspect/rollup.d.mts +0 -1
  451. package/dist/commands/inspect/rollup.mjs +0 -25
  452. package/dist/commands/inspect/rollup.mjs.map +0 -1
  453. package/dist/commands/inspect/run-audit.d.mts +0 -1
  454. package/dist/commands/inspect/run-audit.mjs +0 -25
  455. package/dist/commands/inspect/run-audit.mjs.map +0 -1
  456. package/dist/commands/inspect/set-work-status.d.mts +0 -1
  457. package/dist/commands/inspect/set-work-status.mjs +0 -29
  458. package/dist/commands/inspect/set-work-status.mjs.map +0 -1
  459. package/dist/commands/inspect/stage-map.d.mts +0 -1
  460. package/dist/commands/inspect/stage-map.mjs +0 -131
  461. package/dist/commands/inspect/stage-map.mjs.map +0 -1
  462. package/dist/commands/inspect/task-list.d.mts +0 -1
  463. package/dist/commands/inspect/task-list.mjs +0 -149
  464. package/dist/commands/inspect/task-list.mjs.map +0 -1
  465. package/dist/commands/inspect/task-show.d.mts +0 -1
  466. package/dist/commands/inspect/task-show.mjs +0 -108
  467. package/dist/commands/inspect/task-show.mjs.map +0 -1
  468. package/dist/commands/inspect/time-report.d.mts +0 -1
  469. package/dist/commands/inspect/time-report.mjs +0 -24
  470. package/dist/commands/inspect/time-report.mjs.map +0 -1
  471. package/dist/commands/inspect/usage-report.d.mts +0 -1
  472. package/dist/commands/inspect/usage-report.mjs +0 -23
  473. package/dist/commands/inspect/usage-report.mjs.map +0 -1
  474. package/dist/commands/inspect/user-response.d.mts +0 -1
  475. package/dist/commands/inspect/user-response.mjs +0 -35
  476. package/dist/commands/inspect/user-response.mjs.map +0 -1
  477. package/dist/commands/inspect/worker-liveness.d.mts +0 -1
  478. package/dist/commands/inspect/worker-liveness.mjs +0 -45
  479. package/dist/commands/inspect/worker-liveness.mjs.map +0 -1
  480. package/dist/commands/lifecycle/contract-check.d.mts +0 -1
  481. package/dist/commands/lifecycle/contract-check.mjs +0 -18
  482. package/dist/commands/lifecycle/contract-check.mjs.map +0 -1
  483. package/dist/commands/lifecycle/migrate.d.mts +0 -1
  484. package/dist/commands/lifecycle/migrate.mjs +0 -30
  485. package/dist/commands/lifecycle/migrate.mjs.map +0 -1
  486. package/dist/commands/lifecycle/model.d.mts +0 -1
  487. package/dist/commands/lifecycle/model.mjs +0 -22
  488. package/dist/commands/lifecycle/model.mjs.map +0 -1
  489. package/dist/commands/manager.d.mts +0 -3
  490. package/dist/commands/manager.mjs +0 -50
  491. package/dist/commands/manager.mjs.map +0 -1
  492. package/dist/commands/report/agent-activity.d.mts +0 -1
  493. package/dist/commands/report/agent-activity.mjs +0 -20
  494. package/dist/commands/report/agent-activity.mjs.map +0 -1
  495. package/dist/commands/report/approval-decision.d.mts +0 -1
  496. package/dist/commands/report/approval-decision.mjs +0 -21
  497. package/dist/commands/report/approval-decision.mjs.map +0 -1
  498. package/dist/commands/report/design-snapshot.d.mts +0 -1
  499. package/dist/commands/report/design-snapshot.mjs +0 -19
  500. package/dist/commands/report/design-snapshot.mjs.map +0 -1
  501. package/dist/commands/report/finalize.d.mts +0 -4
  502. package/dist/commands/report/finalize.mjs +0 -64
  503. package/dist/commands/report/finalize.mjs.map +0 -1
  504. package/dist/commands/report/inject-report-index.d.mts +0 -1
  505. package/dist/commands/report/inject-report-index.mjs +0 -21
  506. package/dist/commands/report/inject-report-index.mjs.map +0 -1
  507. package/dist/commands/report/render-final-report.d.mts +0 -1
  508. package/dist/commands/report/render-final-report.mjs +0 -23
  509. package/dist/commands/report/render-final-report.mjs.map +0 -1
  510. package/dist/commands/report/render-views.d.mts +0 -1
  511. package/dist/commands/report/render-views.mjs +0 -27
  512. package/dist/commands/report/render-views.mjs.map +0 -1
  513. package/dist/commands/report/translate.d.mts +0 -1
  514. package/dist/commands/report/translate.mjs +0 -33
  515. package/dist/commands/report/translate.mjs.map +0 -1
  516. package/runtime/bin/lib/okstra/tmux-pane.sh +0 -40
  517. package/runtime/bin/lib/okstra-ctl/cmd-batch.sh +0 -59
  518. package/runtime/bin/lib/okstra-ctl/cmd-list.sh +0 -35
  519. package/runtime/bin/lib/okstra-ctl/cmd-open.sh +0 -36
  520. package/runtime/bin/lib/okstra-ctl/cmd-projects.sh +0 -26
  521. package/runtime/bin/lib/okstra-ctl/cmd-reconcile.sh +0 -29
  522. package/runtime/bin/lib/okstra-ctl/cmd-reindex.sh +0 -38
  523. package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +0 -345
  524. package/runtime/bin/lib/okstra-ctl/cmd-show.sh +0 -27
  525. package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +0 -92
  526. package/runtime/bin/lib/okstra-ctl/main.sh +0 -41
  527. package/runtime/bin/lib/okstra-ctl/prepare.sh +0 -31
  528. package/runtime/bin/lib/okstra-ctl/usage.sh +0 -23
  529. package/runtime/bin/okstra-central.sh +0 -152
  530. package/runtime/bin/okstra-incremental-carry.py +0 -10
  531. package/runtime/bin/okstra-incremental-scope.py +0 -10
  532. package/runtime/bin/okstra-team-reconcile.sh +0 -36
  533. package/runtime/python/okstra_ctl/agent_prompt_cli.py +0 -1156
  534. package/runtime/python/okstra_ctl/batch.py +0 -60
  535. package/runtime/python/okstra_ctl/clarification_items.py +0 -1050
  536. package/runtime/python/okstra_ctl/container_registry.py +0 -72
  537. package/runtime/python/okstra_ctl/improvement_assignment.py +0 -61
  538. package/runtime/python/okstra_ctl/resolver.py +0 -54
  539. package/runtime/python/okstra_ctl/team_reconcile.py +0 -275
  540. package/runtime/python/okstra_ctl/tmux.py +0 -134
  541. package/runtime/python/okstra_ctl/worktree.py +0 -1099
@@ -17,9 +17,7 @@ from __future__ import annotations
17
17
 
18
18
  import argparse
19
19
  import json
20
- import os
21
20
  import re
22
- import shlex
23
21
  import subprocess
24
22
  import sys
25
23
  import time
@@ -35,34 +33,6 @@ OKSTRA_LABEL_KEYS = ("okstra.task-key", "okstra.project-name", "okstra.run-trace
35
33
 
36
34
  COMPOSE_FILENAME = "docker-compose.yml"
37
35
 
38
- # spec §5/§10 — 에러 트리거 화이트리스트. 이 정규식 중 하나라도 라인에 걸려야
39
- # 2단계(LLM analyze)로 넘어간다. 건강한 컨테이너는 어느 것도 안 걸려 토큰 0.
40
- ERROR_PATTERNS = (
41
- re.compile(r"\bERROR\b"),
42
- re.compile(r"\bFATAL\b"),
43
- re.compile(r"\bCRITICAL\b"),
44
- re.compile(r"\bPANIC\b"),
45
- re.compile(r"\b\w+(?:Error|Exception)\b"),
46
- re.compile(r"^Traceback \(most recent call last\):"),
47
- re.compile(r"\b(?:exit(?:ed)?|exit code|status)\s+(?:code\s+)?[1-9]\d*\b", re.I),
48
- re.compile(r"\bUnhandled\b", re.I),
49
- )
50
-
51
- # error_signature 정규화: 변동성 노이즈(타임스탬프/pid/라인참조/hex 주소)만 깎고,
52
- # ERROR_PATTERNS 가 *탐지하는* 의미 있는 코드(HTTP status, exit/signal code)는 보존한다.
53
- # 블랭킷 `\d+` 치환은 distinct 에러(500 vs 404, exit 1 vs 137)를 한 시그니처로
54
- # collapse 시켜 디바운스가 영영 분석을 막으므로 쓰지 않는다(spec §10 signal vs noise).
55
- _NOISE_SUBS = (
56
- (re.compile(
57
- r"\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:?\d{2})?"
58
- ), "<ts>"),
59
- (re.compile(r"\b0x[0-9a-fA-F]+\b"), "<hex>"),
60
- (re.compile(r"\b(pid|tid|ppid)=\d+", re.I), r"\1=<n>"),
61
- (re.compile(r":\d+:"), ":<n>:"),
62
- )
63
- _WS_RE = re.compile(r"\s+")
64
-
65
-
66
36
  def merge_env(base_env: dict, override: dict) -> dict:
67
37
  """base_env 위에 override 를 얹는다. 동일 키는 override 가 이긴다."""
68
38
  return {**base_env, **override}
@@ -126,179 +96,6 @@ def build_compose_up_argv(
126
96
  return argv
127
97
 
128
98
 
129
- def scan_log_chunk(text: str, patterns: Iterable[re.Pattern]) -> list[str]:
130
- """ERROR_PATTERNS 중 하나라도 걸리는 라인만 순서대로 반환(순수, docker 무의존)."""
131
- return [
132
- line
133
- for line in text.splitlines()
134
- if line and any(p.search(line) for p in patterns)
135
- ]
136
-
137
-
138
- def error_signature(line: str) -> str:
139
- """디바운스 키: 변동성 노이즈만 깎고 의미 있는 코드는 보존한 라인.
140
-
141
- 타임스탬프/pid/라인참조/hex 주소는 placeholder 로 정규화하지만 HTTP status·
142
- exit/signal code 같은 판별자는 그대로 둔다. 같은 에러가 timestamp·pid 만
143
- 달리해 반복되면 동일 키 → analyze 1회(spec §10 디바운스), 그러나 500 vs 404
144
- 처럼 의미가 다르면 별도 키 → 각각 분석된다.
145
- """
146
- s = line
147
- for pattern, repl in _NOISE_SUBS:
148
- s = pattern.sub(repl, s)
149
- return _WS_RE.sub(" ", s).strip()
150
-
151
-
152
- def watcher_step(
153
- *,
154
- log_chunk: str,
155
- seen_signatures: set,
156
- findings_path: Path,
157
- analyze: Callable[[str], object],
158
- ) -> int:
159
- """log_chunk 를 1단계 스캔 → 새 시그니처마다 analyze 호출 + findings append.
160
-
161
- 이미 본 시그니처는 디바운스(skip). 새 분석 건수를 반환한다. analyze 는 주입이라
162
- 유닛 테스트는 스텁으로 LLM 없이 행위를 검증한다.
163
- """
164
- new_count = 0
165
- for line in scan_log_chunk(log_chunk, ERROR_PATTERNS):
166
- sig = error_signature(line)
167
- if sig in seen_signatures:
168
- continue
169
- seen_signatures.add(sig)
170
- # 탐지 자체를 analyze 보다 먼저 영속화한다 — analyze(=pane spawn) 가 실패해도
171
- # 에러 라인은 findings 에 남는다. analyze 예외가 장기 watcher 루프를 죽이지
172
- # 않도록 격리하고, 실패 사실도 findings 에 기록한다(silent 중단 방지).
173
- _append_finding(Path(findings_path), sig, line)
174
- try:
175
- analyze(line)
176
- except Exception as exc: # noqa: BLE001 — best-effort 외부 spawn, 루프 보존이 우선
177
- _append_finding(Path(findings_path), sig, f"[analyze 실패] {exc}")
178
- new_count += 1
179
- return new_count
180
-
181
-
182
- def _append_finding(findings_path: Path, signature: str, line: str) -> None:
183
- findings_path.parent.mkdir(parents=True, exist_ok=True)
184
- block = f"## {signature}\n\n```\n{line}\n```\n\n"
185
- with findings_path.open("a", encoding="utf-8") as fh:
186
- fh.write(block)
187
-
188
-
189
- # spec §9 — watcher AI 는 "탐지·보고만, 자동 치유 제외". 기본 analyze 가 기동하는
190
- # 에이전트는 findings 파일 append 외 어떤 코드/설정 Edit/Write 도 금지한다. 이
191
- # 계약 문구를 에이전트 프롬프트에 그대로 넣어 enforcement 를 명시한다.
192
- WATCHER_READONLY_CONTRACT = (
193
- "You are a READ-ONLY container log watcher. You MUST NOT Edit or Write any "
194
- "code or config file. Your ONLY permitted write is appending an analysis "
195
- "note to the findings file. Detect and report; never auto-fix."
196
- )
197
-
198
-
199
- def default_analyze(findings_path: Path, *, session_pane: str | None = None):
200
- """프로덕션 기본 analyze 팩토리: read-only 계약을 건 에이전트를 pane 으로 기동.
201
-
202
- codex/antigravity-worker 와 동일한 split_container_pane 메커니즘을 쓰되
203
- WATCHER_READONLY_CONTRACT 를 프롬프트에 박아 코드/설정 수정 권한을 차단한다.
204
- 라이브 spawn 세부(에이전트 CLI·모델·권한 스코프)는 아직 미구현이라 e2e 영역으로
205
- 둔다 — session_pane 이 없으면 split_container_pane 이 guard 로 None 을 돌려주어
206
- 실제 spawn 없이 no-op 이 된다(에러 탐지·findings 기록은 watcher_step 이 담당).
207
- 프롬프트를 셸 명령으로 그대로 실행하지 않도록, 자동 `$TMUX_PANE` fallback 은
208
- 두지 않는다 — 자연어 프롬프트가 pane 명령으로 새지 않게 하는 안전장치다.
209
- """
210
- from okstra_ctl import tmux
211
-
212
- def analyze(window: str) -> str | None:
213
- prompt = f"{WATCHER_READONLY_CONTRACT}\n\nError window:\n{window}\n"
214
- return tmux.split_container_pane(
215
- session_pane=session_pane,
216
- cwd=str(Path(findings_path).parent),
217
- command=prompt,
218
- title="okstra-watcher",
219
- scope_value=str(findings_path),
220
- kind="watcher",
221
- )
222
-
223
- return analyze
224
-
225
-
226
- def build_watcher_pane_command(
227
- *, python_exe: str, module_root: str, project_name: str,
228
- service: str, findings_path: str, scan_interval_seconds: float,
229
- ) -> str:
230
- """watcher pane 이 실행할 셸 명령 문자열.
231
-
232
- `python -m okstra_ctl.container watch` 를 PYTHONPATH 와 함께 박는다 — tmux pane
233
- 의 환경 상속에 의존하면 PYTHONPATH 가 비어 import 가 깨지므로, 현재 프로세스의
234
- module-root 를 명시적으로 넘긴다. 모든 인자는 shlex 로 인용해 경로에 공백/메타
235
- 문자가 있어도 안전하다."""
236
- return " ".join([
237
- f"PYTHONPATH={shlex.quote(module_root)}",
238
- shlex.quote(python_exe), "-m", "okstra_ctl.container", "watch",
239
- "--project-name", shlex.quote(project_name),
240
- "--service", shlex.quote(service),
241
- "--findings", shlex.quote(findings_path),
242
- "--scan-interval", str(scan_interval_seconds),
243
- ])
244
-
245
-
246
- def run_service_watcher(
247
- *, project_name: str, service: str, findings_path: str,
248
- scan_interval_seconds: float,
249
- ) -> None:
250
- """`container watch` 서브커맨드 본체 — 한 서비스의 감시 루프를 돈다.
251
-
252
- stage ① 경량 스캔(ERROR 패턴) → 매칭 시 findings append + stage ② analyze.
253
- 사용자가 pane/세션을 종료(stop-watcher/down)할 때까지 지속한다."""
254
- analyze = default_analyze(Path(findings_path))
255
- run_watcher_loop(
256
- service=service, project_name=project_name,
257
- findings_path=Path(findings_path),
258
- scan_interval_seconds=scan_interval_seconds, analyze=analyze,
259
- )
260
-
261
-
262
- def run_watcher_loop(
263
- *,
264
- service: str,
265
- project_name: str,
266
- findings_path: Path,
267
- scan_interval_seconds: float,
268
- analyze: Callable[[str], object],
269
- ) -> None:
270
- """`docker compose -p <project> logs --since <last>` 증분 fetch + sleep 루프.
271
-
272
- 사용자 종료(stop-watcher/down) 까지 지속. 토큰은 ERROR_PATTERNS 가 걸릴 때만
273
- 쓴다. 루프는 얇게 — 매 tick 의 판단은 watcher_step 에 위임한다.
274
- """
275
- seen: set = set()
276
- # 첫 tick 은 --since 없이 컨테이너 기동 이후 backlog 전체를 본다. `--since 0s`
277
- # 는 'now 이후'를 의미해 기동 중 찍힌 startup-failure 에러를 영구히 놓친다.
278
- since: str | None = None
279
- while True:
280
- tick_start = time.monotonic()
281
- chunk = _fetch_log_chunk(service=service, project_name=project_name, since=since)
282
- watcher_step(
283
- log_chunk=chunk, seen_signatures=seen,
284
- findings_path=Path(findings_path), analyze=analyze,
285
- )
286
- time.sleep(scan_interval_seconds)
287
- # 다음 윈도우는 '직전 fetch 이후 실제 경과시간'(sleep + analyze 포함)을
288
- # 덮는다. 고정 interval 윈도우는 analyze 가 interval 보다 오래 걸리면 그
289
- # 사이 찍힌 에러 라인을 영구히 놓친다. +1s 오버랩의 중복은 디바운스가 흡수.
290
- since = f"{int(time.monotonic() - tick_start) + 1}s"
291
-
292
-
293
- def _fetch_log_chunk(*, service: str, project_name: str, since: str | None) -> str:
294
- argv = ["docker", "compose", "-p", project_name, "logs", "--no-color"]
295
- if since: # 첫 tick(None)은 --since 생략 → 기동 backlog 전체
296
- argv += ["--since", since]
297
- argv.append(service)
298
- result = subprocess.run(argv, capture_output=True, text=True, check=False)
299
- return result.stdout or ""
300
-
301
-
302
99
  # --------------------------------------------------------------------------- #
303
100
  # compose 파싱 — docker 가 정본(canonical). 손수 정규식 파서는 라벨 SSOT 를
304
101
  # silent 하게 깨므로 쓰지 않는다(inline comment / quoted name / 4-space 들여쓰기
@@ -600,11 +397,10 @@ def provision_container_group(
600
397
  *, project_root, task_key: str,
601
398
  healthcheck_timeout_seconds: int = 120,
602
399
  healthcheck_interval_seconds: int = 3,
603
- scan_interval_seconds: float = 5.0,
604
400
  ) -> dict:
605
401
  """skill / bin okstra / okstra.sh 가 수렴하는 container up 단일 진입점."""
606
402
  from . import paths
607
- from .ids import compose_project_name, build_container_session_name
403
+ from .ids import compose_project_name, build_container_run_trace
608
404
 
609
405
  project_root = Path(project_root)
610
406
  inputs = _resolve_up_inputs(project_root, task_key)
@@ -613,7 +409,7 @@ def provision_container_group(
613
409
  cp = paths.container_paths(project_root, inputs["task_group"], inputs["task_id"])
614
410
  project_name = compose_project_name(
615
411
  inputs["project_id"], inputs["task_group"], inputs["task_id"])
616
- session_name = build_container_session_name(
412
+ run_trace = build_container_run_trace(
617
413
  inputs["project_id"], inputs["task_group"], inputs["task_id"])
618
414
  worktree_env, override_env = _synthesize_env_override(worktree, cp)
619
415
  env_files = [p for p in (worktree_env, override_env) if p]
@@ -621,7 +417,7 @@ def provision_container_group(
621
417
  services = resolve_compose_services(compose, env_files, str(worktree))
622
418
  started = _compose_up(
623
419
  cp=cp, worktree=worktree, compose=compose, project_name=project_name,
624
- task_key=task_key, session_name=session_name, services=services,
420
+ task_key=task_key, run_trace=run_trace, services=services,
625
421
  worktree_env=worktree_env, override_env=override_env)
626
422
  if not started:
627
423
  # ps 가 빈 출력(데몬 일시 오류)이거나 모든 서비스가 미생성이면 started 가
@@ -631,7 +427,7 @@ def provision_container_group(
631
427
  # 가능하게 한다 — 실제 기동분을 모르니 full services 를 best-effort 로 기록.
632
428
  _write_deploy_state(
633
429
  cp, project_name=project_name, task_key=task_key, services=services,
634
- warnings=warnings, watch={"enabled": False, "note": "기동 컨테이너 없음"})
430
+ warnings=warnings)
635
431
  raise PrepareError(
636
432
  f"container up: `up -d` 후 기동된 컨테이너가 없습니다(`docker compose -p "
637
433
  f"{project_name} ps -a` 로 확인). compose 정의·활성 profile 을 점검한 뒤 "
@@ -645,24 +441,18 @@ def provision_container_group(
645
441
  # `down --all` 스윕(deploy-state.json 있는 task 만 발견)이 회수할 수 있다.
646
442
  _write_deploy_state(
647
443
  cp, project_name=project_name, task_key=task_key, services=started,
648
- warnings=warnings, watch={"enabled": False, "note": "healthcheck 미통과"},
649
- healthcheckFailed=failed)
444
+ warnings=warnings, healthcheckFailed=failed)
650
445
  raise PrepareError(
651
446
  f"container up: 다음 서비스가 시간 내 기동하지 못했습니다: {', '.join(failed)} "
652
447
  f"(timeout={healthcheck_timeout_seconds}s). `docker compose -p "
653
448
  f"{project_name} logs` 로 원인을 확인한 뒤 `okstra container down "
654
449
  f"--task-key {task_key}` 로 정리하세요.")
655
- # watcher/deploy-state 는 실제 기동된 started 만 대상으로 한다 — full services
656
- # 를 쓰면 profiles 미기동 서비스에 컨테이너 없는 tail/watcher pane 과 registry
657
- # 항목이 phantom 으로 생겨 배포 수명 동안 누수된다.
658
- watch = _spawn_watchers(cp=cp, worktree=worktree, project_name=project_name,
659
- services=started, session_name=session_name,
660
- project_root=project_root, inputs=inputs,
661
- scan_interval_seconds=scan_interval_seconds)
450
+ # deploy-state 는 실제 기동된 started 만 기록한다 — full services 를 쓰면
451
+ # profiles 로 안 뜬 서비스가 `down --all` 스윕의 대상으로 남는다.
662
452
  _write_deploy_state(cp, project_name=project_name, task_key=task_key,
663
- services=started, warnings=warnings, watch=watch)
453
+ services=started, warnings=warnings)
664
454
  return {"projectName": project_name, "services": started,
665
- "warnings": warnings, "watch": watch}
455
+ "warnings": warnings}
666
456
 
667
457
 
668
458
  def _integrate_source_tree(inputs: dict) -> str:
@@ -679,7 +469,7 @@ def _integrate_source_tree(inputs: dict) -> str:
679
469
 
680
470
  def _compose_up(
681
471
  *, cp: dict, worktree: Path, compose: Path, project_name: str, task_key: str,
682
- session_name: str, services: list[str], worktree_env: str, override_env: str,
472
+ run_trace: str, services: list[str], worktree_env: str, override_env: str,
683
473
  ) -> list[str]:
684
474
  """label override 주입 → compose up → 실제 기동된 서비스명 반환.
685
475
 
@@ -689,9 +479,9 @@ def _compose_up(
689
479
  labels = {
690
480
  "okstra.task-key": task_key,
691
481
  "okstra.project-name": project_name,
692
- # run-trace 는 이 기능의 trace 식별자(container session name)로 채워
693
- # project-name 과 distinct 하게 둔다(M1).
694
- "okstra.run-trace": session_name,
482
+ # run-trace 는 이 기능의 trace 식별자로 채워 project-name 과
483
+ # distinct 하게 둔다(M1).
484
+ "okstra.run-trace": run_trace,
695
485
  }
696
486
  label_path = _write_label_override(cp, services, labels)
697
487
  argv = build_compose_up_argv(
@@ -719,117 +509,27 @@ def _started_services(project_name: str, cwd: str, services: list[str]) -> list[
719
509
  return [s for s in services if s in created]
720
510
 
721
511
 
722
- def _spawn_watchers(
723
- *, cp: dict, worktree: Path, project_name: str, services: list[str],
724
- session_name: str, project_root: Path, inputs: dict, scan_interval_seconds: float,
725
- ) -> dict:
726
- """tmux 가 있으면 detached 세션 + 서비스별 tail/watcher pane. 없으면 skip.
727
-
728
- pane split 실패(pane 한도/세션 외부 종료 등)는 watcher 만 degrade 시키고 raise
729
- 하지 않는다 — 컨테이너는 이미 떠 있으므로 호출자가 deploy-state 를 반드시 써서
730
- `down --all` 스윕에서 보이게 해야 한다."""
731
- from . import tmux
732
-
733
- if not tmux.tmux_available():
734
- return {"enabled": False, "note": "감시 비활성(tmux 없음)"}
735
- cp["watchers_dir"].mkdir(parents=True, exist_ok=True)
736
- started: list[str] = []
737
- try:
738
- # holder pane 은 기본 셸로 띄워 오래 살린다(`true` 는 split 전에 세션을
739
- # 무너뜨림). container 태그를 붙여 down/stop-watcher 의 스코프 reap 로
740
- # tail/watcher pane 과 함께 회수되게 한다(마지막 pane 회수 시 세션 종료).
741
- pane = tmux.new_detached_session(session_name, str(worktree))
742
- tmux.tag_container_pane(pane, str(cp["container_dir"]))
743
- for svc in services:
744
- _spawn_service_panes(
745
- pane=pane, cp=cp, worktree=worktree, project_name=project_name,
746
- svc=svc, session_name=session_name, project_root=project_root,
747
- inputs=inputs, scan_interval_seconds=scan_interval_seconds)
748
- started.append(svc)
749
- except (RuntimeError, OSError, subprocess.SubprocessError) as exc:
750
- # watcher spawn 실패(tmux split/RuntimeError, registry flock/디스크 OSError,
751
- # tmux timeout/SubprocessError)는 watcher 만 degrade 시키고 raise 하지
752
- # 않는다 — 호출자가 deploy-state 를 반드시 써 컨테이너 leak 을 막는다.
753
- return {"enabled": True, "session": session_name, "services": started,
754
- "partial": True, "note": f"watcher 부분 실패: {exc}"}
755
- return {"enabled": True, "session": session_name, "services": started}
756
-
757
-
758
- def _spawn_service_panes(
759
- *, pane: str, cp: dict, worktree: Path, project_name: str, svc: str,
760
- session_name: str, project_root: Path, inputs: dict, scan_interval_seconds: float,
761
- ) -> None:
762
- """한 서비스의 tail pane + watcher loop pane 을 split 하고 registry 에 등록."""
763
- from . import tmux, container_registry
764
-
765
- findings = cp["watchers_dir"] / f"{svc}.findings.md"
766
- tmux.split_container_pane(
767
- session_pane=pane, cwd=str(worktree),
768
- command=f"docker compose -p {project_name} logs -f {svc}",
769
- title=f"tail:{svc}", scope_value=str(cp["container_dir"]), kind="tail")
770
- wpane = tmux.split_container_pane(
771
- session_pane=pane, cwd=str(worktree),
772
- command=build_watcher_pane_command(
773
- python_exe=sys.executable,
774
- module_root=str(Path(__file__).resolve().parent.parent),
775
- project_name=project_name, service=svc, findings_path=str(findings),
776
- scan_interval_seconds=scan_interval_seconds),
777
- title=f"watch:{svc}", scope_value=str(cp["container_dir"]), kind="watcher")
778
- container_registry.reserve(
779
- project_root, inputs["task_group"], inputs["task_id"], svc,
780
- session_name=session_name, pane_id=wpane or "", findings_path=str(findings))
781
-
782
-
783
512
  def _write_deploy_state(cp: dict, **state) -> None:
784
513
  cp["container_dir"].mkdir(parents=True, exist_ok=True)
785
514
  write_owned_object_atomic(cp["deploy_state"], state, artifact="container deploy state")
786
515
 
787
516
 
788
517
  # --------------------------------------------------------------------------- #
789
- # status / logs / stop-watcher / down
518
+ # status / down
790
519
  # --------------------------------------------------------------------------- #
791
520
 
792
521
  def status_container_group(*, project_root, task_key: str) -> dict:
793
- """라벨 쿼리(컨테이너 존재 SSOT) + registry(보조 watcher 메타) 합본."""
794
- from . import container_registry
522
+ """라벨 쿼리 — 컨테이너 존재 여부의 기준값(SSOT)."""
795
523
  from .ids import compose_project_name
796
524
 
797
525
  project_id, group, task_id = _split_task_key(task_key)
798
526
  project_name = compose_project_name(project_id, group, task_id)
799
- containers = query_containers_by_label(project_name)
800
- registry = container_registry.lookup(Path(project_root), group, task_id)
801
- return {"projectName": project_name, "containers": containers,
802
- "watchers": registry.get("services", {})}
803
-
804
-
805
- def logs_container_group(*, project_root, task_key: str, service: str = "") -> dict:
806
- """deploy-state + watcher findings 경로를 안내(로그 자체는 tmux pane 에 있음)."""
807
- from . import paths, container_registry
808
-
809
- project_id, group, task_id = _split_task_key(task_key)
810
- cp = paths.container_paths(Path(project_root), group, task_id)
811
- services = container_registry.lookup(Path(project_root), group, task_id).get(
812
- "services", {})
813
- if service:
814
- services = {service: services[service]} if service in services else {}
815
- return {"watchersDir": str(cp["watchers_dir"]), "watchers": services}
816
-
817
-
818
- def stop_watcher_group(*, project_root, task_key: str) -> dict:
819
- """watcher/tail pane(@okstra_container_run) 만 회수하고 컨테이너는 살려둔다."""
820
- project_id, group, task_id = _split_task_key(task_key)
821
- reaped = _reap_container_panes(Path(project_root), group, task_id)
822
- return {"reapedPanes": reaped, "note": "컨테이너는 유지됨(감시만 종료)"}
527
+ return {"projectName": project_name,
528
+ "containers": query_containers_by_label(project_name)}
823
529
 
824
530
 
825
531
  def down_container_group(*, project_root, task_key: str = "", all_groups: bool = False) -> dict:
826
- """라벨 쿼리로 컨테이너 down + tmux 실측으로 orphan watcher 회수.
827
-
828
- registry 는 보조 인덱스라 누락돼도 'watcher 없음'을 의미하지 않는다 — tmux 를
829
- 직접 스캔(@okstra_container_run)해 orphan pane 까지 reap 한다(spec §11).
830
- pane 회수는 항상 현재 project-root 스코프로 한정한다 — 동시 세션이 흔한
831
- 레포라 다른 task/프로젝트의 watcher pane 을 절대 죽이지 않는다(I2)."""
832
- from . import paths
532
+ """라벨 쿼리로 컨테이너를 down 한다."""
833
533
  from .ids import compose_project_name
834
534
 
835
535
  project_root = Path(project_root)
@@ -843,19 +543,8 @@ def down_container_group(*, project_root, task_key: str = "", all_groups: bool =
843
543
  # 영속 데이터를 파괴하지 않는다.
844
544
  _docker_run(["docker", "compose", "-p", project_name, "down",
845
545
  "--remove-orphans"])
846
- reaped = _reap_container_panes(project_root, group, task_id)
847
- results.append({"projectName": project_name, "reapedPanes": reaped})
848
- # registry drift 로 등록 안 된 orphan pane 회수는 `--all`(이 프로젝트 전체)
849
- # 때만, 그것도 **이 project-root 의 .okstra/ prefix** 로만 스코프한다. 단일
850
- # down 은 위 per-task 회수로 끝낸다(글로벌 sweep 금지 — 동시 세션 간섭 차단).
851
- orphans = _reap_project_orphan_panes(project_root) if all_groups else []
852
- return {"downed": results, "orphanPanesReaped": orphans}
853
-
854
-
855
- def _reap_project_orphan_panes(project_root: Path) -> list[str]:
856
- prefix = str((project_root / ".okstra").resolve())
857
- return _kill_panes_by_tag(
858
- lambda tag: tag == prefix or tag.startswith(prefix + os.sep))
546
+ results.append({"projectName": project_name})
547
+ return {"downed": results}
859
548
 
860
549
 
861
550
  def _down_targets(project_root, task_key: str, all_groups: bool) -> list[tuple[str, str, str]]:
@@ -893,66 +582,33 @@ def _read_project_id(project_json: Path) -> str:
893
582
  return ""
894
583
 
895
584
 
896
- def _reap_container_panes(project_root: Path, group: str, task_id: str) -> list[str]:
897
- """이 task 의 container_dir 태그를 가진 pane 만 회수한다(task 스코프)."""
898
- from . import paths, container_registry
899
- cp = paths.container_paths(project_root, group, task_id)
900
- container_dir = str(cp["container_dir"].resolve())
901
- container_registry.clear(project_root, group, task_id)
902
- # tail/watcher pane 은 container_dir 로, analyze pane 은 그 하위 findings 경로로
903
- # 태그된다 — 둘 다 회수하되 prefix 가 task 의 container_dir 라 스코프는 유지된다.
904
- return _kill_panes_by_tag(
905
- lambda tag: tag == container_dir or tag.startswith(container_dir + os.sep))
906
-
907
-
908
- def _kill_panes_by_tag(match: Callable[[str], bool]) -> list[str]:
909
- """`@okstra_container_run` 태그값이 ``match`` 를 통과하는 pane 만 tmux 로 kill.
910
-
911
- 태그값은 해당 task 의 container_dir 절대경로다. 무조건 글로벌 sweep 은 동시
912
- 세션 간섭을 일으키므로 호출자가 항상 스코프 predicate 를 준다(I2)."""
913
- from . import tmux
914
- if not tmux.tmux_available():
915
- return []
916
- result = tmux.run_tmux(
917
- ["list-panes", "-a", "-F", "#{pane_id}\t#{@okstra_container_run}"])
918
- if result.returncode != 0:
919
- return []
920
- killed: list[str] = []
921
- for line in result.stdout.splitlines():
922
- parts = line.split("\t")
923
- if len(parts) == 2 and parts[0] and parts[1] and match(parts[1]):
924
- tmux.kill_pane(parts[0])
925
- killed.append(parts[0])
926
- return killed
927
-
928
-
929
585
  # --------------------------------------------------------------------------- #
930
586
  # argparse 디스패치(recap.py 패턴)
931
587
  # --------------------------------------------------------------------------- #
932
588
 
589
+ _CLI_EPILOG = r"""Usage:
590
+ okstra container up [options]
591
+ okstra container status [options]
592
+ okstra container down [options]
593
+ """
594
+ _CLI_DESCRIPTION = "Manage the okstra container runtime."
595
+
596
+
933
597
  def main(argv: list[str] | None = None) -> int:
934
- parser = argparse.ArgumentParser(prog="okstra container")
598
+ parser = argparse.ArgumentParser(
599
+ description=_CLI_DESCRIPTION,
600
+ epilog=_CLI_EPILOG,
601
+ formatter_class=argparse.RawDescriptionHelpFormatter,
602
+ prog="okstra container")
935
603
  sub = parser.add_subparsers(dest="command", required=True)
936
- for name in ("up", "status", "logs", "stop-watcher", "down"):
604
+ for name in ("up", "status", "down"):
937
605
  sp = sub.add_parser(name)
938
606
  sp.add_argument("--project-root", required=True)
939
607
  sp.add_argument("--task-key", default="")
940
608
  sp.add_argument("--text", action="store_true")
941
609
  if name == "down":
942
610
  sp.add_argument("--all", action="store_true")
943
- if name == "logs":
944
- sp.add_argument("--service", default="")
945
- watch = sub.add_parser("watch") # watcher pane 내부에서 도는 장기 루프
946
- watch.add_argument("--project-name", required=True)
947
- watch.add_argument("--service", required=True)
948
- watch.add_argument("--findings", required=True)
949
- watch.add_argument("--scan-interval", type=float, default=5.0)
950
611
  args = parser.parse_args(argv)
951
- if args.command == "watch":
952
- run_service_watcher(
953
- project_name=args.project_name, service=args.service,
954
- findings_path=args.findings, scan_interval_seconds=args.scan_interval)
955
- return 0
956
612
  result = _dispatch(args)
957
613
  print(
958
614
  render_container_text(args.command, result)
@@ -964,21 +620,16 @@ def main(argv: list[str] | None = None) -> int:
964
620
 
965
621
  def render_container_text(command: str, payload: dict) -> str:
966
622
  """container 명령별 승인된 상태만 고정 순서로 투영한다."""
967
- if command not in {"up", "status", "logs", "stop-watcher", "down"}:
623
+ if command not in {"up", "status", "down"}:
968
624
  raise ValueError("unknown container text purpose")
969
625
  rows = [f"Okstra container {command}\n", line("Status", "ready")]
970
- for label, key in (("Project name", "projectName"), ("Watchers dir", "watchersDir"),
971
- ("Note", "note")):
626
+ for label, key in (("Project name", "projectName"), ("Note", "note")):
972
627
  if key in payload:
973
628
  rows.append(line(label, payload.get(key)))
974
629
  if command == "up":
975
630
  rows.extend(_render_container_up(payload))
976
- elif command in {"status", "logs"}:
977
- rows.extend(_render_container_watchers(payload))
978
- if command == "status":
979
- rows.extend(_render_container_status(payload))
980
- elif command == "stop-watcher":
981
- rows.extend(_render_reaped_panes("Reaped pane", payload.get("reapedPanes")))
631
+ elif command == "status":
632
+ rows.extend(_render_container_status(payload))
982
633
  else:
983
634
  rows.extend(_render_container_down(payload))
984
635
  return "".join(rows)
@@ -990,26 +641,6 @@ def _render_container_up(payload: dict) -> list[str]:
990
641
  rows.append(line(f"Service {index} name", service))
991
642
  for index, warning in enumerate(payload.get("warnings") or [], 1):
992
643
  rows.append(line(f"Warning {index} text", warning))
993
- watch = payload.get("watch") if isinstance(payload.get("watch"), dict) else {}
994
- for label, key in (("Watcher enabled", "enabled"), ("Watcher session", "session"),
995
- ("Watcher note", "note")):
996
- if key in watch:
997
- rows.append(line(label, watch.get(key)))
998
- for index, service in enumerate(watch.get("services") or [], 1):
999
- rows.append(line(f"Watcher service {index} name", service))
1000
- return rows
1001
-
1002
-
1003
- def _render_container_watchers(payload: dict) -> list[str]:
1004
- watchers = payload.get("watchers") if isinstance(payload.get("watchers"), dict) else {}
1005
- rows: list[str] = []
1006
- for index, (service, metadata) in enumerate(sorted(watchers.items()), 1):
1007
- rows.append(line(f"Watcher {index} service", service))
1008
- values = metadata if isinstance(metadata, dict) else {}
1009
- for label, key in (("pane ID", "paneId"), ("session", "sessionName"),
1010
- ("findings path", "findingsPath")):
1011
- if key in values:
1012
- rows.append(line(f"Watcher {index} {label}", values.get(key)))
1013
644
  return rows
1014
645
 
1015
646
 
@@ -1025,21 +656,12 @@ def _render_container_status(payload: dict) -> list[str]:
1025
656
  return rows
1026
657
 
1027
658
 
1028
- def _render_reaped_panes(prefix: str, raw_panes: object) -> list[str]:
1029
- panes = raw_panes if isinstance(raw_panes, list) else []
1030
- return [line(f"{prefix} {index}", pane) for index, pane in enumerate(panes, 1)]
1031
-
1032
-
1033
659
  def _render_container_down(payload: dict) -> list[str]:
1034
660
  downed = payload.get("downed") if isinstance(payload.get("downed"), list) else []
1035
661
  rows: list[str] = []
1036
662
  for index, item in enumerate(downed, 1):
1037
663
  values = item if isinstance(item, dict) else {}
1038
664
  rows.append(line(f"Downed {index} project name", values.get("projectName")))
1039
- rows.extend(_render_reaped_panes(
1040
- f"Downed {index} reaped pane", values.get("reapedPanes")
1041
- ))
1042
- rows.extend(_render_reaped_panes("Orphan pane", payload.get("orphanPanesReaped")))
1043
665
  return rows
1044
666
 
1045
667
 
@@ -1049,11 +671,6 @@ def _dispatch(args) -> dict:
1049
671
  return provision_container_group(project_root=pr, task_key=args.task_key)
1050
672
  if args.command == "status":
1051
673
  return status_container_group(project_root=pr, task_key=args.task_key)
1052
- if args.command == "logs":
1053
- return logs_container_group(
1054
- project_root=pr, task_key=args.task_key, service=args.service)
1055
- if args.command == "stop-watcher":
1056
- return stop_watcher_group(project_root=pr, task_key=args.task_key)
1057
674
  return down_container_group(
1058
675
  project_root=pr, task_key=args.task_key, all_groups=args.all)
1059
676
 
@@ -15,7 +15,7 @@ from typing import Iterable
15
15
 
16
16
  from okstra_ctl.paths import RunRef, runs_dir_of, task_manifest_file
17
17
  from okstra_ctl.task_target import resolve_task_root, project_rel
18
- from okstra_ctl.json_boundary import load_owned_object
18
+ from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
19
19
 
20
20
 
21
21
  INPUT_FILES = (
@@ -85,7 +85,7 @@ def _load_json(path: Path) -> dict:
85
85
  return {}
86
86
  try:
87
87
  data = load_owned_object(path, artifact="context cost input")
88
- except Exception:
88
+ except JsonBoundaryError:
89
89
  return {}
90
90
  return data if isinstance(data, dict) else {}
91
91
 
@@ -481,8 +481,22 @@ def analyze_task_bundle(task_root: Path, project_root: Path) -> dict:
481
481
  }
482
482
 
483
483
 
484
+ _CLI_EPILOG = r"""Usage:
485
+ okstra context-cost <task-root>
486
+ okstra context-cost <task-key> --project-root <dir>
487
+ okstra context-cost <task-key> --cwd <dir>
488
+
489
+ Output: JSON with task file counts, current-run counts, lead Phase 1 read
490
+ surface, analysis-worker initial read surface, and report-writer synthesis
491
+ surface. This is read-only; it never mutates task artifacts.
492
+ """
493
+
494
+
484
495
  def main(argv: list[str] | None = None) -> int:
485
496
  parser = argparse.ArgumentParser(
497
+ prog="okstra context-cost",
498
+ epilog=_CLI_EPILOG,
499
+ formatter_class=argparse.RawDescriptionHelpFormatter,
486
500
  description="Estimate context/file-read cost for an okstra task bundle."
487
501
  )
488
502
  parser.add_argument("target", help="task root path or task-key")