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
@@ -647,6 +647,9 @@ def compute_run_paths(
647
647
  "REPORT_WRITER_PREAMBLE_PATH": str(
648
648
  runtime_home / "templates" / "report-writer-prompt-preamble.md"
649
649
  ),
650
+ "TRANSLATOR_PREAMBLE_PATH": str(
651
+ runtime_home / "templates" / "translator-prompt-preamble.md"
652
+ ),
650
653
  "WORKER_ERROR_CONTRACT_PATH": str(
651
654
  runtime_home / "templates" / "worker-error-contract.md"
652
655
  ),
@@ -26,6 +26,7 @@ from .convergence_store import write_json_atomic
26
26
  from .incremental_carry import sync_prepared_dispatch_queue
27
27
  from .json_boundary import JsonBoundaryError, load_owned_object
28
28
  from .report_finalize import task_manifest_path
29
+ from .qa_commands import split_verification_command
29
30
  from .plan_derivations import extract_tokens, find_derivations
30
31
  from .plan_items import (
31
32
  NextDispatch,
@@ -577,7 +578,11 @@ def _render_scalar_or_list(key: str, value: object, indent: str = "") -> list[st
577
578
  if isinstance(value, Mapping):
578
579
  raise PlanItemContractError(f"plan item {key} must be scalar")
579
580
  if key in {"command", "commandOrObservation"} and isinstance(value, str):
580
- return _render_literal(_label(key), value, indent)
581
+ command, annotation = split_verification_command(value)
582
+ rows = _render_literal(_label(key), command, indent)
583
+ if annotation is not None:
584
+ rows.append(indent + line("Command annotation (not executable)", annotation))
585
+ return rows
581
586
  return [indent + line(_label(key), value)]
582
587
 
583
588
 
@@ -19,6 +19,7 @@ import sys
19
19
  from pathlib import Path
20
20
 
21
21
  from .paths import find_asset_root
22
+ from .role_requirements import RoleProfileError, load_role_profile
22
23
  from .run import PrepareError, _expand_profile_includes
23
24
 
24
25
  _PROFILES_REL = ("prompts", "profiles")
@@ -82,9 +83,49 @@ def sidecar_bodies(root: Path, profile_text: str) -> list[str]:
82
83
  def render(root: Path, task_type: str, *, resolved: bool) -> str:
83
84
  path = profile_path(root, task_type)
84
85
  if not resolved:
86
+ # 계약상 이 출력은 디스크의 파일 그대로다. 역할 구조는 이제 `.json` 에
87
+ # 있으므로 `--resolved` 가 렌더한다.
85
88
  return path.read_text(encoding="utf-8")
86
89
  expanded = _expand_profile_includes(path)
87
- return expanded + "".join(sidecar_bodies(root, expanded))
90
+ body = expanded + "".join(sidecar_bodies(root, expanded))
91
+ return body + _role_requirements_section(path)
92
+
93
+
94
+ def _role_requirements_section(profile_path: Path) -> str:
95
+ """Render the profile's role structure from its contract.
96
+
97
+ The structure moved out of the Markdown into the `.json` sibling, so a
98
+ reader of `okstra profile show` would otherwise no longer see which roles
99
+ the phase runs — which is most of what the command is read for.
100
+ """
101
+ try:
102
+ profile = load_role_profile(profile_path)
103
+ except RoleProfileError as exc:
104
+ return f"\n## Role requirements\n\nunavailable: {exc}\n"
105
+ if not profile.roles:
106
+ return "\n## Role requirements\n\nThis phase runs a single lead and no workers.\n"
107
+ lines = [
108
+ "",
109
+ "## Role requirements",
110
+ "",
111
+ "| Role | Duty | Count (min/recommended/max) | Activation |",
112
+ "|---|---|---|---|",
113
+ ]
114
+ for requirement in profile.roles:
115
+ if requirement.dynamic:
116
+ sources = ", ".join(f"`{role}`" for role in requirement.source_role_ids)
117
+ counts = f"one per activated {sources} slot"
118
+ activation = requirement.activation
119
+ else:
120
+ counts = (
121
+ f"{requirement.min_count} / {requirement.recommended_count} / "
122
+ f"{requirement.max_count}"
123
+ )
124
+ activation = "static"
125
+ lines.append(
126
+ f"| `{requirement.role}` | `{requirement.duty}` | {counts} | {activation} |"
127
+ )
128
+ return "\n".join(lines) + "\n"
88
129
 
89
130
 
90
131
  def _write_stdout(text: str) -> None:
@@ -98,6 +98,21 @@ def find_unfrozen_installs(cmd: str) -> list[str]:
98
98
  return found
99
99
 
100
100
 
101
+ def split_verification_command(command: str) -> tuple[str, str | None]:
102
+ """기존 계획의 파일 인용 설명만 분리하며 일반 셸 구문은 보존한다."""
103
+ match = re.search(r";\s*(existing helper:\s*[^;&|<>$`\n]+:\d+(?:-\d+)?)\s*$", command)
104
+ if match is None:
105
+ return command, None
106
+ executable = command[:match.start()].rstrip()
107
+ try:
108
+ words = shlex.split(executable)
109
+ except ValueError:
110
+ return command, None
111
+ if not words:
112
+ return command, None
113
+ return executable, match.group(1).strip()
114
+
115
+
101
116
  def verification_command_defects(command: str) -> list[str]:
102
117
  """선언된 작업 폴더를 셸에서 다른 체크아웃으로 바꾸는 명령을 거부한다."""
103
118
  try:
@@ -18,6 +18,8 @@ class HostManifest:
18
18
  native_provider_id: str
19
19
  required_executables: tuple[str, ...]
20
20
  relay_contract: Path
21
+ # 선언하지 않은 호스트는 None. 이 호스트 세션 안에서 도는 워커가 읽는 계약이다.
22
+ worker_session_contract: Path | None = None
21
23
 
22
24
 
23
25
  def discover_host_manifests(roots: Iterable[Path]) -> tuple[HostManifest, ...]:
@@ -54,7 +56,15 @@ def _read_manifest(manifest_path: Path) -> HostManifest:
54
56
  native_provider_id = _optional_id(raw, "nativeProviderId", manifest_path)
55
57
  required_executables = _string_tuple(raw, "requiredExecutables", manifest_path)
56
58
  relay_ref = _required_string(raw, "relayContract", manifest_path)
57
- relay_contract = _resolve_relay_contract(manifest_path, relay_ref)
59
+ relay_contract = _resolve_adapter_file(manifest_path, relay_ref, "relay contract")
60
+ session_ref = raw.get("workerSessionContract")
61
+ if session_ref is not None and (not isinstance(session_ref, str) or not session_ref):
62
+ raise HostAdapterContractError(f"invalid host manifest: {manifest_path}")
63
+ worker_session_contract = (
64
+ _resolve_adapter_file(manifest_path, session_ref, "worker session contract")
65
+ if session_ref is not None
66
+ else None
67
+ )
58
68
  return HostManifest(
59
69
  path=manifest_path,
60
70
  host_id=host_id,
@@ -62,6 +72,7 @@ def _read_manifest(manifest_path: Path) -> HostManifest:
62
72
  native_provider_id=native_provider_id,
63
73
  required_executables=required_executables,
64
74
  relay_contract=relay_contract,
75
+ worker_session_contract=worker_session_contract,
65
76
  )
66
77
 
67
78
 
@@ -111,15 +122,12 @@ def _string_tuple(
111
122
  return tuple(value)
112
123
 
113
124
 
114
- def _resolve_relay_contract(manifest_path: Path, relay_ref: str) -> Path:
125
+ def _resolve_adapter_file(manifest_path: Path, ref: str, label: str) -> Path:
126
+ """어댑터 디렉터리 안의 파일 하나를 해석한다. 밖을 가리키면 거절한다."""
115
127
  adapter_root = manifest_path.parent.resolve()
116
- if Path(relay_ref).is_absolute():
117
- raise HostAdapterContractError(
118
- f"relay contract escapes adapter root: {relay_ref!r}"
119
- )
120
- relay_contract = (adapter_root / relay_ref).resolve()
121
- if adapter_root not in relay_contract.parents or not relay_contract.is_file():
122
- raise HostAdapterContractError(
123
- f"relay contract escapes adapter root: {relay_ref!r}"
124
- )
125
- return relay_contract
128
+ if Path(ref).is_absolute():
129
+ raise HostAdapterContractError(f"{label} escapes adapter root: {ref!r}")
130
+ resolved = (adapter_root / ref).resolve()
131
+ if adapter_root not in resolved.parents or not resolved.is_file():
132
+ raise HostAdapterContractError(f"{label} escapes adapter root: {ref!r}")
133
+ return resolved
@@ -248,6 +248,15 @@ def _validated_descriptor(adapter: object, manifest: HostManifest) -> HostDescri
248
248
  raise HostAdapterContractError(
249
249
  f"relay contract mismatch for {manifest.host_id!r}"
250
250
  )
251
+ declared = (
252
+ Path(descriptor.worker_session_contract).resolve()
253
+ if descriptor.worker_session_contract
254
+ else None
255
+ )
256
+ if declared != manifest.worker_session_contract:
257
+ raise HostAdapterContractError(
258
+ f"worker session contract mismatch for {manifest.host_id!r}"
259
+ )
251
260
  return descriptor
252
261
 
253
262
 
@@ -286,6 +295,8 @@ def _copy_descriptor(raw: object, host_id: str) -> HostDescriptor:
286
295
  session_accounting=raw.session_accounting,
287
296
  has_claude_session=raw.has_claude_session,
288
297
  relay_contract=raw.relay_contract,
298
+ worker_session_contract=getattr(raw, "worker_session_contract", ""),
299
+ initial_prompt_delivery_mode=raw.initial_prompt_delivery_mode,
289
300
  )
290
301
  except (AttributeError, TypeError) as exc:
291
302
  raise HostAdapterContractError(
@@ -455,6 +455,10 @@ def _worker_assignments(ctx: dict) -> list[dict]:
455
455
  # 다른데 반환값만 봐서는 구별되지 않는다.
456
456
  print(f"render: WORKER_ASSIGNMENTS_JSON is not valid JSON, "
457
457
  f"falling back to the worker catalog ({exc})", file=sys.stderr)
458
+ # 여기 도달하면 이 run 에는 해소된 배정이 없다 — 준비 경로는 언제나
459
+ # `WORKER_ASSIGNMENTS_JSON` 을 채운다(`run.py` `_model_tokens`). 남은 정보가
460
+ # 워커 id 뿐이라 역할을 복원할 수 없고, 아래 값은 분석 run 로스터의 모양을
461
+ # 가정한 것이다. 이 로스터로 implementation 프롬프트를 고를 수는 없다.
458
462
  catalog = _worker_catalog(ctx, include_assignments=False)
459
463
  return [
460
464
  {
@@ -690,9 +694,13 @@ def _active_runtime_resources(ctx: dict) -> dict:
690
694
  or str(runtime_home / "templates" / "implementation-worker-preamble.md"),
691
695
  "report-writer": ctx.get("REPORT_WRITER_PREAMBLE_PATH")
692
696
  or str(runtime_home / "templates" / "report-writer-prompt-preamble.md"),
697
+ "translator": ctx.get("TRANSLATOR_PREAMBLE_PATH")
698
+ or str(runtime_home / "templates" / "translator-prompt-preamble.md"),
693
699
  },
694
700
  "workerErrorContractPath": ctx.get("WORKER_ERROR_CONTRACT_PATH")
695
701
  or str(runtime_home / "templates" / "worker-error-contract.md"),
702
+ "hostSessionContractPath": ctx.get("HOST_SESSION_CONTRACT_PATH")
703
+ or _host_session_contract_path(ctx),
696
704
  }
697
705
 
698
706
 
@@ -2019,6 +2027,9 @@ _V2_INVOCATION_IDENTITY_KEYS = {
2019
2027
  }
2020
2028
  _AGENT_CONTRACT_KEYS = {
2021
2029
  "schemaVersion",
2030
+ # 이 run 이 동결한 계약의 형식. 없는 run 은 Markdown 직무를 동결했고, 그
2031
+ # 형식의 리더는 제거됐다.
2032
+ "contractFormatVersion",
2022
2033
  "dutyRootPath",
2023
2034
  "catalogDigest",
2024
2035
  "invocationReservationRootPath",
@@ -2610,9 +2621,15 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2610
2621
  report_writer_preamble = ctx.get("REPORT_WRITER_PREAMBLE_PATH") or str(
2611
2622
  runtime_templates / "report-writer-prompt-preamble.md"
2612
2623
  )
2624
+ translator_preamble = ctx.get("TRANSLATOR_PREAMBLE_PATH") or str(
2625
+ runtime_templates / "translator-prompt-preamble.md"
2626
+ )
2613
2627
  worker_error_contract = ctx.get("WORKER_ERROR_CONTRACT_PATH") or str(
2614
2628
  runtime_templates / "worker-error-contract.md"
2615
2629
  )
2630
+ host_session_contract = ctx.get(
2631
+ "HOST_SESSION_CONTRACT_PATH"
2632
+ ) or _host_session_contract_path(ctx)
2616
2633
  okstra_runtime_resources_block = (
2617
2634
  "## Okstra Runtime Resources\n"
2618
2635
  "\n"
@@ -2629,8 +2646,14 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2629
2646
  f"- Analysis worker preamble: `{analysis_preamble}`\n"
2630
2647
  f"- Implementation worker preamble: `{implementation_preamble}`\n"
2631
2648
  f"- Report writer preamble: `{report_writer_preamble}`\n"
2649
+ f"- Translator preamble: `{translator_preamble}`\n"
2650
+
2632
2651
  f"- Shared worker error contract: `{worker_error_contract}`"
2633
2652
  )
2653
+ if host_session_contract:
2654
+ okstra_runtime_resources_block += (
2655
+ f"\n- Host worker session contract: `{host_session_contract}`"
2656
+ )
2634
2657
 
2635
2658
  def fmt_assignment(role: str, model: str, execution: str) -> str:
2636
2659
  if execution and execution != model:
@@ -2814,6 +2837,23 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2814
2837
  ctx["EXECUTION_STATUS_TABLE_ROWS"] = "\n".join(execution_status_table_lines)
2815
2838
 
2816
2839
 
2840
+ def _host_session_contract_path(ctx: Mapping[str, object]) -> str:
2841
+ """이 run 의 호스트가 선언한 워커 세션 계약의 절대 경로. 없으면 빈 문자열.
2842
+
2843
+ 호스트를 못 찾는 것은 여기서 실패할 일이 아니다 — 계약이 없는 호스트와
2844
+ 같게 다루고, 헤더를 빼면 그 워커는 세션 규칙 없이 돈다. 호스트 해석 실패
2845
+ 자체는 이 함수를 부르기 전에 이미 run 준비를 막는다.
2846
+ """
2847
+ host_id = str(ctx.get("HOST_RUNTIME", "") or _lead_runtime(ctx) or "").strip()
2848
+ if not host_id:
2849
+ return ""
2850
+ try:
2851
+ descriptor = default_host_registry().resolve(host_id).descriptor
2852
+ except Exception:
2853
+ return ""
2854
+ return descriptor.worker_session_contract or ""
2855
+
2856
+
2817
2857
  def apply_lead_prompt_defaults(ctx: dict) -> None:
2818
2858
  """Apply default values for optional lead-prompt ctx fields.
2819
2859
 
@@ -2853,10 +2893,20 @@ def apply_lead_prompt_defaults(ctx: dict) -> None:
2853
2893
  "REPORT_WRITER_PREAMBLE_PATH",
2854
2894
  str(runtime_home / "templates" / "report-writer-prompt-preamble.md"),
2855
2895
  )
2896
+ ctx.setdefault(
2897
+ "TRANSLATOR_PREAMBLE_PATH",
2898
+ str(runtime_home / "templates" / "translator-prompt-preamble.md"),
2899
+ )
2856
2900
  ctx.setdefault(
2857
2901
  "WORKER_ERROR_CONTRACT_PATH",
2858
2902
  str(runtime_home / "templates" / "worker-error-contract.md"),
2859
2903
  )
2904
+ # 호스트가 선언한 워커 세션 계약. 선언하지 않은 호스트는 빈 문자열이고,
2905
+ # 그러면 헤더가 방출되지 않는다.
2906
+ ctx.setdefault(
2907
+ "HOST_SESSION_CONTRACT_PATH",
2908
+ _host_session_contract_path(ctx),
2909
+ )
2860
2910
  # Lead resource paths the launch template references directly. paths.py
2861
2911
  # seeds the production values; setdefault backfills callers that render the
2862
2912
  # template without the full path ctx.
@@ -123,7 +123,7 @@ TASK_TYPE_REQUIRED_HUMAN_FIELDS = {
123
123
  "release-handoff": (
124
124
  "releaseHandoff.userSelections",
125
125
  "releaseHandoff.featureBranchState",
126
- "releaseHandoff.pullRequestOutcome",
126
+ "releaseHandoff.pullRequestOutcomes",
127
127
  "releaseHandoff.mergeConflictProbe",
128
128
  ),
129
129
  }
@@ -737,6 +737,8 @@ def _closeout_report_paths(ctx: FinalizeContext) -> dict[str, Any]:
737
737
  답장을 마크다운으로 그리므로 괄호 안에 경로가 들어간 형태만 클릭되고,
738
738
  백틱으로 감싼 경로는 글자로만 남아 사용자가 직접 옮겨 적어야 한다. 리드가
739
739
  그 형태를 매번 다시 만들면 어긋날 자리가 생기므로 완성된 문자열을 준다.
740
+ 링크 목적지는 절대 경로다 — 프로젝트 기준 상대 경로는 호스트 cwd 가 다르거나
741
+ 외부 프로그램으로 열 때 해석되지 않는다. 공백이 든 경로는 `<...>` 로 감싼다.
740
742
  """
741
743
  def rel(path: Path) -> str:
742
744
  try:
@@ -744,17 +746,22 @@ def _closeout_report_paths(ctx: FinalizeContext) -> dict[str, Any]:
744
746
  except ValueError:
745
747
  return str(path)
746
748
 
747
- paths = {
748
- "humanReport": rel(html_view_path(ctx.data_path)),
749
- "reportRecord": rel(ctx.data_path),
750
- "teamState": rel(ctx.team_state_path),
749
+ def link_target(path: Path) -> str:
750
+ target = path.resolve().as_posix()
751
+ return f"<{target}>" if " " in target else target
752
+
753
+ sources = {
754
+ "humanReport": html_view_path(ctx.data_path),
755
+ "reportRecord": ctx.data_path,
756
+ "teamState": ctx.team_state_path,
751
757
  }
758
+ paths = {key: rel(path) for key, path in sources.items()}
752
759
  return {
753
760
  **paths,
754
761
  "renderFullCopy": f"okstra render-final-report {paths['reportRecord']}",
755
762
  "markdown": {
756
- key: f"[{Path(value).name}]({value})"
757
- for key, value in paths.items()
763
+ key: f"[{path.name}]({link_target(path)})"
764
+ for key, path in sources.items()
758
765
  },
759
766
  }
760
767
 
@@ -3,8 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  from ...release_gate import release_handoff_allowed
5
5
  from ..common import evidence_index
6
- from ..models import HumanReportView, VisualNode
7
- from ..visualizations import coverage_figure
6
+ from ..models import HumanReportView
8
7
 
9
8
  # Record fields this template anchors as `id-<row id>` (see
10
9
  # `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
@@ -17,25 +16,8 @@ ANCHORED_FIELDS = (
17
16
  )
18
17
 
19
18
 
20
- def _coverage_nodes(final: dict) -> tuple[VisualNode, ...]:
21
- return tuple(
22
- VisualNode(
23
- row["id"],
24
- row["requirement"],
25
- "requirement",
26
- row["status"],
27
- row["artifact"],
28
- note=row["status"],
29
- )
30
- for row in final["validationEvidence"]
31
- )
32
-
33
-
34
19
  def build_final_verification_view(data: dict) -> HumanReportView:
35
20
  final = data["finalVerification"]
36
- figure = coverage_figure(
37
- rows=_coverage_nodes(final), title="Requirement verification coverage"
38
- )
39
21
  verdict_token = data["finalVerdict"]["verdictToken"]
40
22
  context = {
41
23
  "humanSummary": data["humanSummary"],
@@ -44,7 +26,6 @@ def build_final_verification_view(data: dict) -> HumanReportView:
44
26
  "finalVerdict": data["finalVerdict"],
45
27
  "final": final,
46
28
  "narrative": final["userNarrative"],
47
- "coverageFigure": figure,
48
29
  "releaseAllowed": release_handoff_allowed(data),
49
30
  "evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
50
31
  }
@@ -52,6 +33,6 @@ def build_final_verification_view(data: dict) -> HumanReportView:
52
33
  "final-verification",
53
34
  "html/tasks/final-verification.template.html",
54
35
  context,
55
- (figure,),
36
+ (),
56
37
  anchored_fields=ANCHORED_FIELDS,
57
38
  )
@@ -18,16 +18,31 @@ def _safe_http_url(value: str) -> str:
18
18
  return value if parsed.scheme in {"http", "https"} and parsed.netloc else ""
19
19
 
20
20
 
21
+ def _pr_summary(outcomes: list) -> str:
22
+ """PR 은 stage 당 하나라 결과도 여러 개다. 타임라인 한 칸에는 종류별 건수로
23
+ 접어 넣는다 — 대표 하나를 고르면 나머지 stage 의 결과가 사라진다."""
24
+ if not outcomes:
25
+ return "no-action"
26
+ counts: dict[str, int] = {}
27
+ for row in outcomes:
28
+ kind = row.get("kind", "")
29
+ counts[kind] = counts.get(kind, 0) + 1
30
+ return ", ".join(f"{kind} {n}" for kind, n in sorted(counts.items()))
31
+
32
+
21
33
  def _timeline_nodes(handoff: dict) -> tuple[VisualNode, ...]:
22
34
  selection = handoff["userSelections"]["h1"]
23
35
  branch = handoff["featureBranchState"]["branchName"]
24
36
  conflict = handoff["mergeConflictProbe"]["kind"]
25
- pr = handoff["pullRequestOutcome"]["kind"]
37
+ outcomes = handoff["pullRequestOutcomes"]
38
+ kinds = {row.get("kind", "") for row in outcomes}
39
+ status = kinds.pop() if len(kinds) == 1 else "mixed"
26
40
  return (
27
41
  VisualNode("selection", selection, "handoff", "completed", "User-selected handoff"),
28
42
  VisualNode("branch", branch, "handoff", "completed", "Feature branch state"),
29
43
  VisualNode("conflict", conflict, "handoff", conflict, "Merge conflict probe"),
30
- VisualNode("pull-request", pr, "handoff", pr, "Pull request outcome"),
44
+ VisualNode("pull-request", _pr_summary(outcomes), "handoff", status,
45
+ "Pull request outcome, one per stage"),
31
46
  )
32
47
 
33
48
 
@@ -41,7 +56,10 @@ def build_release_handoff_view(data: dict) -> HumanReportView:
41
56
  "handoff": handoff,
42
57
  "narrative": handoff["userNarrative"],
43
58
  "handoffFigure": figure,
44
- "prUrl": _safe_http_url(handoff["pullRequestOutcome"].get("url", "")),
59
+ "prOutcomes": [
60
+ {**row, "url": _safe_http_url(row.get("url", ""))}
61
+ for row in handoff["pullRequestOutcomes"]
62
+ ],
45
63
  "evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
46
64
  }
47
65
  return HumanReportView(
@@ -356,11 +356,6 @@ def matrix_figure(*, items: Iterable[VisualNode], title: str, x_label: str, y_la
356
356
  return replace(dependency_figure(nodes=nodes, edges=(), title=title), kind="matrix", figure_id="matrix", summary=summary)
357
357
 
358
358
 
359
- def coverage_figure(*, rows: Iterable[VisualNode], title: str) -> FigureModel:
360
- nodes = tuple(rows)
361
- return replace(dependency_figure(nodes=nodes, edges=(), title=title), kind="coverage", figure_id="coverage")
362
-
363
-
364
359
  def timeline_figure(*, events: Iterable[VisualNode], title: str) -> FigureModel:
365
360
  nodes = tuple(events)
366
361
  edges = tuple(VisualEdge(nodes[i].id, nodes[i + 1].id, "next", "sequence") for i in range(len(nodes) - 1))