okstra 0.202.0 → 0.205.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 (263) hide show
  1. package/README.md +7 -6
  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/memory/memory.mjs +41 -8
  7. package/dist/commands/memory/memory.mjs.map +1 -1
  8. package/dist/lib/install-assets.mjs +3 -0
  9. package/dist/lib/install-assets.mjs.map +1 -1
  10. package/dist/lib/runtime-manifest.mjs +2 -1
  11. package/dist/lib/runtime-manifest.mjs.map +1 -1
  12. package/dist/lib/types.d.mts +2 -1
  13. package/docs/architecture/storage-model.md +14 -11
  14. package/docs/architecture.md +26 -20
  15. package/docs/cli.md +15 -12
  16. package/docs/contributor-change-matrix.md +3 -2
  17. package/docs/performance-improvement-plan-v2.md +3 -9
  18. package/docs/project-structure-overview.md +39 -11
  19. package/docs/task-process/README.md +1 -1
  20. package/docs/task-process/common-flow.md +1 -1
  21. package/docs/task-process/final-verification.md +3 -1
  22. package/docs/task-process/implementation-option-selection.md +1 -1
  23. package/docs/task-process/implementation.md +1 -1
  24. package/docs/task-process/release-handoff.md +36 -39
  25. package/package.json +1 -2
  26. package/runtime/BUILD.json +2 -2
  27. package/runtime/agents/common.json +28 -0
  28. package/runtime/agents/operations/code-review.json +6 -0
  29. package/runtime/agents/operations/report-translation.json +6 -0
  30. package/runtime/agents/operations/schedule-verification.json +6 -0
  31. package/runtime/agents/roles/analyser.json +18 -0
  32. package/runtime/agents/roles/critic.json +18 -0
  33. package/runtime/agents/roles/designer.json +18 -0
  34. package/runtime/agents/roles/implementer.json +20 -0
  35. package/runtime/agents/roles/leader.json +20 -0
  36. package/runtime/agents/roles/planner.json +18 -0
  37. package/runtime/agents/roles/report-writer.json +19 -0
  38. package/runtime/agents/roles/translator.json +19 -0
  39. package/runtime/agents/roles/verifier.json +18 -0
  40. package/runtime/bin/lib/okstra/usage.sh +5 -5
  41. package/runtime/prompts/duties/acceptance-critic.json +32 -0
  42. package/runtime/prompts/duties/acceptance-verifier.json +32 -0
  43. package/runtime/prompts/duties/analysis-worker.json +32 -0
  44. package/runtime/prompts/duties/code-reviewer.json +32 -0
  45. package/runtime/prompts/duties/diagnosis-worker.json +32 -0
  46. package/runtime/prompts/duties/direction-selection-worker.json +32 -0
  47. package/runtime/prompts/duties/discovery-worker.json +32 -0
  48. package/runtime/prompts/duties/implementation-executor.json +32 -0
  49. package/runtime/prompts/duties/implementation-verifier.json +32 -0
  50. package/runtime/prompts/duties/lead.json +32 -0
  51. package/runtime/prompts/duties/planning-worker.json +36 -0
  52. package/runtime/prompts/duties/report-writer.json +32 -0
  53. package/runtime/prompts/duties/reverification-worker.json +32 -0
  54. package/runtime/prompts/duties/schedule-verifier.json +32 -0
  55. package/runtime/prompts/duties/scope-critic.json +32 -0
  56. package/runtime/prompts/duties/technical-verification-worker.json +32 -0
  57. package/runtime/prompts/duties/translator.json +32 -0
  58. package/runtime/prompts/launch.template.md +2 -1
  59. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  60. package/runtime/prompts/lead/convergence.md +4 -4
  61. package/runtime/prompts/lead/okstra-lead-contract.md +115 -6
  62. package/runtime/prompts/lead/plan-body-verification.md +6 -6
  63. package/runtime/prompts/lead/report-writer.md +3 -3
  64. package/runtime/prompts/profiles/_common-contract.md +2 -2
  65. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  66. package/runtime/prompts/profiles/_implementation-executor.md +4 -1
  67. package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
  68. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  69. package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
  70. package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
  71. package/runtime/prompts/profiles/error-analysis.json +39 -0
  72. package/runtime/prompts/profiles/error-analysis.md +0 -25
  73. package/runtime/prompts/profiles/feature-analysis.json +31 -0
  74. package/runtime/prompts/profiles/feature-analysis.md +0 -20
  75. package/runtime/prompts/profiles/final-verification.json +30 -0
  76. package/runtime/prompts/profiles/final-verification.md +4 -23
  77. package/runtime/prompts/profiles/forbidden-actions.json +4 -3
  78. package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
  79. package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
  80. package/runtime/prompts/profiles/implementation-planning.json +40 -0
  81. package/runtime/prompts/profiles/implementation-planning.md +4 -29
  82. package/runtime/prompts/profiles/implementation.json +30 -0
  83. package/runtime/prompts/profiles/implementation.md +1 -20
  84. package/runtime/prompts/profiles/improvement-discovery.json +31 -0
  85. package/runtime/prompts/profiles/improvement-discovery.md +0 -20
  86. package/runtime/prompts/profiles/project-analysis.json +31 -0
  87. package/runtime/prompts/profiles/project-analysis.md +0 -20
  88. package/runtime/prompts/profiles/release-handoff.json +5 -0
  89. package/runtime/prompts/profiles/release-handoff.md +74 -74
  90. package/runtime/prompts/profiles/requirements-discovery.json +39 -0
  91. package/runtime/prompts/profiles/requirements-discovery.md +0 -25
  92. package/runtime/prompts/profiles/technical-verification.json +39 -0
  93. package/runtime/prompts/profiles/technical-verification.md +0 -25
  94. package/runtime/prompts/wizard/prompts.ko.json +14 -18
  95. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
  96. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
  97. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
  98. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
  99. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
  100. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
  101. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
  102. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
  103. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
  104. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
  105. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
  106. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
  107. package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
  108. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
  109. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
  110. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
  111. package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
  112. package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
  113. package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
  114. package/runtime/python/okstra_ctl/cmux.py +69 -20
  115. package/runtime/python/okstra_ctl/code_review_target.py +16 -8
  116. package/runtime/python/okstra_ctl/conformance.py +43 -0
  117. package/runtime/python/okstra_ctl/consumers.py +23 -8
  118. package/runtime/python/okstra_ctl/container.py +31 -8
  119. package/runtime/python/okstra_ctl/context_cost.py +11 -15
  120. package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
  121. package/runtime/python/okstra_ctl/convergence_engine.py +38 -0
  122. package/runtime/python/okstra_ctl/convergence_provenance.py +7 -1
  123. package/runtime/python/okstra_ctl/design_prep.py +34 -1
  124. package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
  125. package/runtime/python/okstra_ctl/domain/host.py +5 -0
  126. package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
  127. package/runtime/python/okstra_ctl/error_report.py +4 -3
  128. package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
  129. package/runtime/python/okstra_ctl/handoff.py +384 -286
  130. package/runtime/python/okstra_ctl/handoff_verification.py +25 -6
  131. package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
  132. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
  133. package/runtime/python/okstra_ctl/lead_progress.py +1 -1
  134. package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
  135. package/runtime/python/okstra_ctl/manager_cli.py +92 -4
  136. package/runtime/python/okstra_ctl/manager_launch.py +1 -1
  137. package/runtime/python/okstra_ctl/manager_paths.py +14 -3
  138. package/runtime/python/okstra_ctl/manager_store.py +210 -3
  139. package/runtime/python/okstra_ctl/manager_sync.py +4 -1
  140. package/runtime/python/okstra_ctl/manager_view.py +2 -1
  141. package/runtime/python/okstra_ctl/model_discovery.py +30 -0
  142. package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
  143. package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
  144. package/runtime/python/okstra_ctl/models.py +1 -1
  145. package/runtime/python/okstra_ctl/next_phase.py +16 -6
  146. package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
  147. package/runtime/python/okstra_ctl/option_comparison.py +168 -0
  148. package/runtime/python/okstra_ctl/path_hints.py +9 -0
  149. package/runtime/python/okstra_ctl/paths.py +3 -0
  150. package/runtime/python/okstra_ctl/profile_show.py +42 -1
  151. package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
  152. package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
  153. package/runtime/python/okstra_ctl/render.py +79 -0
  154. package/runtime/python/okstra_ctl/report_contract.py +1 -1
  155. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
  156. package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
  157. package/runtime/python/okstra_ctl/report_translation.py +2 -1
  158. package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
  159. package/runtime/python/okstra_ctl/role_requirements.py +142 -129
  160. package/runtime/python/okstra_ctl/rollup.py +3 -1
  161. package/runtime/python/okstra_ctl/run.py +76 -29
  162. package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
  163. package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
  164. package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
  165. package/runtime/python/okstra_ctl/stage_map.py +16 -2
  166. package/runtime/python/okstra_ctl/stage_targets.py +209 -43
  167. package/runtime/python/okstra_ctl/team.py +22 -13
  168. package/runtime/python/okstra_ctl/time_report.py +2 -1
  169. package/runtime/python/okstra_ctl/usage_report.py +3 -1
  170. package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
  171. package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
  172. package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
  173. package/runtime/python/okstra_ctl/wizard/state.py +3 -5
  174. package/runtime/python/okstra_ctl/wizard/steps_plan.py +12 -21
  175. package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
  176. package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
  177. package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
  178. package/runtime/python/okstra_ctl/workflow.py +1 -1
  179. package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
  180. package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
  181. package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
  182. package/runtime/python/okstra_token_usage/pricing.py +6 -4
  183. package/runtime/schemas/agent-common-v1.schema.json +34 -0
  184. package/runtime/schemas/agent-duty-v1.schema.json +38 -0
  185. package/runtime/schemas/agent-operation-v1.schema.json +11 -0
  186. package/runtime/schemas/agent-profile-v1.schema.json +46 -0
  187. package/runtime/schemas/agent-role-v1.schema.json +29 -0
  188. package/runtime/schemas/final-report-v2.0.schema.json +118 -97
  189. package/runtime/schemas/final-report-v3.0.schema.json +118 -97
  190. package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
  191. package/runtime/skills/okstra-chat/SKILL.md +2 -2
  192. package/runtime/skills/okstra-code-review/SKILL.md +23 -9
  193. package/runtime/skills/okstra-container-build/SKILL.md +10 -10
  194. package/runtime/skills/okstra-inspect/SKILL.md +1 -1
  195. package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
  196. package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
  197. package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
  198. package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
  199. package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
  200. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  201. package/runtime/skills/okstra-inspect/facets/status.md +4 -3
  202. package/runtime/skills/okstra-inspect/facets/time.md +11 -10
  203. package/runtime/skills/okstra-manager/SKILL.md +18 -2
  204. package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
  205. package/runtime/skills/okstra-rollup/SKILL.md +5 -5
  206. package/runtime/skills/okstra-run/SKILL.md +31 -12
  207. package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
  208. package/runtime/skills/okstra-setup/SKILL.md +12 -10
  209. package/runtime/skills/okstra-setup/references/project-config.md +7 -6
  210. package/runtime/skills/okstra-usage/SKILL.md +1 -1
  211. package/runtime/skills/okstra-user-response/SKILL.md +1 -1
  212. package/runtime/templates/manager/view.template.html +1 -0
  213. package/runtime/templates/report-writer-prompt-preamble.md +8 -0
  214. package/runtime/templates/reports/brief.template.md +14 -4
  215. package/runtime/templates/reports/html/i18n/en.json +5 -4
  216. package/runtime/templates/reports/html/i18n/ko.json +5 -4
  217. package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
  218. package/runtime/templates/reports/i18n/en.json +1 -1
  219. package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
  220. package/runtime/templates/reports/release-handoff-input.template.md +6 -4
  221. package/runtime/templates/translator-prompt-preamble.md +36 -0
  222. package/runtime/validators/checks/validate-assets-01.py +7 -8
  223. package/runtime/validators/validate-brief.py +70 -0
  224. package/runtime/validators/validate-implementation-plan-stages.py +2 -1
  225. package/runtime/validators/validate-run.py +72 -15
  226. package/runtime/validators/validate-schedule.py +9 -0
  227. package/docs/for-ai/README.md +0 -68
  228. package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
  229. package/docs/for-ai/skills/okstra-chat.md +0 -34
  230. package/docs/for-ai/skills/okstra-code-review.md +0 -57
  231. package/docs/for-ai/skills/okstra-container-build.md +0 -129
  232. package/docs/for-ai/skills/okstra-inspect.md +0 -262
  233. package/docs/for-ai/skills/okstra-manager.md +0 -86
  234. package/docs/for-ai/skills/okstra-memory.md +0 -126
  235. package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
  236. package/docs/for-ai/skills/okstra-rollup.md +0 -114
  237. package/docs/for-ai/skills/okstra-run.md +0 -250
  238. package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
  239. package/docs/for-ai/skills/okstra-setup.md +0 -167
  240. package/docs/for-ai/skills/okstra-usage.md +0 -29
  241. package/docs/for-ai/skills/okstra-user-response.md +0 -72
  242. package/runtime/agents/workers/claude-worker.md +0 -128
  243. package/runtime/agents/workers/report-writer-worker.md +0 -37
  244. package/runtime/agents/workers/translator-worker.md +0 -63
  245. package/runtime/prompts/duties/acceptance-critic.md +0 -44
  246. package/runtime/prompts/duties/acceptance-verifier.md +0 -44
  247. package/runtime/prompts/duties/analysis-worker.md +0 -44
  248. package/runtime/prompts/duties/code-reviewer.md +0 -44
  249. package/runtime/prompts/duties/common.md +0 -39
  250. package/runtime/prompts/duties/diagnosis-worker.md +0 -44
  251. package/runtime/prompts/duties/direction-selection-worker.md +0 -44
  252. package/runtime/prompts/duties/discovery-worker.md +0 -44
  253. package/runtime/prompts/duties/implementation-executor.md +0 -44
  254. package/runtime/prompts/duties/implementation-verifier.md +0 -44
  255. package/runtime/prompts/duties/lead.md +0 -44
  256. package/runtime/prompts/duties/planning-worker.md +0 -52
  257. package/runtime/prompts/duties/report-writer.md +0 -44
  258. package/runtime/prompts/duties/reverification-worker.md +0 -44
  259. package/runtime/prompts/duties/schedule-verifier.md +0 -44
  260. package/runtime/prompts/duties/scope-critic.md +0 -44
  261. package/runtime/prompts/duties/technical-verification-worker.md +0 -44
  262. package/runtime/prompts/duties/translator.md +0 -44
  263. package/runtime/python/okstra_ctl/pane_title.py +0 -154
@@ -0,0 +1,168 @@
1
+ """구현 방향 비교 자료 — 선택 리포트 기록 하나를 채팅용 사실 묶음으로 투영한다.
2
+
3
+ 사용자가 방향을 고르기 전에 비교 자료를 청하면 리드는 점수표·투표표·충족
4
+ 요약을 보여 준다. 그 숫자는 전부 `implementationOptionSelection` 기록에 있고
5
+ `weightedScore` 는 발행 시점에 재계산 값과 대조된다
6
+ (`implementation_options.py` `_validate_option_scores`). 리드가 JSON 을 손으로
7
+ 읽고 가중 점수를 다시 계산하던 자리를 이 명령이 대신한다. 서술(도입 문장,
8
+ 방향별 요지)은 리드가 `narrative` 와 각 방향의 근거 필드로 쓴다.
9
+
10
+ 읽기 전용이다. 어떤 파일도 쓰지 않는다.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from collections.abc import Mapping, Sequence
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from okstra_ctl import next_phase
22
+ from okstra_ctl.final_report_paths import final_report_data_path
23
+ from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
24
+
25
+
26
+ class OptionComparisonError(ValueError):
27
+ """비교 자료를 만들 수 없는 기록."""
28
+
29
+
30
+ def _rows(value: object) -> list[Mapping[str, Any]]:
31
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
32
+ return []
33
+ return [row for row in value if isinstance(row, Mapping)]
34
+
35
+
36
+ def _text(value: object) -> str:
37
+ return value.strip() if isinstance(value, str) else ""
38
+
39
+
40
+ def _count(value: object) -> int:
41
+ return len(value) if isinstance(value, Sequence) and not isinstance(value, str) else 0
42
+
43
+
44
+ def _option(
45
+ option: Mapping[str, Any], recommended_id: str
46
+ ) -> dict[str, Any]:
47
+ coverage = option.get("coverageSummary")
48
+ coverage = coverage if isinstance(coverage, Mapping) else {}
49
+ scores = _rows(option.get("criterionScores"))
50
+ return {
51
+ "id": _text(option.get("id")),
52
+ "name": _text(option.get("name")),
53
+ "recommended": _text(option.get("id")) == recommended_id,
54
+ "goal": _text(option.get("goal")),
55
+ "coreMechanism": _text(option.get("coreMechanism")),
56
+ # 발행 검증이 재계산 값과 같음을 보장한 기록값이다. 다시 계산하지 않는다.
57
+ "weightedScore": option.get("weightedScore"),
58
+ "scores": {_text(row.get("criterion")): row.get("score") for row in scores},
59
+ "scoreRationales": {
60
+ _text(row.get("criterion")): _text(row.get("rationale")) for row in scores
61
+ },
62
+ "coverage": {
63
+ "verdict": _text(coverage.get("coverageVerdict")),
64
+ "coveredCount": coverage.get("coveredCount"),
65
+ "totalCount": coverage.get("totalCount"),
66
+ "requirementIds": [
67
+ _text(row.get("requirementId"))
68
+ for row in _rows(option.get("requirementCoverage"))
69
+ if _text(row.get("requirementId"))
70
+ ],
71
+ },
72
+ "safetyBlockers": _count(option.get("safetyBlockers")),
73
+ "unresolvedFeasibilityFacts": _count(option.get("unresolvedFeasibilityFacts")),
74
+ "votes": {
75
+ _text(vote.get("worker")): {
76
+ "verdict": _text(vote.get("verdict")),
77
+ "rationale": _text(vote.get("rationale")),
78
+ "counterevidence": _text(vote.get("counterevidence")),
79
+ }
80
+ for vote in _rows(option.get("feasibilityVotes"))
81
+ if _text(vote.get("worker"))
82
+ },
83
+ }
84
+
85
+
86
+ def build_comparison(report_data: Mapping[str, Any]) -> dict[str, Any]:
87
+ """선택 리포트 기록 → 비교 자료 사실 묶음."""
88
+ selection = report_data.get("implementationOptionSelection")
89
+ if not isinstance(selection, Mapping):
90
+ raise OptionComparisonError(
91
+ "report has no implementationOptionSelection block; "
92
+ "pass an implementation-option-selection report record"
93
+ )
94
+ recommended_id = _text(selection.get("recommendedOptionId"))
95
+ options = [
96
+ _option(row, recommended_id) for row in _rows(selection.get("rankedOptions"))
97
+ ]
98
+ if not options:
99
+ raise OptionComparisonError("report ranks no direction; there is nothing to compare")
100
+ # 투표 열 순서는 처음 등장한 순서다 — 방향마다 같은 분석자 집합이 투표한다.
101
+ workers: list[str] = []
102
+ for option in options:
103
+ for worker in option["votes"]:
104
+ if worker not in workers:
105
+ workers.append(worker)
106
+ narrative = selection.get("userNarrative")
107
+ narrative = narrative if isinstance(narrative, Mapping) else {}
108
+ return {
109
+ "mode": _text(selection.get("mode")),
110
+ "routing": _text(selection.get("routing")),
111
+ "recommendedOptionId": recommended_id,
112
+ "criteria": [
113
+ {"criterion": _text(row.get("criterion")), "weight": row.get("weight")}
114
+ for row in _rows(selection.get("evaluationCriteria"))
115
+ ],
116
+ "workers": workers,
117
+ "options": options,
118
+ "narrative": {
119
+ key: _text(block.get("text"))
120
+ for key, block in narrative.items()
121
+ if isinstance(block, Mapping) and _text(block.get("text"))
122
+ },
123
+ # "남은 절차" 문단의 근거. 다음 phase 포인터와 같은 문장이다.
124
+ "nextStep": next_phase.project(report_data)["rationale"],
125
+ }
126
+
127
+
128
+ def _load(report: Path) -> Mapping[str, Any]:
129
+ data_path = final_report_data_path(report)
130
+ try:
131
+ data = load_owned_object(data_path, artifact="implementation option data")
132
+ except (OSError, JsonBoundaryError) as exc:
133
+ raise OptionComparisonError(f"cannot read report record {data_path}: {exc}") from exc
134
+ return data
135
+
136
+
137
+ def build_parser() -> argparse.ArgumentParser:
138
+ parser = argparse.ArgumentParser(
139
+ prog="okstra option-comparison",
140
+ description=(
141
+ "Print the comparison facts of an implementation-option-selection "
142
+ "report as JSON: criteria and weights, per-direction scores and "
143
+ "recorded weighted score, requirement coverage, blocker counts, "
144
+ "per-worker feasibility votes, the report narrative, and the next step."
145
+ ),
146
+ )
147
+ parser.add_argument(
148
+ "--report",
149
+ required=True,
150
+ help="final-report-implementation-option-selection-<seq>.data.json (a .md sibling is accepted)",
151
+ )
152
+ return parser
153
+
154
+
155
+ def main(argv: list[str] | None = None) -> int:
156
+ args = build_parser().parse_args(argv)
157
+ try:
158
+ payload = build_comparison(_load(Path(args.report)))
159
+ except OptionComparisonError as exc:
160
+ print(f"option-comparison: {exc}", file=sys.stderr)
161
+ return 2
162
+ json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
163
+ sys.stdout.write("\n")
164
+ return 0
165
+
166
+
167
+ if __name__ == "__main__":
168
+ sys.exit(main())
@@ -64,6 +64,7 @@ def compact_active_run_context(
64
64
  "run": {
65
65
  "stage": ctx.get("RUN_STAGE", ""),
66
66
  "fixRunCarry": ctx.get("FIX_RUN_CONTEXT", ""),
67
+ "hostSessionContract": ctx.get("HOST_SESSION_CONTRACT_PATH", ""),
67
68
  },
68
69
  "agentContract": dict(_mapping(payload.get("agentContract"))),
69
70
  "inputs": _compact_active_inputs(payload),
@@ -118,8 +119,13 @@ def hydrate_active_run_context(payload: Mapping[str, Any]) -> dict[str, Any]:
118
119
  "IMPLEMENTATION_WORKER_PREAMBLE_PATH", ""
119
120
  ),
120
121
  "report-writer": ctx.get("REPORT_WRITER_PREAMBLE_PATH", ""),
122
+ "translator": ctx.get("TRANSLATOR_PREAMBLE_PATH", ""),
121
123
  },
122
124
  "workerErrorContractPath": ctx.get("WORKER_ERROR_CONTRACT_PATH", ""),
125
+ "hostSessionContractPath": (
126
+ str(_mapping(payload.get("run")).get("hostSessionContract", ""))
127
+ or ctx.get("HOST_SESSION_CONTRACT_PATH", "")
128
+ ),
123
129
  },
124
130
  "executorWorktree": dict(_mapping(payload.get("executorWorktree"))),
125
131
  "verificationTarget": dict(_mapping(payload.get("verificationTarget"))),
@@ -509,6 +515,9 @@ def _runtime_fields(paths: Mapping[str, Any]) -> dict[str, str]:
509
515
  "REPORT_WRITER_PREAMBLE_PATH": str(
510
516
  runtime_home / "templates" / "report-writer-prompt-preamble.md"
511
517
  ),
518
+ "TRANSLATOR_PREAMBLE_PATH": str(
519
+ runtime_home / "templates" / "translator-prompt-preamble.md"
520
+ ),
512
521
  "WORKER_ERROR_CONTRACT_PATH": str(
513
522
  runtime_home / "templates" / "worker-error-contract.md"
514
523
  ),
@@ -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
  ),
@@ -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:
@@ -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
 
@@ -2002,6 +2010,35 @@ def _initialize_report_ledgers(ctx: Mapping[str, Any], manifest: Mapping[str, An
2002
2010
  activity_path = Path(activity_value)
2003
2011
  if activity_value and not activity_path.is_file():
2004
2012
  _write_text(activity_path, "")
2013
+ _initialize_lead_only_convergence(ctx, manifest)
2014
+
2015
+
2016
+ def _initialize_lead_only_convergence(
2017
+ ctx: Mapping[str, Any], manifest: Mapping[str, Any],
2018
+ ) -> None:
2019
+ """워커를 띄우지 않는 phase 의 수렴 상태.
2020
+
2021
+ 리포트 조립은 모든 리포트에 수렴 입력을 요구하는데(ADR-0020), 프로필이
2022
+ "convergence 를 돌지 않는다" 고 선언한 phase 에는 그 파일을 만드는 주체가
2023
+ 없었다. 리드가 매 run 마다 seed → plan-round → finalize 를 손으로 돌려 빈
2024
+ 상태를 만들어 왔다(실측: dev-10860 release-handoff 001·002 둘 다). 나머지
2025
+ 입력과 같은 자리에서 같은 조건으로 만든다 — 없을 때만.
2026
+ """
2027
+ from .convergence_engine import lead_only_final_state
2028
+ from .convergence_store import write_final_state_atomic
2029
+
2030
+ if _resolve_workers(dict(ctx)):
2031
+ return
2032
+ value = str(ctx.get("CONVERGENCE_STATE_PATH") or "")
2033
+ path = Path(value)
2034
+ if not value or path.exists():
2035
+ return
2036
+ path.parent.mkdir(parents=True, exist_ok=True)
2037
+ write_final_state_atomic(
2038
+ path,
2039
+ lead_only_final_state(str(manifest.get("taskKey") or "")),
2040
+ migration=None,
2041
+ )
2005
2042
 
2006
2043
 
2007
2044
  _MODEL_ASSIGNMENT_KEYS = {
@@ -2019,6 +2056,9 @@ _V2_INVOCATION_IDENTITY_KEYS = {
2019
2056
  }
2020
2057
  _AGENT_CONTRACT_KEYS = {
2021
2058
  "schemaVersion",
2059
+ # 이 run 이 동결한 계약의 형식. 없는 run 은 Markdown 직무를 동결했고, 그
2060
+ # 형식의 리더는 제거됐다.
2061
+ "contractFormatVersion",
2022
2062
  "dutyRootPath",
2023
2063
  "catalogDigest",
2024
2064
  "invocationReservationRootPath",
@@ -2610,9 +2650,15 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2610
2650
  report_writer_preamble = ctx.get("REPORT_WRITER_PREAMBLE_PATH") or str(
2611
2651
  runtime_templates / "report-writer-prompt-preamble.md"
2612
2652
  )
2653
+ translator_preamble = ctx.get("TRANSLATOR_PREAMBLE_PATH") or str(
2654
+ runtime_templates / "translator-prompt-preamble.md"
2655
+ )
2613
2656
  worker_error_contract = ctx.get("WORKER_ERROR_CONTRACT_PATH") or str(
2614
2657
  runtime_templates / "worker-error-contract.md"
2615
2658
  )
2659
+ host_session_contract = ctx.get(
2660
+ "HOST_SESSION_CONTRACT_PATH"
2661
+ ) or _host_session_contract_path(ctx)
2616
2662
  okstra_runtime_resources_block = (
2617
2663
  "## Okstra Runtime Resources\n"
2618
2664
  "\n"
@@ -2629,8 +2675,14 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2629
2675
  f"- Analysis worker preamble: `{analysis_preamble}`\n"
2630
2676
  f"- Implementation worker preamble: `{implementation_preamble}`\n"
2631
2677
  f"- Report writer preamble: `{report_writer_preamble}`\n"
2678
+ f"- Translator preamble: `{translator_preamble}`\n"
2679
+
2632
2680
  f"- Shared worker error contract: `{worker_error_contract}`"
2633
2681
  )
2682
+ if host_session_contract:
2683
+ okstra_runtime_resources_block += (
2684
+ f"\n- Host worker session contract: `{host_session_contract}`"
2685
+ )
2634
2686
 
2635
2687
  def fmt_assignment(role: str, model: str, execution: str) -> str:
2636
2688
  if execution and execution != model:
@@ -2814,6 +2866,23 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2814
2866
  ctx["EXECUTION_STATUS_TABLE_ROWS"] = "\n".join(execution_status_table_lines)
2815
2867
 
2816
2868
 
2869
+ def _host_session_contract_path(ctx: Mapping[str, object]) -> str:
2870
+ """이 run 의 호스트가 선언한 워커 세션 계약의 절대 경로. 없으면 빈 문자열.
2871
+
2872
+ 호스트를 못 찾는 것은 여기서 실패할 일이 아니다 — 계약이 없는 호스트와
2873
+ 같게 다루고, 헤더를 빼면 그 워커는 세션 규칙 없이 돈다. 호스트 해석 실패
2874
+ 자체는 이 함수를 부르기 전에 이미 run 준비를 막는다.
2875
+ """
2876
+ host_id = str(ctx.get("HOST_RUNTIME", "") or _lead_runtime(ctx) or "").strip()
2877
+ if not host_id:
2878
+ return ""
2879
+ try:
2880
+ descriptor = default_host_registry().resolve(host_id).descriptor
2881
+ except Exception:
2882
+ return ""
2883
+ return descriptor.worker_session_contract or ""
2884
+
2885
+
2817
2886
  def apply_lead_prompt_defaults(ctx: dict) -> None:
2818
2887
  """Apply default values for optional lead-prompt ctx fields.
2819
2888
 
@@ -2853,10 +2922,20 @@ def apply_lead_prompt_defaults(ctx: dict) -> None:
2853
2922
  "REPORT_WRITER_PREAMBLE_PATH",
2854
2923
  str(runtime_home / "templates" / "report-writer-prompt-preamble.md"),
2855
2924
  )
2925
+ ctx.setdefault(
2926
+ "TRANSLATOR_PREAMBLE_PATH",
2927
+ str(runtime_home / "templates" / "translator-prompt-preamble.md"),
2928
+ )
2856
2929
  ctx.setdefault(
2857
2930
  "WORKER_ERROR_CONTRACT_PATH",
2858
2931
  str(runtime_home / "templates" / "worker-error-contract.md"),
2859
2932
  )
2933
+ # 호스트가 선언한 워커 세션 계약. 선언하지 않은 호스트는 빈 문자열이고,
2934
+ # 그러면 헤더가 방출되지 않는다.
2935
+ ctx.setdefault(
2936
+ "HOST_SESSION_CONTRACT_PATH",
2937
+ _host_session_contract_path(ctx),
2938
+ )
2860
2939
  # Lead resource paths the launch template references directly. paths.py
2861
2940
  # seeds the production values; setdefault backfills callers that render the
2862
2941
  # 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
  }
@@ -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(