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
@@ -96,6 +96,7 @@ from .initial_prompt_materialization import (
96
96
  InitialPromptMaterializationRequest,
97
97
  InitialPromptWorkerRequest,
98
98
  PromptDeliveryMode,
99
+ declared_delivery_mode,
99
100
  materialize_initial_prompts,
100
101
  )
101
102
  from .agent.invocation import AgentInvocationError, materialize_retry_invocation
@@ -183,6 +184,9 @@ def revalidate_role_execution(
183
184
  interaction_surface="role-execution-dispatch",
184
185
  ),
185
186
  environment=host,
187
+ # 이 run 은 이 모델을 이미 골랐다. 지금 고를 수 있는지가 아니라 그
188
+ # 배정이 그대로 실행 가능한지를 묻는 자리다.
189
+ require_selectable=False,
186
190
  )
187
191
  binding = assignment.binding
188
192
  if binding is None:
@@ -261,6 +265,7 @@ class WorkerHandle:
261
265
  completed_process: subprocess.CompletedProcess[str] | None
262
266
  status_sidecar_path: Path | None
263
267
  degraded_from: str
268
+ degraded_reason: str = ""
264
269
  running_process: subprocess.Popen[str] | None = None
265
270
  mutation_snapshot: MutationSnapshot | None = None
266
271
  mutation_policies: tuple[WritePolicy, ...] = ()
@@ -1439,31 +1444,11 @@ def _invocation_job_fields(
1439
1444
  def _initial_prompt_delivery_mode(
1440
1445
  manifest: Mapping[str, Any],
1441
1446
  ) -> PromptDeliveryMode:
1442
- """이 run 의 어댑터가 선언한 초기 프롬프트 배송 방식.
1443
-
1444
- 선언은 `leadAdapter.initialPromptDeliveryMode` 에 있고, 그 값은 render 가
1445
- 호스트 descriptor(어댑터가 워커 디스패치를 가져갔으면 그 어댑터)에서
1446
- 옮겨 적은 것이다. 선언이 없는 매니페스트는 이 필드가 생기기 전에 준비된
1447
- run 이므로 그때의 동작인 `eager-include` 로 간다. 아는 값이 아니면 멈춘다 —
1448
- 프롬프트를 발행하고 나면 그 프롬프트는 불변이라, 잘못 고른 배송 방식은
1449
- 나중에 고칠 수 없다.
1450
- """
1451
- adapter = manifest.get("leadAdapter")
1452
- declared = (
1453
- _string_value(adapter.get("initialPromptDeliveryMode"))
1454
- if isinstance(adapter, Mapping)
1455
- else ""
1456
- )
1457
- if not declared:
1458
- return PromptDeliveryMode.EAGER_INCLUDE
1447
+ """이 run 의 초기 프롬프트 배송 방식. 판정은 `declared_delivery_mode` 가 한다."""
1459
1448
  try:
1460
- return PromptDeliveryMode(declared)
1461
- except ValueError:
1462
- allowed = ", ".join(mode.value for mode in PromptDeliveryMode)
1463
- raise DispatchError(
1464
- "leadAdapter.initialPromptDeliveryMode is not a delivery mode: "
1465
- f"{declared!r} (allowed: {allowed})"
1466
- ) from None
1449
+ return declared_delivery_mode(manifest)
1450
+ except InitialPromptMaterializationError as exc:
1451
+ raise DispatchError(str(exc)) from None
1467
1452
 
1468
1453
 
1469
1454
  def _materialize_roster_prompts(
@@ -2034,8 +2019,20 @@ def _start_job(plan: DispatchPlan, job: WorkerJob) -> WorkerHandle:
2034
2019
  raise DispatchError(str(exc)) from exc
2035
2020
  degraded_from = job.backend if runtime.surface != job.backend else ""
2036
2021
  settled = replace(job, backend=runtime.surface) if degraded_from else job
2022
+ if degraded_from:
2023
+ print(
2024
+ f"DEGRADED {job.role}: {degraded_from} -> {runtime.surface} "
2025
+ f"({runtime.fallback_reason or 'no reason reported'})",
2026
+ flush=True,
2027
+ )
2037
2028
  completed = None
2038
- if runtime.waitable:
2029
+ # 강등된 job 은 여기서 기다리지 않는다. `dispatch_plan` 은 wait=True 에서
2030
+ # pane job 을 거절하므로(같은 함수의 pane_backends 검사) 강등은 비차단 배치
2031
+ # 에서만 일어난다. 그 배치에서 기다리면 라운드가 직렬화되고 — 실측 2026-09-21,
2032
+ # 워커 3명이 병렬 대신 37분 — dispatch 행이 워커가 끝난 뒤에야 써져
2033
+ # `okstra worker-liveness` 가 실행 중인 워커를 모른다. 정산은 상태 사이드카를
2034
+ # 읽는 `await_dispatches` 가 맡는다.
2035
+ if runtime.waitable and not degraded_from:
2039
2036
  from .adapters.runtime.assembly import port_for
2040
2037
  returncode = port_for(plan.runtime_chain, runtime.surface).wait(runtime)
2041
2038
  completed = subprocess.CompletedProcess(list(settled.command), returncode)
@@ -2046,6 +2043,7 @@ def _start_job(plan: DispatchPlan, job: WorkerJob) -> WorkerHandle:
2046
2043
  completed,
2047
2044
  status_path_for_prompt(settled.prompt_path),
2048
2045
  degraded_from,
2046
+ degraded_reason=runtime.fallback_reason,
2049
2047
  runtime_handle=runtime,
2050
2048
  started_at=started_at,
2051
2049
  )
@@ -2237,9 +2235,28 @@ def _retry_from_record(
2237
2235
  # 사유 한 줄에 실을 경로 개수. 전부 실으면 team-state 행이 부풀고, 하나도 안
2238
2236
  # 실으면 읽는 쪽이 다른 파일을 뒤져야 한다.
2239
2237
  _MUTATION_REASON_PATH_LIMIT = 5
2238
+ # 사유는 team-state 의 한 행이다. 래퍼 실패 문장은 길 수 있으므로 자른다.
2239
+ _MUTATION_REASON_FAILURE_LIMIT = 200
2240
+
2241
+
2242
+ def _wrapper_failure_text(outcome: WorkerOutcome | None) -> str:
2243
+ """워커 래퍼가 남긴 실패 문장. 없으면 빈 문자열.
2244
+
2245
+ `mutation-present-unresolved` 는 "시도가 실패했는데 소스가 바뀌어 있다" 는
2246
+ 뜻이고, 왜 실패했는지는 상태 사이드카에만 있었다. 읽는 쪽은 상태 이름만 보고
2247
+ 소스를 뒤져야 했다 — 실측 2026-09-23: 두 세션이 같은 자리에서 그렇게
2248
+ 진단했고, 원인은 카탈로그에 없는 served 모델이었다.
2249
+ """
2250
+ if outcome is None or outcome.status_sidecar_path is None:
2251
+ return ""
2252
+ status = read_wrapper_status(outcome.status_sidecar_path)
2253
+ if status is None:
2254
+ return ""
2255
+ failure = _string_value(status.raw.get("failure"))
2256
+ return failure[:_MUTATION_REASON_FAILURE_LIMIT]
2240
2257
 
2241
2258
 
2242
- def _mutation_failure_reason(mutation) -> str:
2259
+ def _mutation_failure_reason(mutation, outcome: WorkerOutcome | None = None) -> str:
2243
2260
  """왜 실패했는지와 무엇이 바뀌었는지를 한 줄에 담는다.
2244
2261
 
2245
2262
  종전에는 디스패치 행에 `readonly source changed` 만 남고 바뀐 경로는
@@ -2249,6 +2266,9 @@ def _mutation_failure_reason(mutation) -> str:
2249
2266
  사유 옆에 놓는다.
2250
2267
  """
2251
2268
  reason = "; ".join(mutation.violations) or mutation.status
2269
+ failure = _wrapper_failure_text(outcome)
2270
+ if failure:
2271
+ reason = f"{reason}: {failure}"
2252
2272
  paths = list(mutation.changed_paths)
2253
2273
  if not paths:
2254
2274
  return reason
@@ -2273,7 +2293,7 @@ def _finish_attempt(
2273
2293
  "contract-failed-unattributed",
2274
2294
  "mutation-present-unresolved",
2275
2295
  }:
2276
- reason = _mutation_failure_reason(mutation)
2296
+ reason = _mutation_failure_reason(mutation, outcome)
2277
2297
  _update_dispatch_status(
2278
2298
  plan.team_state_path, job, attempt, "error", reason
2279
2299
  )
@@ -2913,6 +2933,7 @@ def _record_dispatch(
2913
2933
  handle.pane_id,
2914
2934
  handle.degraded_from,
2915
2935
  reason,
2936
+ degraded_reason=handle.degraded_reason,
2916
2937
  mutation_snapshot_path=(
2917
2938
  _mutation_snapshot_path(handle.job)
2918
2939
  if handle.mutation_snapshot is not None
@@ -2932,6 +2953,7 @@ def _dispatch_record(
2932
2953
  degraded_from: str,
2933
2954
  reason: str = "",
2934
2955
  *,
2956
+ degraded_reason: str = "",
2935
2957
  mutation_snapshot_path: Path | None = None,
2936
2958
  write_policy_digest: str = "",
2937
2959
  write_enforcement: WriteEnforcement | None = None,
@@ -2958,6 +2980,9 @@ def _dispatch_record(
2958
2980
  ),
2959
2981
  "statusSidecarPath": str(status_path_for_prompt(job.prompt_path)),
2960
2982
  "degradedFrom": degraded_from,
2983
+ # `reason` 은 이 행의 정산 사유라 마감과 teardown 이 덮어쓴다. 강등 사유는
2984
+ # 디스패치 시점의 사실이므로 자기 칸을 쓴다.
2985
+ "degradedReason": degraded_reason,
2961
2986
  "reason": reason,
2962
2987
  "modelExecutionValue": job.model_execution_value,
2963
2988
  "wrapperPath": str(job.wrapper_path),
@@ -3186,6 +3211,7 @@ def _event_details(job: WorkerJob, handle: WorkerHandle) -> dict[str, Any]:
3186
3211
  "role": job.role,
3187
3212
  "paneId": handle.pane_id,
3188
3213
  "degradedFrom": handle.degraded_from,
3214
+ "degradedReason": handle.degraded_reason,
3189
3215
  }
3190
3216
  return details
3191
3217
 
@@ -51,6 +51,11 @@ class HostDescriptor:
51
51
  session_accounting: str
52
52
  has_claude_session: bool
53
53
  relay_contract: str
54
+ # 이 호스트 세션 안에서 도는 워커(runner=native-session)가 읽어야 하는
55
+ # 계약 파일의 절대 경로. 호스트가 그런 계약을 선언하지 않으면 빈 문자열이다.
56
+ # 호스트 전역 발견 경로에 설치되지 않는다 — 디스패치 프롬프트가 경로로
57
+ # 전달하고, 워커가 프리앰블과 같은 방식으로 연다.
58
+ worker_session_contract: str = ""
54
59
  # 이 호스트가 초기 워커 프롬프트를 어떻게 싣겠다고 선언하는가. 값은
55
60
  # `relay.md` 의 `initialPromptDeliveryMode` 행과 같아야 한다 — 그 표를
56
61
  # 읽고 dispatch 를 판단하는 것은 리드이고, 실제로 프롬프트를 만드는 것은
@@ -31,9 +31,19 @@ class WorkerSpawnRequest:
31
31
 
32
32
  @dataclass(frozen=True)
33
33
  class RuntimeHandle:
34
+ """An opened surface, and why it is not the one that was planned.
35
+
36
+ `fallback_reason` is empty when the planned surface opened. It carries the
37
+ last `SurfaceUnavailable` message when an earlier surface in the chain was
38
+ skipped, because that message exists nowhere else: the chain swallows the
39
+ exception it recovers from, and a degraded dispatch used to be recorded
40
+ with an empty reason.
41
+ """
42
+
34
43
  surface: str
35
44
  identifier: str
36
45
  waitable: bool
46
+ fallback_reason: str = ""
37
47
 
38
48
 
39
49
  @dataclass(frozen=True)
@@ -1,8 +1,8 @@
1
1
  """Read-side aggregator for okstra-run error logs.
2
2
 
3
3
  task 의 모든 run errors-<type>-<seq>.jsonl 을 모아 phase / worker / errorType
4
- 별로 집계하고 timestamp 누적 .md 리포트로 렌더한다. task 산출물을 mutate 하지
5
- 않는다.
4
+ 별로 집계하고 timestamp 누적 .md 리포트로 렌더한다. 리포트 외에 쓰는 것은
5
+ task 해석 중 `find_task_root` 가 끝난 implementation 결과를 기록하는 경우뿐이다.
6
6
  """
7
7
  from __future__ import annotations
8
8
 
@@ -236,7 +236,8 @@ _CLI_EPILOG = r"""Usage:
236
236
 
237
237
  Writes a timestamped markdown report under <task-root>/error-reports/ and
238
238
  prints a JSON summary (counts by errorType / source / phase / agent) to stdout.
239
- This is read-only against task state; it never mutates task artifacts.
239
+ Resolving a task may record a finished implementation outcome in its task
240
+ manifest, the task catalog, and consumers.jsonl; nothing else is written.
240
241
  """
241
242
 
242
243
 
@@ -213,7 +213,9 @@ def reserve_derived_role_invocation(
213
213
  raise ExecutionManifestError(
214
214
  f"source role execution is unknown: {source_role_execution_ref}"
215
215
  )
216
- derived, created = _derived_role_execution(manifest, source, role)
216
+ derived, created, derived_participant = _derived_role_execution(
217
+ manifest, source, role
218
+ )
217
219
  invocation = invocation_factory(derived)
218
220
  if not isinstance(invocation, Invocation):
219
221
  raise ExecutionManifestError("invocation factory must return Invocation")
@@ -244,14 +246,22 @@ def reserve_derived_role_invocation(
244
246
  _write_json_atomic(task_manifest_path, recovered_task_payload)
245
247
  return derived, existing_invocation
246
248
 
249
+ base = manifest
250
+ if created and derived_participant is not None:
251
+ base = _replace_manifest(
252
+ manifest,
253
+ participant_assignments=(
254
+ *manifest.participant_assignments,
255
+ derived_participant,
256
+ ),
257
+ )
247
258
  roles = (
248
- (*manifest.role_executions, derived)
249
- if created
250
- else manifest.role_executions
259
+ (*base.role_executions, derived) if created else base.role_executions
251
260
  )
252
- with_role = _replace_manifest(manifest, role_executions=roles)
261
+ with_role = _replace_manifest(base, role_executions=roles)
253
262
  if created:
254
- _validate_next_role_execution(manifest, derived)
263
+ # 새 참가자가 실린 뒤라야 참가자 참조 검사가 성립한다.
264
+ _validate_next_role_execution(base, derived)
255
265
  _validate_next_invocation(with_role, invocation)
256
266
  updated = _replace_manifest(
257
267
  with_role,
@@ -540,11 +550,13 @@ def _validate_roles(
540
550
  "sourceRoleExecutionRef must reference an earlier different "
541
551
  "role execution"
542
552
  )
543
- if source_role.get("participantRef") != participant_ref:
544
- raise ExecutionManifestError(
545
- "sourceRoleExecutionRef participant must match derived role "
546
- "participant"
547
- )
553
+ # 참가자는 더 이상 공유하지 않는다(ADR-0017). 파생을 파생이게 하는
554
+ # 것은 같은 모델로 같은 일을 다시 읽는다는 것이므로, 거기를 검사한다.
555
+ for key in ("provider", "modelRef", "modelId"):
556
+ if source_role.get(key) != row.get(key):
557
+ raise ExecutionManifestError(
558
+ f"derived role execution {key} must match its source"
559
+ )
548
560
  earlier_roles[role_ref] = row
549
561
  for role, values in ordinals.items():
550
562
  if sorted(values) != list(range(1, len(values) + 1)):
@@ -815,20 +827,29 @@ def _derived_role_execution(
815
827
  manifest: ExecutionManifest,
816
828
  source: RoleExecution,
817
829
  role: str,
818
- ) -> tuple[RoleExecution, bool]:
830
+ ) -> tuple[RoleExecution, bool, ParticipantAssignment | None]:
831
+ """The dynamic role slot derived from *source*, creating it when absent.
832
+
833
+ A created slot is its own participant. It copies the source's provider,
834
+ model and binding — that is what makes it the same model re-reading the
835
+ same work — but not the source's identity or session. Sharing the
836
+ participant made the derived slot indistinguishable from its source
837
+ wherever a participant is what gets attributed or resumed.
838
+ """
819
839
  if source.role == role:
820
- return source, False
840
+ return source, False, None
821
841
  existing = [
822
842
  item
823
843
  for item in manifest.role_executions
824
- if item.participant_ref == source.participant_ref and item.role == role
844
+ if item.source_role_execution_ref == source.role_execution_ref
845
+ and item.role == role
825
846
  ]
826
847
  if len(existing) > 1:
827
848
  raise ExecutionManifestError(
828
- f"participant has multiple {role} role executions: {source.participant_ref}"
849
+ f"source has multiple {role} role executions: {source.role_execution_ref}"
829
850
  )
830
851
  if existing:
831
- return existing[0], False
852
+ return existing[0], False, None
832
853
  ordinal = max(
833
854
  (item.ordinal for item in manifest.role_executions if item.role == role),
834
855
  default=0,
@@ -840,9 +861,10 @@ def _derived_role_execution(
840
861
  raise ExecutionManifestError(
841
862
  f"derived roleExecutionRef is already reserved: {role_ref}"
842
863
  )
864
+ participant = _derived_participant(manifest, source)
843
865
  return RoleExecution(
844
866
  role_execution_ref=role_ref,
845
- participant_ref=source.participant_ref,
867
+ participant_ref=participant.participant_ref,
846
868
  source_role_execution_ref=source.role_execution_ref,
847
869
  role=role,
848
870
  provider=source.provider,
@@ -859,7 +881,38 @@ def _derived_role_execution(
859
881
  binding=source.binding,
860
882
  served_model_attestation=ServedModelAttestation.unknown(),
861
883
  status="prepared",
862
- ), True
884
+ ), True, participant
885
+
886
+
887
+ def _derived_participant(
888
+ manifest: ExecutionManifest,
889
+ source: RoleExecution,
890
+ ) -> ParticipantAssignment:
891
+ """A fresh participant carrying the source's model but none of its session."""
892
+ origin = next(
893
+ (
894
+ item
895
+ for item in manifest.participant_assignments
896
+ if item.participant_ref == source.participant_ref
897
+ ),
898
+ None,
899
+ )
900
+ if origin is None:
901
+ raise ExecutionManifestError(
902
+ f"source role execution references unknown participant: "
903
+ f"{source.participant_ref}"
904
+ )
905
+ used = {item.participant_ref for item in manifest.participant_assignments}
906
+ ordinal = len(used) + 1
907
+ while f"participant-{ordinal:03d}" in used:
908
+ ordinal += 1
909
+ return replace(
910
+ origin,
911
+ participant_ref=f"participant-{ordinal:03d}",
912
+ session_ref=None,
913
+ window_ref=None,
914
+ status="prepared",
915
+ )
863
916
 
864
917
 
865
918
  def _validate_next_invocation(manifest: ExecutionManifest, row: Invocation) -> None: