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
@@ -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:
@@ -122,6 +122,7 @@ class MutationAuditResult:
122
122
  changed_artifact_paths: tuple[str, ...] = ()
123
123
  declared_out_of_plan_paths: tuple[str, ...] = ()
124
124
  unobserved_artifact_paths: tuple[str, ...] = ()
125
+ external_workspace_changes: tuple[str, ...] = ()
125
126
 
126
127
  def change_summary(self) -> dict[str, Any]:
127
128
  return {
@@ -138,6 +139,7 @@ class MutationAuditResult:
138
139
  "changedArtifactPaths": list(self.changed_artifact_paths),
139
140
  "declaredOutOfPlanPaths": list(self.declared_out_of_plan_paths),
140
141
  "unobservedArtifactPaths": list(self.unobserved_artifact_paths),
142
+ "externalWorkspaceChanges": list(self.external_workspace_changes),
141
143
  "beforeDigest": self.before_digest,
142
144
  "afterDigest": self.after_digest,
143
145
  }
@@ -240,13 +242,13 @@ class ExecutionMutationAudit:
240
242
  rows,
241
243
  source_changes,
242
244
  )
243
- artifact_failures, untracked_artifact_changes, switched = (
245
+ artifact_failures, untracked_artifact_changes, switched, external = (
244
246
  _artifact_policy_failures(before, rows, artifact_changed, after=after)
245
247
  )
246
248
  violations.extend(artifact_failures)
247
249
  worker_artifact_changes = {
248
250
  path for path in artifact_changed
249
- if path not in untracked_artifact_changes and path not in switched
251
+ if path not in untracked_artifact_changes and path not in switched and path not in external
250
252
  and not any(_is_relative_to(before.artifact_root / path, Path(item))
251
253
  for item in before.orchestrator_paths)
252
254
  }
@@ -276,11 +278,14 @@ class ExecutionMutationAudit:
276
278
  untracked_artifact_paths=tuple(sorted(untracked_artifact_changes)),
277
279
  warnings=_audit_warnings(untracked_artifact_changes)
278
280
  + _branch_switch_warnings(before, after, switched)
281
+ + tuple(f"unattributed change outside assigned worktree and artifact paths: {path}"
282
+ for path in sorted(external))
279
283
  + _plan_change_warnings(rows, source_changes | untracked_changes,
280
284
  artifact_changed, out_of_plan_edits, unobserved),
281
- changed_artifact_paths=tuple(sorted(artifact_changed)),
285
+ changed_artifact_paths=tuple(sorted(artifact_changed - external)),
282
286
  declared_out_of_plan_paths=tuple(out_of_plan_edits),
283
287
  unobserved_artifact_paths=tuple(sorted(unobserved)),
288
+ external_workspace_changes=tuple(sorted(external)),
284
289
  )
285
290
 
286
291
 
@@ -753,22 +758,12 @@ def _artifact_policy_failures(
753
758
  changed: set[str],
754
759
  *,
755
760
  after: MutationSnapshot | None = None,
756
- ) -> tuple[list[str], set[str], set[str]]:
757
- """``(위반 목록, 위반이 아닌 비추적 신규 경로, 브랜치 전환으로 설명되는 경로)``.
758
-
759
- artifact root 의 변경 중 위반으로 남는 것은 두 부류다 — okstra 산출물
760
- 서브트리(`.okstra/`) 안의 허용 밖 쓰기와, git 이 추적하는 파일의 변경.
761
- 그 밖의 비추적 신규 파일은 워커의 도구가 남긴 로그·캐시이므로 소스 root 의
762
- `_split_tracked` 와 같은 이유로 기록만 한다. artifact root 가 git 레포가
763
- 아니면 추적 여부를 알 수 없으므로 종전대로 전부 위반으로 본다.
764
-
765
- 추적 파일의 변경 중 **artifact root 의 HEAD 가 실행 창 안에서 옮겨졌고 그
766
- 두 커밋 사이에서 실제로 달라지는 경로**는 워커의 쓰기가 아니라 사람의 브랜치
767
- 전환이다. 실측(2026-09-09, `fontsninja-v3-site` dev-10627 reverify r1b): 워커가
768
- 워크트리에서 읽기만 하는 8분 동안 프로젝트 루트에서 `checkout preprod →
769
- rebase` 가 있었고, run 브랜치에만 있는 `CardHero.{tsx,styled.ts}` 가 사라져
770
- 완주한 결과가 `contract-failed-unattributed` 로 폐기됐다. 그 경로는 위반에서
771
- 빼고 경고로 남긴다. 전환으로 설명되지 않는 추적 파일 변경은 그대로 위반이다.
761
+ ) -> tuple[list[str], set[str], set[str], set[str]]:
762
+ """위반·비추적 파일·브랜치 전환·외부 작업공간 관측을 분리한다.
763
+
764
+ 작업트리와 기본 체크아웃이 다르면 기본 체크아웃 소스의 변경 주체는
765
+ 전후 비교로 판단할 수 없다. 배정된 산출물 밖의 변경은 관측으로 남기고,
766
+ `.okstra` 내부의 허용 밖 쓰기와 배정된 소스 정책은 별도로 검사한다.
772
767
  """
773
768
  allowed = _allowed_artifact_paths(policies)
774
769
  orchestrator = {
@@ -799,9 +794,14 @@ def _artifact_policy_failures(
799
794
  if path in unauthorized and not _is_within(path, _OKSTRA_ARTIFACT_SUBTREE)
800
795
  }
801
796
  untracked_outside -= switched
802
- violating = unauthorized - untracked_outside - switched
797
+ external = {
798
+ path for path in unauthorized - untracked_outside - switched
799
+ if snapshot.artifact_root != snapshot.root
800
+ and not _is_within(path, _OKSTRA_ARTIFACT_SUBTREE)
801
+ }
802
+ violating = unauthorized - untracked_outside - switched - external
803
803
  failures = ["artifact-root change exceeds batch policy union"] if violating else []
804
- return failures, untracked_outside, switched
804
+ return failures, untracked_outside, switched, external
805
805
 
806
806
 
807
807
  def _branch_switch_paths(