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
@@ -4,7 +4,8 @@ from __future__ import annotations
4
4
  import json
5
5
  import os
6
6
  import tempfile
7
- from collections.abc import Mapping, Sequence
7
+ from collections.abc import Mapping, MutableMapping, Sequence
8
+ from dataclasses import replace
8
9
  from functools import lru_cache
9
10
  from pathlib import Path
10
11
  from typing import Any, Callable
@@ -13,6 +14,7 @@ from .domain.provider import HostModelBinding, ServedModelAttestation
13
14
  from .domain.worker_exec import WorkerWriteCapability
14
15
  from .execution_identity import (
15
16
  Attempt,
17
+ build_dispatch_id,
16
18
  EXECUTION_IDENTITY_VERSION,
17
19
  ExecutionManifest,
18
20
  ExecutionManifestError,
@@ -62,10 +64,50 @@ def read_execution_manifest_view(
62
64
  surface = read_identity_surface(path, surface="run-manifest")
63
65
  if surface.legacy:
64
66
  return synthesize_v1_execution_view(surface.payload), surface.payload
67
+ hoist_legacy_attempt_write_contract(surface.payload)
65
68
  validate_execution_manifest_payload(surface.payload)
66
69
  return _execution_manifest_from_payload(surface.payload), surface.payload
67
70
 
68
71
 
72
+ WRITE_CONTRACT_KEYS = ("writePolicy", "writePolicyDigest", "writeEnforcement")
73
+
74
+
75
+ def hoist_legacy_attempt_write_contract(payload: Any) -> None:
76
+ """쓰기 계약이 invocation 에 실린 매니페스트를 attempt 소유로 옮긴다.
77
+
78
+ 계약은 attempt 마다 다르다(`execution_identity.Attempt` 참고). 그 이전에
79
+ 쓰인 매니페스트는 계약을 invocation 행에 담고 attempt 행에는 안 담으므로,
80
+ 새 검증기가 보면 attempt 가 계약 없는 행이 된다 — 진행 중인 태스크가 전부
81
+ 그 상태다. 여기서 옮겨 두면 다음 쓰기에서 파일 자체가 새 모양이 된다.
82
+
83
+ 이 함수는 옮기기만 하고 값은 만들지 않는다. attempt 가 없는 invocation 의
84
+ 계약은 버린다 — 그 값을 읽는 소비자는 없고, 디스패치가 attempt 를 열 때
85
+ 다시 계산한다. 진행 중인 태스크가 모두 끝나면 통째로 지울 수 있다.
86
+ """
87
+ if not isinstance(payload, MutableMapping):
88
+ return
89
+ invocations = payload.get("invocations")
90
+ attempts = payload.get("attempts")
91
+ if not isinstance(invocations, list):
92
+ return
93
+ carried: dict[str, dict[str, Any]] = {}
94
+ for row in invocations:
95
+ if not isinstance(row, MutableMapping) or "writePolicy" not in row:
96
+ continue
97
+ contract = {key: row.pop(key) for key in WRITE_CONTRACT_KEYS if key in row}
98
+ ref = row.get("invocationRef")
99
+ if isinstance(ref, str):
100
+ carried[ref] = contract
101
+ if not carried or not isinstance(attempts, list):
102
+ return
103
+ for row in attempts:
104
+ if not isinstance(row, MutableMapping) or "writePolicy" in row:
105
+ continue
106
+ contract = carried.get(row.get("invocationRef"))
107
+ if contract:
108
+ row.update(contract)
109
+
110
+
69
111
  def write_execution_manifest(path: Path, manifest: ExecutionManifest) -> None:
70
112
  if manifest.legacy:
71
113
  raise ExecutionManifestError("cannot write a v1 memory view")
@@ -130,7 +172,7 @@ def _drift_detail(existing: Invocation, incoming: Invocation) -> str:
130
172
  before = existing.to_payload()
131
173
  after = incoming.to_payload()
132
174
  changed = sorted(
133
- key for key in (*before, *after) if before.get(key) != after.get(key)
175
+ {key for key in (*before, *after) if before.get(key) != after.get(key)}
134
176
  )
135
177
  return ", ".join(changed) if changed else "(no field differs)"
136
178
 
@@ -349,10 +391,8 @@ def finish_attempt_mutation(
349
391
  if index is None:
350
392
  raise ExecutionManifestError("attempt to finish is unknown")
351
393
  existing = manifest.attempts[index]
352
- closed = Attempt(
353
- invocation_ref=invocation_ref,
354
- attempt=attempt,
355
- started_at=existing.started_at,
394
+ closed = replace(
395
+ existing,
356
396
  finished_at=finished_at,
357
397
  status=status,
358
398
  result_path=result_path,
@@ -422,7 +462,7 @@ def validate_execution_manifest_payload(payload: Mapping[str, Any]) -> None:
422
462
  invocations = _indexed_rows(payload["invocations"], "invocationRef")
423
463
  _validate_roles(payload, participants)
424
464
  _validate_invocations(participants, roles, invocations)
425
- _validate_attempts(payload["attempts"], invocations)
465
+ _validate_attempts(payload["attempts"], invocations, participants, roles)
426
466
  _validate_seals_and_recoveries(payload)
427
467
 
428
468
 
@@ -613,10 +653,9 @@ def _validate_invocations(
613
653
  source = row.get("sourceInvocationRef")
614
654
  if source is not None and source not in invocations:
615
655
  raise ExecutionManifestError("invocation sourceInvocationRef is unknown")
616
- _validate_invocation_write_contract(row, participants[participant_ref], roles[role_ref])
617
656
 
618
657
 
619
- def _validate_invocation_write_contract(
658
+ def _validate_attempt_write_contract(
620
659
  row: Mapping[str, Any],
621
660
  participant: Mapping[str, Any],
622
661
  role: Mapping[str, Any],
@@ -624,21 +663,21 @@ def _validate_invocation_write_contract(
624
663
  policy = row.get("writePolicy")
625
664
  enforcement = row.get("writeEnforcement")
626
665
  if not isinstance(policy, Mapping) or not isinstance(enforcement, Mapping):
627
- raise ExecutionManifestError("invocation write contract must be structured")
666
+ raise ExecutionManifestError("attempt write contract must be structured")
628
667
  try:
629
668
  validate_write_policy_payload(policy)
630
669
  validate_write_enforcement_payload(enforcement)
631
670
  expected_digest = write_policy_digest(policy)
632
671
  except WritePolicyError as exc:
633
- raise ExecutionManifestError(f"invocation write contract: {exc}") from exc
672
+ raise ExecutionManifestError(f"attempt write contract: {exc}") from exc
634
673
  if row.get("writePolicyDigest") != expected_digest:
635
- raise ExecutionManifestError("invocation writePolicyDigest does not match")
674
+ raise ExecutionManifestError("attempt writePolicyDigest does not match")
636
675
  source = policy.get("sourcePolicy")
637
676
  expected_mode = (
638
677
  "project-mutation" if role.get("role") == "implementer" else "source-readonly"
639
678
  )
640
679
  if not isinstance(source, Mapping) or source.get("mode") != expected_mode:
641
- raise ExecutionManifestError("invocation sourcePolicy does not match role")
680
+ raise ExecutionManifestError("attempt sourcePolicy does not match role")
642
681
  git_policy = policy.get("gitPolicy")
643
682
  expected_git_mode = (
644
683
  "fast-forward-descendant-chain"
@@ -649,10 +688,10 @@ def _validate_invocation_write_contract(
649
688
  not isinstance(git_policy, Mapping)
650
689
  or git_policy.get("mode") != expected_git_mode
651
690
  ):
652
- raise ExecutionManifestError("invocation gitPolicy does not match role")
691
+ raise ExecutionManifestError("attempt gitPolicy does not match role")
653
692
  if expected_mode == "source-readonly" and source.get("plannedPaths"):
654
693
  raise ExecutionManifestError(
655
- "source-readonly invocation cannot declare planned paths"
694
+ "source-readonly attempt cannot declare planned paths"
656
695
  )
657
696
  _validate_enforcement_capability(enforcement, participant, role)
658
697
 
@@ -674,7 +713,7 @@ def _validate_enforcement_capability(
674
713
  )
675
714
  return
676
715
  if not isinstance(capability, Mapping):
677
- raise ExecutionManifestError("worker invocation has no write capability")
716
+ raise ExecutionManifestError("worker attempt has no write capability")
678
717
  rank = {"none": 0, "directory-boundary": 1, "exact-path": 2}
679
718
  maximum = capability.get("maxBoundaryPrecision")
680
719
  actual = enforcement.get("boundaryPrecision")
@@ -685,6 +724,8 @@ def _validate_enforcement_capability(
685
724
  def _validate_attempts(
686
725
  rows: object,
687
726
  invocations: Mapping[str, Mapping[str, Any]],
727
+ participants: Mapping[str, Mapping[str, Any]],
728
+ roles: Mapping[str, Mapping[str, Any]],
688
729
  ) -> None:
689
730
  if not isinstance(rows, list):
690
731
  raise ExecutionManifestError("execution manifest attempts must be an array")
@@ -698,6 +739,12 @@ def _validate_attempts(
698
739
  attempt = row.get("attempt")
699
740
  if not isinstance(attempt, int) or attempt < 1:
700
741
  raise ExecutionManifestError("attempt number must be positive")
742
+ invocation = invocations[invocation_ref]
743
+ participant = participants.get(str(invocation.get("participantRef")))
744
+ role = roles.get(str(invocation.get("roleExecutionRef")))
745
+ if participant is None or role is None:
746
+ raise ExecutionManifestError("attempt references unknown execution identity")
747
+ _validate_attempt_write_contract(row, participant, role)
701
748
  attempts_by_invocation.setdefault(invocation_ref, []).append(attempt)
702
749
  for invocation_ref, values in attempts_by_invocation.items():
703
750
  if sorted(values) != list(range(1, len(values) + 1)):
@@ -710,7 +757,6 @@ def _validate_seals_and_recoveries(payload: Mapping[str, Any]) -> None:
710
757
  attempts = payload.get("attempts")
711
758
  if not isinstance(attempts, list):
712
759
  return
713
- from .dispatch_state import build_dispatch_id
714
760
 
715
761
  attempt_refs = {
716
762
  build_dispatch_id(str(row.get("invocationRef")), int(row.get("attempt") or 0))
@@ -925,9 +971,6 @@ def _invocation_from_payload(row: Mapping[str, Any]) -> Invocation:
925
971
  dispatch_kind=str(row["dispatchKind"]),
926
972
  round=int(row["round"]),
927
973
  input_digest=str(row["inputDigest"]),
928
- write_policy=dict(row["writePolicy"]),
929
- write_policy_digest=str(row["writePolicyDigest"]),
930
- write_enforcement=dict(row["writeEnforcement"]),
931
974
  )
932
975
 
933
976
 
@@ -942,6 +985,9 @@ def _attempt_from_payload(row: Mapping[str, Any]) -> Attempt:
942
985
  error_path=_optional_text(row.get("errorPath")),
943
986
  change_summary=dict(row["changeSummary"]),
944
987
  evidence_seal_ref=_optional_text(row.get("evidenceSealRef")),
988
+ write_policy=dict(row["writePolicy"]),
989
+ write_policy_digest=str(row["writePolicyDigest"]),
990
+ write_enforcement=dict(row["writeEnforcement"]),
945
991
  )
946
992
 
947
993
 
@@ -1223,6 +1269,7 @@ def _role_execution_index_from_runs(
1223
1269
  surface = read_identity_surface(resolved_path, surface="run-manifest")
1224
1270
  if surface.legacy:
1225
1271
  continue
1272
+ hoist_legacy_attempt_write_contract(surface.payload)
1226
1273
  validate_execution_manifest_payload(surface.payload)
1227
1274
  run_ref = resolved_path.relative_to(resolved_reference_root).as_posix()
1228
1275
  rows.extend(
@@ -83,6 +83,10 @@ class MutationSnapshot:
83
83
  class MutationAuditResult:
84
84
  status: str
85
85
  changed_paths: tuple[str, ...]
86
+ # 추적되지 않는 채로 생기거나 바뀐 경로. 소스 변경이 아니라 명령이 남긴
87
+ # 산출물이라 판정을 막지 않는다 — 그래도 기록은 남긴다. 없던 일로 하면
88
+ # 워커가 워크트리에 무엇을 남겼는지 아무도 모른다.
89
+ untracked_paths: tuple[str, ...]
86
90
  source_changed: bool
87
91
  git_changed: bool
88
92
  attribution: str
@@ -96,6 +100,7 @@ class MutationAuditResult:
96
100
  return {
97
101
  "mutationStatus": self.status,
98
102
  "changedPaths": list(self.changed_paths),
103
+ "untrackedPaths": list(self.untracked_paths),
99
104
  "sourceChanged": self.source_changed,
100
105
  "gitChanged": self.git_changed,
101
106
  "attribution": self.attribution,
@@ -118,11 +123,13 @@ class ExecutionMutationAudit:
118
123
  rows = tuple(policies)
119
124
  root, artifact_root = _validate_batch(rows)
120
125
  generated = _generated_paths(rows)
121
- file_digests = _content_snapshot(root, generated)
126
+ file_digests = _content_snapshot(root, generated | _ignored_paths(root))
122
127
  artifact_digests = (
123
128
  file_digests
124
129
  if artifact_root == root
125
- else _content_snapshot(artifact_root, generated)
130
+ else _content_snapshot(
131
+ artifact_root, generated | _ignored_paths(artifact_root)
132
+ )
126
133
  )
127
134
  scratch_digests = _scratch_snapshot(rows)
128
135
  git_projection = _git_projection(root)
@@ -174,7 +181,9 @@ class ExecutionMutationAudit:
174
181
  scratch_changed = _changed_keys(
175
182
  before.scratch_digests, after.scratch_digests
176
183
  )
177
- source_changes = _source_changes(changed, rows, before)
184
+ source_changes, untracked_changes = _split_tracked(
185
+ before.root, _source_changes(changed, rows, before)
186
+ )
178
187
  # Reported and, through `_retry_allowed`, load-bearing: a stat-cache
179
188
  # refresh must not read as "this worker touched Git" and must not
180
189
  # withhold the retry a worker that failed for another reason is owed.
@@ -203,6 +212,7 @@ class ExecutionMutationAudit:
203
212
  return MutationAuditResult(
204
213
  status=status,
205
214
  changed_paths=tuple(sorted(source_changes)),
215
+ untracked_paths=tuple(sorted(untracked_changes)),
206
216
  source_changed=bool(source_changes),
207
217
  git_changed=git_changed,
208
218
  attribution="batch-unattributed",
@@ -273,7 +283,9 @@ when a run produces the same evidence for it, not before.
273
283
  """
274
284
 
275
285
 
276
- def _content_snapshot(root: Path, generated: frozenset[str]) -> dict[str, str]:
286
+ def _content_snapshot(root: Path, excluded: frozenset[str]) -> dict[str, str]:
287
+ """감사 대상 트리의 내용 다이제스트. `excluded` 는 정책이 선언한 생성
288
+ 경로와 레포가 무시하는 경로를 합친 접두사 집합이다."""
277
289
  rows: dict[str, str] = {}
278
290
  for current, directories, filenames in os.walk(root, followlinks=False):
279
291
  current_path = Path(current)
@@ -284,7 +296,7 @@ def _content_snapshot(root: Path, generated: frozenset[str]) -> dict[str, str]:
284
296
  if (
285
297
  name == ".git"
286
298
  or name in _INSTALLED_DEPENDENCY_DIRS
287
- or _excluded(relative_path, generated)
299
+ or _excluded(relative_path, excluded)
288
300
  ):
289
301
  continue
290
302
  if path.is_symlink():
@@ -295,7 +307,7 @@ def _content_snapshot(root: Path, generated: frozenset[str]) -> dict[str, str]:
295
307
  for name in sorted(filenames):
296
308
  path = current_path / name
297
309
  relative = path.relative_to(root).as_posix()
298
- if relative == ".git" or _excluded(Path(relative), generated):
310
+ if relative == ".git" or _excluded(Path(relative), excluded):
299
311
  continue
300
312
  rows[relative] = _path_digest(path)
301
313
  return rows
@@ -327,9 +339,79 @@ def _path_digest(path: Path) -> str:
327
339
  return "sha256:" + hashlib.sha256(value).hexdigest()
328
340
 
329
341
 
330
- def _excluded(path: Path, generated: frozenset[str]) -> bool:
342
+ def _excluded(path: Path, excluded: frozenset[str]) -> bool:
331
343
  relative = path.as_posix()
332
- return any(relative == item or relative.startswith(item + "/") for item in generated)
344
+ return any(relative == item or relative.startswith(item + "/") for item in excluded)
345
+
346
+
347
+ def _ignored_paths(root: Path) -> frozenset[str]:
348
+ """레포가 스스로 소스가 아니라고 선언한 경로.
349
+
350
+ 감사는 워커가 *소스* 를 바꿨는지를 묻는다. `.gitignore` 는 그 레포에서
351
+ 무엇이 소스가 아닌지에 대한 사람이 쓴 답이므로, 그 답을 여기서 다시 쓴다 —
352
+ okstra 가 파일 이름 목록을 따로 들고 있으면 레포마다 틀린다.
353
+
354
+ 이 면제가 필요한 이유는 감사 창이 사람·OS 와 공유되기 때문이다. 실측
355
+ (2026-08-26, `fontsninja-nlpvibe` planning run 019~022): 완주한 워커의 결과가
356
+ 버려진 10건 중 2건의 유발자가 Finder 가 쓴 `.DS_Store` 와 IDE 가 쓴
357
+ `.idea/workspace.xml` 이었다. 어느 쪽도 워커가 쓴 것이 아니고, 둘 다 그
358
+ 레포의 무시 대상이다.
359
+
360
+ 추적 중인 파일은 git 이 무시 대상으로 세지 않으므로 그대로 감사한다 —
361
+ 나머지 8건처럼 사람이 추적 소스를 동시에 고친 경우는 이 면제가 가리지
362
+ 않는다. git 이 없거나 레포가 아니면 빈 집합을 돌려 종전대로 전부 감사한다.
363
+ """
364
+ result = subprocess.run(
365
+ ["git", "-C", str(root), "ls-files", "-z", "--others", "--ignored",
366
+ "--exclude-standard", "--directory"],
367
+ capture_output=True,
368
+ text=True,
369
+ check=False,
370
+ )
371
+ if result.returncode != 0:
372
+ return frozenset()
373
+ # `--directory` 는 통째로 무시되는 디렉터리를 `name/` 한 줄로 접는다.
374
+ # `_excluded` 는 접두사로 대조하므로 그 꼬리 슬래시를 떼어 준다.
375
+ return frozenset(
376
+ entry.rstrip("/") for entry in result.stdout.split("\0") if entry
377
+ )
378
+
379
+
380
+ def _tracked_paths(root: Path) -> frozenset[str] | None:
381
+ """이 레포가 추적 중인 파일. git 이 없거나 레포가 아니면 ``None``.
382
+
383
+ `source-readonly` 가 묻는 것은 워커가 **소스** 를 바꿨는가이다. 추적 중인
384
+ 파일의 변경이 그 답이다. 추적되지 않는 새 파일은 소스의 변경이 아니라
385
+ 명령이 남긴 산출물이고, 그 둘을 한 등급으로 묶으면 규칙을 만족시킬 수 있는
386
+ 실물 조건이 없어진다 — 프로필은 검증자에게 승인 계획의 Tier 1 명령을 독립
387
+ 재실행하라고 요구하는데, 실제 프로젝트의 그 명령들은 빌드 산출물과 평가
388
+ 리포트를 쓴다. 실측(2026-08-26, `fontsninja-nlpvibe` final-verification
389
+ run 005): 계획대로 게이트를 돌린 검증자 셋이 전부
390
+ `readonly source changed [eval/reports/clip-baseline.{html,json}]` 로
391
+ 버려졌다. 두 파일은 추적되지 않는 생성 산출물이고, 계획이 승인 시점에
392
+ 적어 둔 명령이 만든 것이다.
393
+
394
+ ``None`` 은 "판정할 수 없음" 이다 — 그때는 좁히지 않고 종전대로 전부
395
+ 소스 변경으로 본다. 추적 여부를 모르면서 면제하면 진짜 소스 변경을 놓친다.
396
+ """
397
+ result = subprocess.run(
398
+ ["git", "-C", str(root), "ls-files", "-z"],
399
+ capture_output=True,
400
+ text=True,
401
+ check=False,
402
+ )
403
+ if result.returncode != 0:
404
+ return None
405
+ return frozenset(entry for entry in result.stdout.split("\0") if entry)
406
+
407
+
408
+ def _split_tracked(root: Path, changes: set[str]) -> tuple[set[str], set[str]]:
409
+ """``(추적 중인 소스 변경, 추적되지 않는 산출물)``."""
410
+ tracked = _tracked_paths(root)
411
+ if tracked is None:
412
+ return set(changes), set()
413
+ untracked = {path for path in changes if path not in tracked}
414
+ return changes - untracked, untracked
333
415
 
334
416
 
335
417
  def _generated_paths(policies: Sequence[WritePolicy]) -> frozenset[str]:
@@ -119,3 +119,20 @@ def translation_sidecar_path(data_path: Path, lang: str) -> Path:
119
119
  overwrite it.
120
120
  """
121
121
  return data_path.with_name(f"{_stem(data_path)}.i18n.{lang}.json")
122
+
123
+
124
+ def sidecar_source_rel(path: Path) -> str:
125
+ """사이드카 프론트매터가 `source-report` / `source-data` 에 적는 경로.
126
+
127
+ 기준점은 **태스크 루트**다 (`runs/<task-type>/reports/...`). 사이드카를
128
+ 읽는 쪽은 리포트를 태스크 루트에서 되짚으므로
129
+ (`implementation_direction._relative_source`), 다른 기준으로 적힌 값은
130
+ 같은 파일을 가리켜도 대조에 실패한다.
131
+
132
+ HTML 열람본을 렌더할 때 넘기는 `--source-report` 와 CLI 가 직접 쓰는
133
+ 사이드카가 이 함수 하나를 공유한다. 둘이 각자 계산하던 동안 HTML Export 는
134
+ 프로젝트 루트 기준을 적었고, 그 사이드카는 방향 선택 게이트를 통과할 수
135
+ 없었다.
136
+ """
137
+ indices = [index for index, part in enumerate(path.parts) if part == "runs"]
138
+ return Path(*path.parts[indices[-1]:]).as_posix() if indices else path.name
@@ -4,7 +4,8 @@ This is a deliberately narrow JSON Schema implementation that supports
4
4
  exactly the keywords used by ``schemas/final-report-v2.0.schema.json``:
5
5
 
6
6
  type, required, properties, additionalProperties, enum, const, pattern,
7
- minLength, minItems, maxItems, uniqueItems, items, minimum, maximum, $ref ($defs),
7
+ minLength, minItems, maxItems, uniqueItems, prefixItems, items, minimum, maximum,
8
+ boolean schemas, $ref ($defs),
8
9
  oneOf, allOf, if/then/else, contains, minContains, maxContains, not
9
10
 
10
11
  We do NOT depend on the ``jsonschema`` PyPI package because its
@@ -102,12 +103,32 @@ def _resolve_ref(ref: str, root: dict) -> dict:
102
103
  return node
103
104
 
104
105
 
106
+ _BRANCH_ROOT_PREFIX = f"{_format_path(())}: "
107
+ """분기 probe 의 경로는 분기 루트 기준이라 `<root>:` 가 매 줄 반복된다.
108
+ 바깥 오류가 이미 실제 경로를 싣고 있으므로 안쪽에서는 뗀다."""
109
+
110
+ _BRANCH_ERROR_LIMIT = 8
111
+ """분기 하나당 실을 오류 수. 나머지는 개수만 적는다 — 열세 개 짜리 목록도
112
+ 앞의 여덟이면 어느 블록을 안 썼는지 판정된다."""
113
+
114
+
105
115
  class _Validator:
106
116
  def __init__(self, root_schema: dict):
107
117
  self.root = root_schema
108
118
  self.errors: list[str] = []
109
119
 
110
- def validate(self, instance: Any, schema: dict, path: tuple[str | int, ...]) -> None:
120
+ def validate(
121
+ self,
122
+ instance: Any,
123
+ schema: dict | bool,
124
+ path: tuple[str | int, ...],
125
+ ) -> None:
126
+ if schema is True:
127
+ return
128
+ if schema is False:
129
+ self._err(path, "value is not allowed by the false schema")
130
+ return
131
+
111
132
  # Handle $ref first — everything else is composed under the
112
133
  # resolved schema.
113
134
  if "$ref" in schema:
@@ -196,9 +217,14 @@ class _Validator:
196
217
  for previous in instance[:index]
197
218
  ):
198
219
  self._err(path, "array items are not unique")
220
+ prefix_items = schema.get("prefixItems") or []
221
+ for i, item_schema in enumerate(prefix_items[: len(instance)]):
222
+ self.validate(instance[i], item_schema, path + (i,))
223
+
199
224
  items = schema.get("items")
200
225
  if items is not None:
201
- for i, value in enumerate(instance):
226
+ remaining_items = instance[len(prefix_items) :]
227
+ for i, value in enumerate(remaining_items, start=len(prefix_items)):
202
228
  self.validate(value, items, path + (i,))
203
229
  contains = schema.get("contains")
204
230
  if contains is not None:
@@ -229,30 +255,78 @@ class _Validator:
229
255
  if else_schema is not None:
230
256
  self.validate(instance, else_schema, path)
231
257
 
232
- def _validate_one_of(self, instance: Any, branches: list[dict], path: tuple[str | int, ...]) -> None:
233
- matches = sum(1 for b in branches if self._matches(instance, b))
234
- if matches != 1:
258
+ def _validate_one_of(
259
+ self,
260
+ instance: Any,
261
+ branches: list[dict | bool],
262
+ path: tuple[str | int, ...],
263
+ ) -> None:
264
+ per_branch = [self._branch_errors(instance, branch) for branch in branches]
265
+ matches = sum(1 for errors in per_branch if not errors)
266
+ if matches == 1:
267
+ return
268
+ if matches == 0:
235
269
  self._err(
236
270
  path,
237
- f"oneOf matched {matches} branches (expected exactly 1); "
238
- f"branches: {[self._summarise(b) for b in branches]}",
271
+ "oneOf matched no branch (expected exactly 1); "
272
+ + self._branch_diagnosis(per_branch),
239
273
  )
274
+ return
275
+ self._err(
276
+ path,
277
+ f"oneOf matched {matches} branches (expected exactly 1); "
278
+ f"branches: {[self._summarise(b) for b in branches]}",
279
+ )
280
+
281
+ @staticmethod
282
+ def _branch_diagnosis(per_branch: list[list[str]]) -> str:
283
+ """왜 각 분기가 안 맞았는지. 분기 스키마를 인라인하지 않는다.
240
284
 
241
- def _validate_not(self, instance: Any, sub_schema: dict, path: tuple[str | int, ...]) -> None:
285
+ 종전에는 `oneOf` 실패가 분기 스키마 개를 통째로 찍었다. 1,500자
286
+ 한 줄이면서 **어느 필드가 빠졌는지는 말하지 않아서**, 읽는 사람이
287
+ narrative 의 키 목록을 뽑아 스키마의 `required` 와 손으로 대조해야
288
+ 했다. 분기가 실제로 낸 오류가 그 대조의 답이므로 그것을 싣는다.
289
+ """
290
+ parts: list[str] = []
291
+ for index, errors in enumerate(per_branch, start=1):
292
+ shown = [
293
+ error[len(_BRANCH_ROOT_PREFIX):]
294
+ if error.startswith(_BRANCH_ROOT_PREFIX)
295
+ else error
296
+ for error in errors[:_BRANCH_ERROR_LIMIT]
297
+ ]
298
+ remainder = len(errors) - len(shown)
299
+ if remainder > 0:
300
+ shown.append(f"(+{remainder} more)")
301
+ parts.append(f"branch {index}: " + "; ".join(shown))
302
+ return " | ".join(parts)
303
+
304
+ def _branch_errors(self, instance: Any, schema: dict | bool) -> list[str]:
305
+ probe = _Validator(self.root)
306
+ probe.validate(instance, schema, ())
307
+ return probe.errors
308
+
309
+ def _validate_not(
310
+ self,
311
+ instance: Any,
312
+ sub_schema: dict | bool,
313
+ path: tuple[str | int, ...],
314
+ ) -> None:
242
315
  if self._matches(instance, sub_schema):
243
316
  self._err(path, f"value must NOT match {self._summarise(sub_schema)}")
244
317
 
245
- def _matches(self, instance: Any, schema: dict) -> bool:
246
- """Cheap 'does this validate' probe used by if/oneOf/contains.
318
+ def _matches(self, instance: Any, schema: dict | bool) -> bool:
319
+ """Cheap 'does this validate' probe used by if/contains.
247
320
  Returns True iff the sub-schema produces zero errors. Does not
248
- mutate ``self.errors``.
321
+ mutate ``self.errors``. `oneOf` keeps the errors themselves
322
+ (`_branch_errors`) because it has to say why every branch failed.
249
323
  """
250
- probe = _Validator(self.root)
251
- probe.validate(instance, schema, ())
252
- return not probe.errors
324
+ return not self._branch_errors(instance, schema)
253
325
 
254
326
  @staticmethod
255
- def _summarise(schema: dict) -> str:
327
+ def _summarise(schema: dict | bool) -> str:
328
+ if isinstance(schema, bool):
329
+ return str(schema).lower()
256
330
  keys = sorted(k for k in schema if k not in ("description", "$comment"))
257
331
  return "{" + ", ".join(f"{k}={schema[k]!r}" for k in keys[:3]) + "}"
258
332
 
@@ -273,12 +273,32 @@ def _items_as_json(items: List[StaleItem]) -> list:
273
273
  "suggested_commit": i.suggested_commit} for i in items]
274
274
 
275
275
 
276
+ _CLI_EPILOG = r"""기본은 검사:
277
+ stale 항목을 JSON 으로 출력하고, confirm 항목이 남으면 exit 2.
278
+ patch-id 로 내용 동일성이 증명되는 항목(auto)은 --apply 로 일괄 보정되고,
279
+ 내용이 바뀐 항목(confirm)은 --stage/--use-ref 로만 보정된다.
280
+
281
+ Usage:
282
+ okstra git-reconcile --plan-run-root <dir> --project-id <id> \
283
+ --task-group <g> --task-id <t> --work-category <c> \
284
+ [--apply] [--stage <N> --use-ref <ref>] [--reset-anchor <ref>] [--json]
285
+
286
+ Exit codes:
287
+ 0 stale 없음 또는 보정 완료
288
+ 2 confirm 항목 잔존 (check: 확인 필요 / apply: 일부 미보정)
289
+ 1 error (resolve 실패 등)
290
+ """
291
+ _CLI_DESCRIPTION = "Okstra \ubc16 git \ud788\uc2a4\ud1a0\ub9ac \ubcc0\uacbd \ud6c4 \uae30\ub85d \ud654\ud574."
292
+
293
+
276
294
  def _parse_args(argv: Optional[list]):
277
295
  import argparse
278
296
 
279
297
  p = argparse.ArgumentParser(
280
- prog="okstra git-reconcile",
281
- epilog="exit codes: 0 = clean/applied, 2 = confirm items remain, 1 = error")
298
+ description=_CLI_DESCRIPTION,
299
+ epilog=_CLI_EPILOG,
300
+ formatter_class=argparse.RawDescriptionHelpFormatter,
301
+ prog="okstra git-reconcile")
282
302
  p.add_argument("--project-root", default=".")
283
303
  p.add_argument("--plan-run-root", required=True,
284
304
  help="consumers.jsonl 이 있는 runs/implementation-planning/ 경로")
@@ -422,10 +422,33 @@ def _parse_stages_csv(raw: str) -> List[int]:
422
422
  return out
423
423
 
424
424
 
425
+ _CLI_EPILOG = r"""JSON 출력.
426
+
427
+ Usage:
428
+ okstra handoff eligible --plan-run-root <dir> --approved-plan <md>
429
+ okstra handoff assemble --plan-run-root <dir> --approved-plan <md> \
430
+ --project-root <dir> --project-id <id> --task-group <g> --task-id <t> \
431
+ --work-category <c> --stages 2,3 --base <branch>
432
+ okstra handoff record-verified --plan-run-root <dir> --stage <N> \
433
+ --report-path <md> --data-json <json>
434
+ okstra handoff record-pr --plan-run-root <dir> --stages 2,3 \
435
+ --branch <b> --url <u>
436
+ okstra handoff local-checkout --project-root <dir> --project-id <id> \
437
+ --task-group <g> --task-id <t> [--stage <N>]
438
+
439
+ Exit codes: 0 ok / 1 자격·전제 위반 / 2 stage 간 merge 충돌(conflicts 동봉)
440
+ """
441
+ _CLI_DESCRIPTION = "Release-handoff stage-group \ubcf4\uc870 (\uc790\uaca9/\uc218\uc9d1/\uae30\ub85d)."
442
+
443
+
425
444
  def main(argv: Optional[list] = None) -> int:
426
445
  import argparse
427
446
 
428
- p = argparse.ArgumentParser(prog="okstra handoff")
447
+ p = argparse.ArgumentParser(
448
+ description=_CLI_DESCRIPTION,
449
+ epilog=_CLI_EPILOG,
450
+ formatter_class=argparse.RawDescriptionHelpFormatter,
451
+ prog="okstra handoff")
429
452
  sub = p.add_subparsers(dest="cmd", required=True)
430
453
 
431
454
  def common(sp, *, plan=True):
@@ -5,7 +5,9 @@ import hashlib as _hashlib
5
5
  import re as _re
6
6
  from typing import Dict
7
7
 
8
- from okstra_project.state import slugify as _slugify
8
+ # leaf 규칙 모듈에서 바로 가져온다. state 거치면 okstra_ctl 패키지 __init__ 이
9
+ # 상태 접근자 전체를 끌어와 okstra_project ↔ okstra_ctl 순환이 된다.
10
+ from okstra_project.slug import slugify as _slugify
9
11
 
10
12
  # docker compose project name 길이 상한. compose 는 [a-z0-9][a-z0-9_-]* 만 허용한다.
11
13
  CONTAINER_PROJECT_NAME_MAX = 60
@@ -73,28 +75,16 @@ def _escape_segment_for_join(segment: str) -> str:
73
75
  return segment.replace("-", "--")
74
76
 
75
77
 
76
- def run_id_to_session_name(run_id: str) -> str:
77
- """tmux 세션명으로 변환한다. 'okstra-' 접두사 + 세그먼트 escape 후 `-` 조인.
78
- 예: 'a/b-c/r01' → 'okstra-a-b--c-r01'. 세그먼트 내부 `-` 는 `--` 로
79
- 이스케이프되어, 슬래시 위치가 다른 두 runId 가 동일 세션명으로
80
- 충돌하지 않는다. 세그먼트는 fs-safe 슬러그로 강제 정규화해
81
- `..` 같은 traversal 토큰이 세션명에 그대로 노출되지 않게 한다.
82
- """
83
- parts = [_escape_segment_for_join(_safe_fs_segment(seg))
84
- for seg in run_id.split("/")]
85
- return "okstra-" + "-".join(parts)
86
-
87
-
88
78
  def slugify_task_segment(value: str) -> str:
89
79
  """Match okstra.sh slugify_value for task directory segments."""
90
80
  return _re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
91
81
 
92
82
 
93
- def build_container_session_name(project_id: str, group: str, task_id: str) -> str:
94
- """container detached tmux 세션명을 합친다 → 'okstra-container-<slug>'.
83
+ def build_container_run_trace(project_id: str, group: str, task_id: str) -> str:
84
+ """container 배포의 `okstra.run-trace` 라벨값을 합친다 → 'okstra-container-<slug>'.
95
85
 
96
86
  세그먼트는 task_dir 가 쓰는 slugify_task_segment 와 동일 규칙으로 정규화해
97
- 세션명 slug 가 프로젝트 디스크 경로 slug 와 분기하지 않게 한다.
87
+ 라벨 slug 가 프로젝트 디스크 경로 slug 와 분기하지 않게 한다.
98
88
  """
99
89
  seg = slugify_task_segment
100
90
  return f"okstra-container-{seg(project_id)}-{seg(group)}-{seg(task_id)}"