okstra 0.201.3 → 0.204.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 (273) hide show
  1. package/README.md +3 -3
  2. package/dist/cli-registry.mjs +7 -7
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/lifecycle/install.mjs +50 -124
  5. package/dist/commands/lifecycle/install.mjs.map +1 -1
  6. package/dist/commands/lifecycle/setup.mjs +15 -0
  7. package/dist/commands/lifecycle/setup.mjs.map +1 -1
  8. package/dist/commands/memory/memory.mjs +41 -8
  9. package/dist/commands/memory/memory.mjs.map +1 -1
  10. package/dist/lib/citation-guidance.d.mts +21 -0
  11. package/dist/lib/citation-guidance.mjs +79 -0
  12. package/dist/lib/citation-guidance.mjs.map +1 -0
  13. package/dist/lib/install-assets.mjs +3 -0
  14. package/dist/lib/install-assets.mjs.map +1 -1
  15. package/dist/lib/runtime-manifest.mjs +2 -1
  16. package/dist/lib/runtime-manifest.mjs.map +1 -1
  17. package/dist/lib/types.d.mts +2 -1
  18. package/docs/architecture/storage-model.md +17 -10
  19. package/docs/architecture.md +26 -20
  20. package/docs/cli.md +16 -13
  21. package/docs/contributor-change-matrix.md +3 -2
  22. package/docs/performance-improvement-plan-v2.md +2 -3
  23. package/docs/project-structure-overview.md +38 -9
  24. package/docs/task-process/README.md +1 -1
  25. package/docs/task-process/common-flow.md +1 -1
  26. package/docs/task-process/final-verification.md +3 -1
  27. package/docs/task-process/implementation.md +1 -1
  28. package/docs/task-process/release-handoff.md +36 -39
  29. package/package.json +1 -2
  30. package/runtime/BUILD.json +2 -2
  31. package/runtime/agents/common.json +28 -0
  32. package/runtime/agents/operations/code-review.json +6 -0
  33. package/runtime/agents/operations/report-translation.json +6 -0
  34. package/runtime/agents/operations/schedule-verification.json +6 -0
  35. package/runtime/agents/roles/analyser.json +18 -0
  36. package/runtime/agents/roles/critic.json +18 -0
  37. package/runtime/agents/roles/designer.json +18 -0
  38. package/runtime/agents/roles/implementer.json +20 -0
  39. package/runtime/agents/roles/leader.json +20 -0
  40. package/runtime/agents/roles/planner.json +18 -0
  41. package/runtime/agents/roles/report-writer.json +19 -0
  42. package/runtime/agents/roles/translator.json +19 -0
  43. package/runtime/agents/roles/verifier.json +18 -0
  44. package/runtime/bin/lib/okstra/usage.sh +5 -5
  45. package/runtime/prompts/duties/acceptance-critic.json +32 -0
  46. package/runtime/prompts/duties/acceptance-verifier.json +32 -0
  47. package/runtime/prompts/duties/analysis-worker.json +32 -0
  48. package/runtime/prompts/duties/code-reviewer.json +32 -0
  49. package/runtime/prompts/duties/diagnosis-worker.json +32 -0
  50. package/runtime/prompts/duties/direction-selection-worker.json +32 -0
  51. package/runtime/prompts/duties/discovery-worker.json +32 -0
  52. package/runtime/prompts/duties/implementation-executor.json +32 -0
  53. package/runtime/prompts/duties/implementation-verifier.json +32 -0
  54. package/runtime/prompts/duties/lead.json +32 -0
  55. package/runtime/prompts/duties/planning-worker.json +36 -0
  56. package/runtime/prompts/duties/report-writer.json +32 -0
  57. package/runtime/prompts/duties/reverification-worker.json +32 -0
  58. package/runtime/prompts/duties/schedule-verifier.json +32 -0
  59. package/runtime/prompts/duties/scope-critic.json +32 -0
  60. package/runtime/prompts/duties/technical-verification-worker.json +32 -0
  61. package/runtime/prompts/duties/translator.json +32 -0
  62. package/runtime/prompts/launch.template.md +3 -2
  63. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  64. package/runtime/prompts/lead/convergence.md +4 -4
  65. package/runtime/prompts/lead/okstra-lead-contract.md +115 -6
  66. package/runtime/prompts/lead/plan-body-verification.md +6 -6
  67. package/runtime/prompts/lead/report-writer.md +3 -3
  68. package/runtime/prompts/profiles/_common-contract.md +2 -2
  69. package/runtime/prompts/profiles/_implementation-executor.md +4 -1
  70. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  71. package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
  72. package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
  73. package/runtime/prompts/profiles/error-analysis.json +39 -0
  74. package/runtime/prompts/profiles/error-analysis.md +0 -25
  75. package/runtime/prompts/profiles/feature-analysis.json +31 -0
  76. package/runtime/prompts/profiles/feature-analysis.md +0 -20
  77. package/runtime/prompts/profiles/final-verification.json +30 -0
  78. package/runtime/prompts/profiles/final-verification.md +3 -22
  79. package/runtime/prompts/profiles/forbidden-actions.json +4 -3
  80. package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
  81. package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
  82. package/runtime/prompts/profiles/implementation-planning.json +40 -0
  83. package/runtime/prompts/profiles/implementation-planning.md +6 -29
  84. package/runtime/prompts/profiles/implementation.json +30 -0
  85. package/runtime/prompts/profiles/implementation.md +1 -20
  86. package/runtime/prompts/profiles/improvement-discovery.json +31 -0
  87. package/runtime/prompts/profiles/improvement-discovery.md +0 -20
  88. package/runtime/prompts/profiles/project-analysis.json +31 -0
  89. package/runtime/prompts/profiles/project-analysis.md +0 -20
  90. package/runtime/prompts/profiles/release-handoff.json +5 -0
  91. package/runtime/prompts/profiles/release-handoff.md +71 -73
  92. package/runtime/prompts/profiles/requirements-discovery.json +39 -0
  93. package/runtime/prompts/profiles/requirements-discovery.md +0 -25
  94. package/runtime/prompts/profiles/technical-verification.json +39 -0
  95. package/runtime/prompts/profiles/technical-verification.md +0 -25
  96. package/runtime/prompts/wizard/prompts.ko.json +12 -17
  97. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
  98. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
  99. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
  100. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
  101. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
  102. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
  103. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
  104. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
  105. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
  106. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
  107. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
  108. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
  109. package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
  110. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
  111. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
  112. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
  113. package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
  114. package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
  115. package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
  116. package/runtime/python/okstra_ctl/cmux.py +69 -20
  117. package/runtime/python/okstra_ctl/code_review_target.py +16 -8
  118. package/runtime/python/okstra_ctl/conformance.py +43 -0
  119. package/runtime/python/okstra_ctl/consumers.py +6 -3
  120. package/runtime/python/okstra_ctl/container.py +31 -8
  121. package/runtime/python/okstra_ctl/context_cost.py +11 -15
  122. package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
  123. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +4 -6
  124. package/runtime/python/okstra_ctl/convergence_provenance.py +81 -18
  125. package/runtime/python/okstra_ctl/design_prep.py +34 -1
  126. package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
  127. package/runtime/python/okstra_ctl/domain/host.py +5 -0
  128. package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
  129. package/runtime/python/okstra_ctl/error_report.py +4 -3
  130. package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
  131. package/runtime/python/okstra_ctl/execution_mutation_audit.py +21 -21
  132. package/runtime/python/okstra_ctl/handoff.py +167 -277
  133. package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
  134. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
  135. package/runtime/python/okstra_ctl/lead_progress.py +1 -1
  136. package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
  137. package/runtime/python/okstra_ctl/manager_cli.py +175 -14
  138. package/runtime/python/okstra_ctl/manager_launch.py +41 -19
  139. package/runtime/python/okstra_ctl/manager_paths.py +22 -3
  140. package/runtime/python/okstra_ctl/manager_split.py +474 -0
  141. package/runtime/python/okstra_ctl/manager_store.py +331 -21
  142. package/runtime/python/okstra_ctl/manager_sync.py +37 -16
  143. package/runtime/python/okstra_ctl/manager_view.py +217 -0
  144. package/runtime/python/okstra_ctl/model_discovery.py +30 -0
  145. package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
  146. package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
  147. package/runtime/python/okstra_ctl/models.py +1 -1
  148. package/runtime/python/okstra_ctl/next_phase.py +16 -6
  149. package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
  150. package/runtime/python/okstra_ctl/option_comparison.py +168 -0
  151. package/runtime/python/okstra_ctl/path_hints.py +9 -0
  152. package/runtime/python/okstra_ctl/paths.py +3 -0
  153. package/runtime/python/okstra_ctl/plan_items_cli.py +6 -1
  154. package/runtime/python/okstra_ctl/profile_show.py +42 -1
  155. package/runtime/python/okstra_ctl/qa_commands.py +15 -0
  156. package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
  157. package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
  158. package/runtime/python/okstra_ctl/render.py +50 -0
  159. package/runtime/python/okstra_ctl/report_contract.py +1 -1
  160. package/runtime/python/okstra_ctl/report_finalize.py +13 -6
  161. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +2 -21
  162. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
  163. package/runtime/python/okstra_ctl/report_html/visualizations.py +0 -5
  164. package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
  165. package/runtime/python/okstra_ctl/report_translation.py +2 -1
  166. package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
  167. package/runtime/python/okstra_ctl/role_requirements.py +142 -129
  168. package/runtime/python/okstra_ctl/rollup.py +3 -1
  169. package/runtime/python/okstra_ctl/run.py +76 -29
  170. package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
  171. package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
  172. package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
  173. package/runtime/python/okstra_ctl/stage_map.py +16 -2
  174. package/runtime/python/okstra_ctl/stage_targets.py +209 -43
  175. package/runtime/python/okstra_ctl/team.py +22 -13
  176. package/runtime/python/okstra_ctl/time_report.py +2 -1
  177. package/runtime/python/okstra_ctl/usage_report.py +3 -1
  178. package/runtime/python/okstra_ctl/verification_target.py +13 -2
  179. package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
  180. package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
  181. package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
  182. package/runtime/python/okstra_ctl/wizard/state.py +3 -5
  183. package/runtime/python/okstra_ctl/wizard/steps_plan.py +3 -23
  184. package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
  185. package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
  186. package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
  187. package/runtime/python/okstra_ctl/workflow.py +1 -1
  188. package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
  189. package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
  190. package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
  191. package/runtime/python/okstra_token_usage/pricing.py +6 -4
  192. package/runtime/schemas/agent-common-v1.schema.json +34 -0
  193. package/runtime/schemas/agent-duty-v1.schema.json +38 -0
  194. package/runtime/schemas/agent-operation-v1.schema.json +11 -0
  195. package/runtime/schemas/agent-profile-v1.schema.json +46 -0
  196. package/runtime/schemas/agent-role-v1.schema.json +29 -0
  197. package/runtime/schemas/final-report-v2.0.schema.json +118 -97
  198. package/runtime/schemas/final-report-v3.0.schema.json +118 -97
  199. package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
  200. package/runtime/skills/okstra-chat/SKILL.md +2 -2
  201. package/runtime/skills/okstra-code-review/SKILL.md +23 -9
  202. package/runtime/skills/okstra-container-build/SKILL.md +10 -10
  203. package/runtime/skills/okstra-inspect/SKILL.md +1 -1
  204. package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
  205. package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
  206. package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
  207. package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
  208. package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
  209. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  210. package/runtime/skills/okstra-inspect/facets/status.md +4 -3
  211. package/runtime/skills/okstra-inspect/facets/time.md +11 -10
  212. package/runtime/skills/okstra-manager/SKILL.md +70 -5
  213. package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
  214. package/runtime/skills/okstra-rollup/SKILL.md +5 -5
  215. package/runtime/skills/okstra-run/SKILL.md +32 -13
  216. package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
  217. package/runtime/skills/okstra-setup/SKILL.md +21 -10
  218. package/runtime/skills/okstra-setup/references/project-config.md +7 -6
  219. package/runtime/skills/okstra-usage/SKILL.md +1 -1
  220. package/runtime/skills/okstra-user-response/SKILL.md +1 -1
  221. package/runtime/templates/manager/view.template.html +109 -0
  222. package/runtime/templates/report-writer-prompt-preamble.md +8 -0
  223. package/runtime/templates/reports/brief.template.md +14 -4
  224. package/runtime/templates/reports/html/i18n/en.json +7 -4
  225. package/runtime/templates/reports/html/i18n/ko.json +7 -4
  226. package/runtime/templates/reports/html/tasks/final-verification.template.html +2 -2
  227. package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
  228. package/runtime/templates/reports/i18n/en.json +1 -1
  229. package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
  230. package/runtime/templates/reports/release-handoff-input.template.md +6 -4
  231. package/runtime/templates/translator-prompt-preamble.md +36 -0
  232. package/runtime/validators/checks/validate-assets-01.py +7 -8
  233. package/runtime/validators/validate-brief.py +77 -2
  234. package/runtime/validators/validate-implementation-plan-stages.py +2 -1
  235. package/runtime/validators/validate-run.py +59 -9
  236. package/runtime/validators/validate-schedule.py +9 -0
  237. package/docs/for-ai/README.md +0 -68
  238. package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
  239. package/docs/for-ai/skills/okstra-chat.md +0 -34
  240. package/docs/for-ai/skills/okstra-code-review.md +0 -57
  241. package/docs/for-ai/skills/okstra-container-build.md +0 -129
  242. package/docs/for-ai/skills/okstra-inspect.md +0 -262
  243. package/docs/for-ai/skills/okstra-manager.md +0 -69
  244. package/docs/for-ai/skills/okstra-memory.md +0 -126
  245. package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
  246. package/docs/for-ai/skills/okstra-rollup.md +0 -114
  247. package/docs/for-ai/skills/okstra-run.md +0 -250
  248. package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
  249. package/docs/for-ai/skills/okstra-setup.md +0 -158
  250. package/docs/for-ai/skills/okstra-usage.md +0 -29
  251. package/docs/for-ai/skills/okstra-user-response.md +0 -72
  252. package/runtime/agents/workers/claude-worker.md +0 -128
  253. package/runtime/agents/workers/report-writer-worker.md +0 -37
  254. package/runtime/agents/workers/translator-worker.md +0 -63
  255. package/runtime/prompts/duties/acceptance-critic.md +0 -44
  256. package/runtime/prompts/duties/acceptance-verifier.md +0 -44
  257. package/runtime/prompts/duties/analysis-worker.md +0 -44
  258. package/runtime/prompts/duties/code-reviewer.md +0 -44
  259. package/runtime/prompts/duties/common.md +0 -39
  260. package/runtime/prompts/duties/diagnosis-worker.md +0 -44
  261. package/runtime/prompts/duties/direction-selection-worker.md +0 -44
  262. package/runtime/prompts/duties/discovery-worker.md +0 -44
  263. package/runtime/prompts/duties/implementation-executor.md +0 -44
  264. package/runtime/prompts/duties/implementation-verifier.md +0 -44
  265. package/runtime/prompts/duties/lead.md +0 -44
  266. package/runtime/prompts/duties/planning-worker.md +0 -52
  267. package/runtime/prompts/duties/report-writer.md +0 -44
  268. package/runtime/prompts/duties/reverification-worker.md +0 -44
  269. package/runtime/prompts/duties/schedule-verifier.md +0 -44
  270. package/runtime/prompts/duties/scope-critic.md +0 -44
  271. package/runtime/prompts/duties/technical-verification-worker.md +0 -44
  272. package/runtime/prompts/duties/translator.md +0 -44
  273. package/runtime/python/okstra_ctl/pane_title.py +0 -154
@@ -131,6 +131,8 @@ _REQUIRED_PROMPT_RESOURCES_HEADING = "## Required prompt resources"
131
131
  _HTML_COMMENTS = re.compile(r"<!--.*?-->\n?", re.DOTALL)
132
132
  _AGENT_CONTRACT_KEYS = {
133
133
  "schemaVersion",
134
+ # 이 run 이 동결한 계약 형식. 없는 run 은 Markdown 직무를 동결했다.
135
+ "contractFormatVersion",
134
136
  "dutyRootPath",
135
137
  "catalogDigest",
136
138
  "invocationReservationRootPath",
@@ -214,6 +216,117 @@ def materialize_initial_prompts(
214
216
  ) from exc
215
217
 
216
218
 
219
+ def declared_delivery_mode(manifest: Mapping[str, Any]) -> PromptDeliveryMode:
220
+ """이 run 의 어댑터가 선언한 초기 프롬프트 배송 방식.
221
+
222
+ 선언은 `leadAdapter.initialPromptDeliveryMode` 에 있고, 그 값은 render 가
223
+ 호스트 descriptor(어댑터가 워커 디스패치를 가져갔으면 그 어댑터)에서
224
+ 옮겨 적은 것이다. 선언이 없는 매니페스트는 이 필드가 생기기 전에 준비된
225
+ run 이므로 그때의 동작인 `eager-include` 로 간다. 아는 값이 아니면 멈춘다 —
226
+ 프롬프트를 발행하고 나면 그 프롬프트는 불변이라, 잘못 고른 배송 방식은
227
+ 나중에 고칠 수 없다.
228
+ """
229
+ adapter = manifest.get("leadAdapter")
230
+ declared = (
231
+ _string_value(adapter.get("initialPromptDeliveryMode"))
232
+ if isinstance(adapter, Mapping)
233
+ else ""
234
+ )
235
+ if not declared:
236
+ return PromptDeliveryMode.EAGER_INCLUDE
237
+ try:
238
+ return PromptDeliveryMode(declared)
239
+ except ValueError:
240
+ allowed = ", ".join(mode.value for mode in PromptDeliveryMode)
241
+ raise InitialPromptMaterializationError(
242
+ "required_input_missing",
243
+ "leadAdapter.initialPromptDeliveryMode is not a delivery mode: "
244
+ f"{declared!r} (allowed: {allowed})",
245
+ ) from None
246
+
247
+
248
+ def _read_side_context(
249
+ project_root: Path, run_manifest_path: Path, runtime_root: Path,
250
+ ) -> "_MaterializationContext":
251
+ """프롬프트를 쓰지 않는 호출자를 위한 컨텍스트.
252
+
253
+ 읽는 것은 매니페스트·active run context·런타임 루트뿐이다. 발행 권한 검사와
254
+ team-state 는 프롬프트를 쓰는 쪽(호출한 CLI)의 몫이다.
255
+ """
256
+ root = project_root.resolve()
257
+ manifest = _load_json_object(
258
+ _resolve_input_path(root, run_manifest_path), "run manifest"
259
+ )
260
+ return _MaterializationContext(
261
+ request=InitialPromptMaterializationRequest(
262
+ project_root=root,
263
+ run_manifest_path=run_manifest_path,
264
+ runtime_root=runtime_root,
265
+ delivery_mode=declared_delivery_mode(manifest),
266
+ workers=(),
267
+ ),
268
+ project_root=root,
269
+ manifest=manifest,
270
+ team_state={},
271
+ active_context=hydrate_active_run_context(
272
+ _load_referenced_json(root, manifest, "activeRunContextPath")
273
+ ),
274
+ )
275
+
276
+
277
+ def implementation_anchor_lines(
278
+ *,
279
+ project_root: Path,
280
+ run_manifest_path: Path,
281
+ runtime_root: Path,
282
+ worker_id: str,
283
+ ) -> list[str]:
284
+ """이 워커의 프롬프트가 싣는 구현 앵커 줄. 구현 audience 가 아니면 빈 목록.
285
+
286
+ executor 의 mutating cwd, 그리고 executor·verifier 공통의
287
+ `**Approved plan:**` / `**Stage for this implementation run:**` 이다.
288
+ `okstra agent-prompt jobs` 는 이 두 헤더를 요구하므로, 리드가 회수 경로로
289
+ 발행한 프롬프트는 그 검사에서 거절됐다(실측 2026-09-23, fontradar-v2-api
290
+ dev-10860 stage-3: 리드가 헤더 두 줄을 손으로 지시문에 붙여 통과시켰다).
291
+ """
292
+ context = _read_side_context(project_root, run_manifest_path, runtime_root)
293
+ return _implementation_anchor_lines(
294
+ context, _prompt_plan(context.manifest, worker_id)
295
+ )
296
+
297
+
298
+ def required_resource_block(
299
+ *,
300
+ project_root: Path,
301
+ run_manifest_path: Path,
302
+ runtime_root: Path,
303
+ worker_id: str,
304
+ ) -> str:
305
+ """이 워커의 프롬프트가 싣는 필수 자원 블록. 없으면 빈 문자열.
306
+
307
+ 디스패치 시점의 composer 와 같은 블록이다 — executor 는 BLOCKING 게이트 세
308
+ 본문과 stage discipline, verifier 는 `_implementation-verifier.md`, 둘 다
309
+ clarification 권한 블록과 fix-run carry. 리드가 `okstra agent-prompt
310
+ materialize` 로 먼저 발행한 프롬프트는 디스패치가 기존 파일로 그대로 쓰므로
311
+ composer 가 돌지 않았고, 이 블록이 통째로 빠졌다(실측 2026-09-22,
312
+ fontradar-v2-api dev-10860 implementation stage-1: executor·verifier 4개
313
+ 프롬프트 모두 0건). 그 명령이 이 함수로 같은 블록을 붙인다.
314
+ """
315
+ context = _read_side_context(project_root, run_manifest_path, runtime_root)
316
+ plan = _prompt_plan(context.manifest, worker_id)
317
+ item = _PromptItem(
318
+ worker=InitialPromptWorkerRequest(worker_id, ""),
319
+ final_path=Path(),
320
+ plan=plan,
321
+ existed=False,
322
+ clarification_input=_resolve_clarification_input(context, plan),
323
+ )
324
+ item = _with_prompt_resources(
325
+ item, _resolve_prompt_resources(context, plan, existed=False)
326
+ )
327
+ return "\n".join(_resource_lines(context, item)).strip()
328
+
329
+
217
330
  def _materialize_initial_prompts(
218
331
  request: InitialPromptMaterializationRequest,
219
332
  ) -> dict[str, Path]:
@@ -243,7 +243,7 @@ def _append(args: argparse.Namespace) -> int:
243
243
 
244
244
  _CLI_EPILOG = r"""Usage:
245
245
  okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-1-intake
246
- okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-4-dispatch --worker codex-worker --field model=gpt-5.6-sol
246
+ okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-4-dispatch --worker codex-worker --field model=gpt-6-sol
247
247
  okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-5-collect --worker codex-worker --field status=completed
248
248
 
249
249
  `progressLine` in the output is the line to emit to the user, verbatim.
@@ -11,7 +11,7 @@ from .domain.host import (
11
11
  HostSessionContext,
12
12
  )
13
13
  from .domain.role import RoleCatalogError, normalize_role
14
- from .role_requirements import RoleProfile, RoleRequirement
14
+ from .role_requirements import RoleProfile, RoleRequirement, StaticRoleRequirement
15
15
 
16
16
 
17
17
  class ModelSelectionInputError(ValueError):
@@ -404,7 +404,7 @@ def _validate_selection(
404
404
  explicit_counts: Mapping[str, int],
405
405
  ) -> None:
406
406
  requirements = {row.role: row for row in profile.roles if not row.dynamic}
407
- requirements["leader"] = RoleRequirement("leader", 1, 1, 1, "lead")
407
+ requirements["leader"] = StaticRoleRequirement("leader", 1, 1, 1, "lead")
408
408
  for role in sorted(set(counts) | set(models)):
409
409
  requirement = requirements.get(role)
410
410
  if requirement is None:
@@ -10,6 +10,7 @@ from okstra_project.dirs import okstra_home
10
10
 
11
11
  from .manager_launch import build_launch_packet
12
12
  from .fixed_text import line, value_lines
13
+ from .manager_paths import ManagerPathError
13
14
  from .manager_store import (
14
15
  ManagerError,
15
16
  append_directive,
@@ -19,20 +20,49 @@ from .manager_store import (
19
20
  discover_projects,
20
21
  init_manager,
21
22
  link_project,
23
+ list_managers,
24
+ list_projects,
25
+ list_task_groups,
26
+ list_tasks,
27
+ remove_child,
28
+ remove_task,
29
+ remove_task_group,
30
+ unlink_project,
31
+ update_task,
22
32
  )
33
+ from .manager_split import split_task
23
34
  from .manager_sync import status_task, sync_task
35
+ from .manager_view import write_view
36
+
37
+ # list 명령마다 목록 한 줄에 싣는 (라벨, 키). 라벨은 "<접두어> <번호> <라벨>" 로 찍힌다.
38
+ _LIST_ROWS = {
39
+ "discover-projects": ("Project", (("ID", "projectId"), ("root", "projectRoot"),
40
+ ("run count", "runCount"), ("active count", "activeCount"),
41
+ ("last run at", "lastRunAt"))),
42
+ "list-managers": ("Manager", (("ID", "managerId"), ("created at", "createdAt"),
43
+ ("project count", "projectCount"), ("task count", "taskCount"))),
44
+ "list-projects": ("Project", (("ID", "projectId"), ("root", "projectRoot"), ("role", "role"),
45
+ ("tags", "tags"), ("linked at", "linkedAt"))),
46
+ "list-task-groups": ("Task group", (("name", "taskGroup"), ("created at", "createdAt"),
47
+ ("task count", "taskCount"))),
48
+ "list-tasks": ("Task", (("group", "taskGroup"), ("ID", "taskId"), ("objective", "objective"),
49
+ ("progress mode", "progressMode"), ("child count", "childCount"),
50
+ ("created at", "createdAt"))),
51
+ }
24
52
 
25
53
 
26
54
  def render_manager_text(command: str, payload: dict | list[dict]) -> str:
27
55
  """manager 모델 표면에 승인된 식별자와 상태만 투영한다."""
28
56
  allowed = {"init", "discover-projects", "new-project", "new-task-group", "new-task",
29
- "task-assign", "task-note", "task-sync", "task-status", "task-run"}
57
+ "task-assign", "task-note", "task-sync", "task-status", "task-run", "task-split",
58
+ "list-managers", "list-projects", "list-task-groups", "list-tasks", "view",
59
+ "task-update", "task-remove-child", "remove-project", "remove-task", "remove-task-group"}
30
60
  if command not in allowed:
31
61
  raise ValueError("unknown manager text purpose")
32
62
  rows = [f"Okstra manager {command}\n", line("Status", "ready")]
33
63
  if isinstance(payload, list):
34
64
  rows.append(line("Result count", len(payload)))
35
- rows.extend(_render_manager_projects(payload))
65
+ rows.extend(_render_list(command, payload))
36
66
  return "".join(rows)
37
67
  if command == "task-status":
38
68
  rows.extend(_render_manager_status(payload))
@@ -44,7 +74,9 @@ def render_manager_text(command: str, payload: dict | list[dict]) -> str:
44
74
  ("Work status", "workStatus"), ("Run command", "command"),
45
75
  ("Backend", "backend"),
46
76
  ("Worker dispatch backend", "workerDispatchBackend"),
47
- ("Project root", "projectRoot"), ("Context path", "contextPath")):
77
+ ("Project root", "projectRoot"), ("Context path", "contextPath"),
78
+ ("Shell command", "shellCommand"), ("View path", "viewPath"),
79
+ ("View URL", "viewUrl")):
48
80
  if key in payload:
49
81
  rows.append(line(label, payload.get(key)))
50
82
  run_args = payload.get("runArgs")
@@ -64,20 +96,46 @@ def render_manager_text(command: str, payload: dict | list[dict]) -> str:
64
96
  ("Directive body", "body"), ("Created at", "createdAt"),
65
97
  ("Source", "source")):
66
98
  rows.append(line(label, payload.get(key)))
99
+ if command == "task-split":
100
+ briefs = payload.get("briefs") if isinstance(payload.get("briefs"), list) else []
101
+ rows.append(line("Brief count", len(briefs)))
102
+ for index, brief in enumerate(briefs, 1):
103
+ for label, key in (("task key", "taskKey"), ("project ID", "projectId"), ("ticket", "ticketId"),
104
+ ("path", "briefPath"), ("status", "briefStatus")):
105
+ rows.append(line(f"Brief {index} {label}", brief.get(key)))
106
+ if command == "task-update":
107
+ rows.append(line("Changed fields", ", ".join(payload.get("changedFields") or []) or "none"))
108
+ rows.extend(value_lines("Manifest", payload.get("manifest")))
109
+ if command.startswith("remove-") or command == "task-remove-child":
110
+ rows.extend(_render_removal(payload))
67
111
  if command == "task-sync" and "syncedAt" in payload:
68
112
  rows.append(line("Synced at", payload.get("syncedAt")))
69
113
  return "".join(rows)
70
114
 
71
115
 
72
- def _render_manager_projects(projects: list[dict]) -> list[str]:
116
+ # 삭제 명령은 --confirm 이 없으면 대상만 보여 준다. 무엇이 지워지고 무엇이 남는지 한 줄씩 찍는다.
117
+ def _render_removal(payload: dict) -> list[str]:
118
+ rows = [line("Removed", "yes" if payload.get("removed") else "no — dry run; rerun with --confirm")]
119
+ for label, key in (("Removed path", "path"), ("Child count", "childCount")):
120
+ if key in payload:
121
+ rows.append(line(label, payload.get(key)))
122
+ for label, key in (("Kept child", "keptChildren"), ("Removed task", "taskIds")):
123
+ values = payload.get(key)
124
+ if isinstance(values, list):
125
+ rows.append(line(f"{label} count", len(values)))
126
+ rows.extend(line(f"{label} {index}", value) for index, value in enumerate(values, 1))
127
+ return rows
128
+
129
+
130
+ def _render_list(command: str, items: list[dict]) -> list[str]:
131
+ prefix, fields = _LIST_ROWS[command]
73
132
  rows: list[str] = []
74
- for index, project in enumerate(projects, 1):
75
- if not isinstance(project, dict):
133
+ for index, item in enumerate(items, 1):
134
+ if not isinstance(item, dict):
76
135
  continue
77
- for label, key in (("ID", "projectId"), ("root", "projectRoot"),
78
- ("run count", "runCount"), ("active count", "activeCount"),
79
- ("last run at", "lastRunAt")):
80
- rows.append(line(f"Project {index} {label}", project.get(key)))
136
+ for label, key in fields:
137
+ value = item.get(key)
138
+ rows.append(line(f"{prefix} {index} {label}", ", ".join(value) if isinstance(value, list) else value))
81
139
  return rows
82
140
 
83
141
 
@@ -96,9 +154,12 @@ def _render_manager_status(payload: dict) -> list[str]:
96
154
  continue
97
155
  for label, key in (("task key", "taskKey"), ("run status", "latestRunStatus"),
98
156
  ("current phase", "currentPhase"), ("work status", "workStatus"),
157
+ ("summary", "summaryBucket"), ("project linked", "projectLinked"),
158
+ ("ticket", "ticketId"), ("brief path", "briefPath"),
99
159
  ("report path", "latestReportRecordPath"), ("error", "error")):
100
160
  if key in child:
101
- rows.append(line(f"Child {index} {label}", child.get(key)))
161
+ value = child.get(key)
162
+ rows.append(line(f"Child {index} {label}", ("yes" if value else "no") if isinstance(value, bool) else value))
102
163
  return rows
103
164
 
104
165
 
@@ -156,7 +217,19 @@ _CLI_EPILOG = r"""Usage:
156
217
  okstra manager task assign --manager-id <id> --task-group <task-group> --task-id <task-id> --project-id <project-id>
157
218
  okstra manager task note --manager-id <id> --task-group <task-group> --task-id <task-id> --scope <shared|project> --body <text>
158
219
  okstra manager task run --manager-id <id> --project-id <project-id> --task-group <task-group> --task-id <task-id>
159
-
220
+ okstra manager task split --manager-id <id> --task-group <task-group> --task-id <task-id> --plan <split-plan.json> [--overwrite]
221
+ okstra manager task update --manager-id <id> --task-group <task-group> --task-id <task-id> [--objective <text>] [--common-brief <path>] [--progress-mode <manual|auto>]
222
+ okstra manager task remove-child --manager-id <id> --task-group <task-group> --task-id <task-id> --project-id <project-id> [--child-task-id <id>] [--confirm]
223
+ okstra manager remove project --manager-id <id> --project-id <project-id> [--confirm]
224
+ okstra manager remove task --manager-id <id> --task-group <task-group> --task-id <task-id> [--confirm]
225
+ okstra manager remove task-group --manager-id <id> --task-group <task-group> [--confirm]
226
+ okstra manager list managers
227
+ okstra manager list projects --manager-id <id>
228
+ okstra manager list task-groups --manager-id <id>
229
+ okstra manager list tasks --manager-id <id>
230
+ okstra manager view --manager-id <id>
231
+
232
+ remove and task remove-child only show their targets unless --confirm is given.
160
233
  --workspace-root is owned by this command.
161
234
  """
162
235
  _CLI_DESCRIPTION = "Manage cross-project okstra tasks."
@@ -198,7 +271,7 @@ def _parser() -> argparse.ArgumentParser:
198
271
  task_new.add_argument("--task", action="append", default=[])
199
272
  task_new.add_argument("--objective", default="")
200
273
  task_new.add_argument("--common-brief", default="")
201
- task_new.add_argument("--progress-mode", choices=["manual", "auto"], default="manual")
274
+ task_new.add_argument("--progress-mode", choices=["manual", "auto"], default=None)
202
275
 
203
276
  task = sub.add_parser("task")
204
277
  task_sub = task.add_subparsers(dest="task_command", required=True)
@@ -233,6 +306,54 @@ def _parser() -> argparse.ArgumentParser:
233
306
  run.add_argument("--task-group", required=True)
234
307
  run.add_argument("--task-id", required=True)
235
308
  run.add_argument("--child-task-id", default=None)
309
+
310
+ split = task_sub.add_parser("split")
311
+ split.add_argument("--manager-id", required=True)
312
+ split.add_argument("--task-group", required=True)
313
+ split.add_argument("--task-id", required=True)
314
+ split.add_argument("--plan", required=True)
315
+ split.add_argument("--overwrite", action="store_true")
316
+
317
+ update = task_sub.add_parser("update")
318
+ update.add_argument("--manager-id", required=True)
319
+ update.add_argument("--task-group", required=True)
320
+ update.add_argument("--task-id", required=True)
321
+ update.add_argument("--objective", default=None)
322
+ update.add_argument("--common-brief", default=None)
323
+ update.add_argument("--progress-mode", choices=["manual", "auto"], default=None)
324
+
325
+ remove_child_cmd = task_sub.add_parser("remove-child")
326
+ remove_child_cmd.add_argument("--manager-id", required=True)
327
+ remove_child_cmd.add_argument("--task-group", required=True)
328
+ remove_child_cmd.add_argument("--task-id", required=True)
329
+ remove_child_cmd.add_argument("--project-id", required=True)
330
+ remove_child_cmd.add_argument("--child-task-id", default=None)
331
+ remove_child_cmd.add_argument("--confirm", action="store_true")
332
+
333
+ remove = sub.add_parser("remove")
334
+ remove_sub = remove.add_subparsers(dest="remove_command", required=True)
335
+ remove_project = remove_sub.add_parser("project")
336
+ remove_project.add_argument("--manager-id", required=True)
337
+ remove_project.add_argument("--project-id", required=True)
338
+ remove_project.add_argument("--confirm", action="store_true")
339
+ remove_task_cmd = remove_sub.add_parser("task")
340
+ remove_task_cmd.add_argument("--manager-id", required=True)
341
+ remove_task_cmd.add_argument("--task-group", required=True)
342
+ remove_task_cmd.add_argument("--task-id", required=True)
343
+ remove_task_cmd.add_argument("--confirm", action="store_true")
344
+ remove_group = remove_sub.add_parser("task-group")
345
+ remove_group.add_argument("--manager-id", required=True)
346
+ remove_group.add_argument("--task-group", required=True)
347
+ remove_group.add_argument("--confirm", action="store_true")
348
+
349
+ listing = sub.add_parser("list")
350
+ list_sub = listing.add_subparsers(dest="list_command", required=True)
351
+ list_sub.add_parser("managers")
352
+ for name in ("projects", "task-groups", "tasks"):
353
+ list_sub.add_parser(name).add_argument("--manager-id", required=True)
354
+
355
+ view = sub.add_parser("view")
356
+ view.add_argument("--manager-id", required=True)
236
357
  return parser
237
358
 
238
359
 
@@ -322,11 +443,51 @@ def main(argv: list[str] | None = None) -> int:
322
443
  ),
323
444
  args.json,
324
445
  )
446
+ elif args.command == "task" and args.task_command == "split":
447
+ _emit(
448
+ "task-split",
449
+ split_task(
450
+ home,
451
+ args.manager_id,
452
+ args.task_group,
453
+ args.task_id,
454
+ Path(args.plan),
455
+ overwrite=args.overwrite,
456
+ ),
457
+ args.json,
458
+ )
459
+ elif args.command == "list" and args.list_command == "managers":
460
+ _emit("list-managers", list_managers(home), args.json)
461
+ elif args.command == "list" and args.list_command == "projects":
462
+ _emit("list-projects", list_projects(home, args.manager_id), args.json)
463
+ elif args.command == "task" and args.task_command == "update":
464
+ _emit("task-update", update_task(home, args.manager_id, args.task_group, args.task_id,
465
+ objective=args.objective, common_brief_path=args.common_brief,
466
+ progress_mode=args.progress_mode), args.json)
467
+ elif args.command == "task" and args.task_command == "remove-child":
468
+ _emit("task-remove-child", remove_child(home, args.manager_id, args.task_group, args.task_id,
469
+ args.project_id, child_task_id=args.child_task_id,
470
+ confirm=args.confirm), args.json)
471
+ elif args.command == "remove" and args.remove_command == "project":
472
+ _emit("remove-project", unlink_project(home, args.manager_id, args.project_id,
473
+ confirm=args.confirm), args.json)
474
+ elif args.command == "remove" and args.remove_command == "task":
475
+ _emit("remove-task", remove_task(home, args.manager_id, args.task_group, args.task_id,
476
+ confirm=args.confirm), args.json)
477
+ elif args.command == "remove" and args.remove_command == "task-group":
478
+ _emit("remove-task-group", remove_task_group(home, args.manager_id, args.task_group,
479
+ confirm=args.confirm), args.json)
480
+ elif args.command == "list" and args.list_command == "task-groups":
481
+ _emit("list-task-groups", list_task_groups(home, args.manager_id), args.json)
482
+ elif args.command == "list" and args.list_command == "tasks":
483
+ _emit("list-tasks", list_tasks(home, args.manager_id), args.json)
484
+ elif args.command == "view":
485
+ _emit("view", write_view(home, args.manager_id), args.json)
325
486
  else:
326
487
  parser.print_help()
327
488
  return 2
328
489
  return 0
329
- except ManagerError as exc:
490
+ except (ManagerError, ManagerPathError) as exc:
330
491
  print(f"okstra manager: {exc}", file=sys.stderr)
331
492
  return 2
332
493
 
@@ -1,6 +1,7 @@
1
1
  """Build manager child launch packets and context documents."""
2
2
  from __future__ import annotations
3
3
 
4
+ import shlex
4
5
  from pathlib import Path
5
6
 
6
7
  from okstra_ctl.jsonl import append_jsonl, read_jsonl
@@ -15,12 +16,16 @@ from .manager_paths import (
15
16
  task_manifest_path,
16
17
  )
17
18
  from .manager_store import ManagerError, _now_iso, _read_json, _read_json_default, _write_json
19
+ from .manager_sync import _resolve_task_root_read_only
18
20
 
19
21
 
20
22
  WORKER_DISPATCH_BACKEND = "subagent"
21
- # child lead 는 언제나 subagent 로 뜬다. 예전에는 `$TMUX` 가 잡히면
22
- # `tmux-child-lead` 를 골랐지만 tmux 백엔드가 사라져 분기가 남지 않았다.
23
- CHILD_LEAD_BACKEND = "subagent-child-lead"
23
+ # child lead 는 설치본 런처(`okstra.sh`)가 새 host 프로세스로 띄운다. `okstra run`
24
+ # 은 lead host 에 `--launch-only` 를 붙여 task 인자와 `--directive` 를 버리므로
25
+ # child 맥락이 전달되지 않는다(run.mts 의 launch-lead 분기). 런처는 prepare 까지
26
+ # 거친 뒤 `launch-plan --entry-mode spawn-process` 로 lead 를 띄운다.
27
+ CHILD_LEAD_BACKEND = "spawn-process"
28
+ LAUNCHER_RELATIVE = Path("bin") / "okstra.sh"
24
29
  LAUNCH_STATUS_PREPARED = "prepared"
25
30
 
26
31
 
@@ -28,7 +33,7 @@ def _find_project(projects: dict, project_id: str) -> dict:
28
33
  for project in projects.get("projects", []):
29
34
  if isinstance(project, dict) and project.get("projectId") == project_id:
30
35
  return project
31
- raise ManagerError(f"unknown project membership: {project_id}")
36
+ raise ManagerError(f"{project_id}: project unlinked from this manager; re-register it with `okstra manager new project`")
32
37
 
33
38
 
34
39
  def _target_task_key(project_id: str, task_group: str, task_id: str) -> str:
@@ -110,9 +115,14 @@ def _render_context(
110
115
  "",
111
116
  child.get("assignment") or "",
112
117
  "",
113
- "## Manager Directives",
114
- "",
115
118
  ]
119
+ if child.get("scope"):
120
+ # `task split` 이 만든 하위 태스크. 이 프로젝트에서 할 범위만 적는다.
121
+ lines.extend(["## Project Scope", "", f"- ticket: {child.get('ticketId') or ''}",
122
+ f"- brief: {child.get('briefPath') or ''}", ""])
123
+ lines.extend(f"- in scope: {item}" for item in child["scope"])
124
+ lines.append("")
125
+ lines.extend(["## Manager Directives", ""])
116
126
  for row in directives:
117
127
  lines.append(f"- [{row.get('scope')}] {row.get('body')}")
118
128
  if not directives:
@@ -214,6 +224,28 @@ def build_launch_packet(
214
224
  ),
215
225
  encoding="utf-8",
216
226
  )
227
+ command = str(Path(home) / LAUNCHER_RELATIVE)
228
+ run_args = [
229
+ "--project-root",
230
+ project_root,
231
+ "--project-id",
232
+ project_id,
233
+ "--task-group",
234
+ str(child.get("taskGroup") or task_group),
235
+ "--task-id",
236
+ child_task_id_value,
237
+ ]
238
+ # `task split` 이 기록한 브리프·권장 phase 를 넘겨 런처가 첫 실행에서 다시 묻지 않게 한다.
239
+ # phase 는 하위 태스크가 아직 없을 때만 넘긴다. 이미 있으면 런처가 manifest 에서 다음
240
+ # phase 를 채워야 하는데, 두 값이 다 주어지면 그 채우기를 건너뛴다(autofill_from_manifest).
241
+ if child.get("recommendedPhase") and _resolve_task_root_read_only(Path(project_root), target_task_key) is None:
242
+ run_args.extend(["--task-type", str(child["recommendedPhase"])])
243
+ if child.get("briefPath"):
244
+ run_args.extend(["--task-brief", str(child["briefPath"])])
245
+ run_args += [
246
+ "--directive",
247
+ f"Read manager child context: {context_path}",
248
+ ]
217
249
  packet = {
218
250
  "managerId": manager_id,
219
251
  "taskGroup": task_group,
@@ -224,19 +256,9 @@ def build_launch_packet(
224
256
  "workerDispatchBackend": WORKER_DISPATCH_BACKEND,
225
257
  "projectRoot": project_root,
226
258
  "contextPath": str(context_path),
227
- "runArgs": [
228
- "run",
229
- "--project-root",
230
- project_root,
231
- "--project-id",
232
- project_id,
233
- "--task-group",
234
- str(child.get("taskGroup") or task_group),
235
- "--task-id",
236
- child_task_id_value,
237
- "--directive",
238
- f"Read manager child context: {context_path}",
239
- ],
259
+ "command": command,
260
+ "runArgs": run_args,
261
+ "shellCommand": shlex.join([command, *run_args]),
240
262
  "createdAt": created_at,
241
263
  }
242
264
  _record_launch_prepared(
@@ -54,10 +54,21 @@ def manager_root(home: Path, manager_id: str) -> Path:
54
54
  return Path(home) / "managers" / canonical_manager_id(manager_id)
55
55
 
56
56
 
57
+ def task_groups_root(home: Path, manager_id: str) -> Path:
58
+ return manager_root(home, manager_id) / "task-groups"
59
+
60
+
61
+ def task_group_root(home: Path, manager_id: str, task_group: str) -> Path:
62
+ return task_groups_root(home, manager_id) / _slug_segment(task_group, "task-group")
63
+
64
+
65
+ def task_group_meta_path(home: Path, manager_id: str, task_group: str) -> Path:
66
+ """폴더 이름은 slug 라 원래 그룹 이름을 여기에 남긴다."""
67
+ return task_group_root(home, manager_id, task_group) / "group.json"
68
+
69
+
57
70
  def task_root(home: Path, manager_id: str, task_group: str, task_id: str) -> Path:
58
- group_segment = _slug_segment(task_group, "task-group")
59
- task_segment = _slug_segment(task_id, "task-id")
60
- return manager_root(home, manager_id) / "task-groups" / group_segment / task_segment
71
+ return task_group_root(home, manager_id, task_group) / _slug_segment(task_id, "task-id")
61
72
 
62
73
 
63
74
  def manager_json_path(home: Path, manager_id: str) -> Path:
@@ -84,6 +95,10 @@ def snapshots_json_path(home: Path, manager_id: str, task_group: str, task_id: s
84
95
  return task_root(home, manager_id, task_group, task_id) / "snapshots.json"
85
96
 
86
97
 
98
+ def split_plan_path(home: Path, manager_id: str, task_group: str, task_id: str) -> Path:
99
+ return task_root(home, manager_id, task_group, task_id) / "split-plan.json"
100
+
101
+
87
102
  def events_jsonl_path(home: Path, manager_id: str, task_group: str, task_id: str) -> Path:
88
103
  return task_root(home, manager_id, task_group, task_id) / "events.jsonl"
89
104
 
@@ -99,3 +114,7 @@ def child_context_path(
99
114
  safe_project = _slug_segment(project_id, "project-id")
100
115
  safe_task = _slug_segment(child_task_id, "task-id")
101
116
  return task_root(home, manager_id, task_group, task_id) / "child-context" / f"{safe_project}-{safe_task}.md"
117
+
118
+
119
+ def view_html_path(home: Path, manager_id: str) -> Path:
120
+ return manager_root(home, manager_id) / "view" / "index.html"