okstra 0.202.0 → 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 (258) 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/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 +25 -19
  15. package/docs/cli.md +15 -12
  16. package/docs/contributor-change-matrix.md +3 -2
  17. package/docs/performance-improvement-plan-v2.md +2 -3
  18. package/docs/project-structure-overview.md +35 -9
  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.md +1 -1
  23. package/docs/task-process/release-handoff.md +36 -39
  24. package/package.json +1 -2
  25. package/runtime/BUILD.json +2 -2
  26. package/runtime/agents/common.json +28 -0
  27. package/runtime/agents/operations/code-review.json +6 -0
  28. package/runtime/agents/operations/report-translation.json +6 -0
  29. package/runtime/agents/operations/schedule-verification.json +6 -0
  30. package/runtime/agents/roles/analyser.json +18 -0
  31. package/runtime/agents/roles/critic.json +18 -0
  32. package/runtime/agents/roles/designer.json +18 -0
  33. package/runtime/agents/roles/implementer.json +20 -0
  34. package/runtime/agents/roles/leader.json +20 -0
  35. package/runtime/agents/roles/planner.json +18 -0
  36. package/runtime/agents/roles/report-writer.json +19 -0
  37. package/runtime/agents/roles/translator.json +19 -0
  38. package/runtime/agents/roles/verifier.json +18 -0
  39. package/runtime/bin/lib/okstra/usage.sh +5 -5
  40. package/runtime/prompts/duties/acceptance-critic.json +32 -0
  41. package/runtime/prompts/duties/acceptance-verifier.json +32 -0
  42. package/runtime/prompts/duties/analysis-worker.json +32 -0
  43. package/runtime/prompts/duties/code-reviewer.json +32 -0
  44. package/runtime/prompts/duties/diagnosis-worker.json +32 -0
  45. package/runtime/prompts/duties/direction-selection-worker.json +32 -0
  46. package/runtime/prompts/duties/discovery-worker.json +32 -0
  47. package/runtime/prompts/duties/implementation-executor.json +32 -0
  48. package/runtime/prompts/duties/implementation-verifier.json +32 -0
  49. package/runtime/prompts/duties/lead.json +32 -0
  50. package/runtime/prompts/duties/planning-worker.json +36 -0
  51. package/runtime/prompts/duties/report-writer.json +32 -0
  52. package/runtime/prompts/duties/reverification-worker.json +32 -0
  53. package/runtime/prompts/duties/schedule-verifier.json +32 -0
  54. package/runtime/prompts/duties/scope-critic.json +32 -0
  55. package/runtime/prompts/duties/technical-verification-worker.json +32 -0
  56. package/runtime/prompts/duties/translator.json +32 -0
  57. package/runtime/prompts/launch.template.md +2 -1
  58. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  59. package/runtime/prompts/lead/convergence.md +4 -4
  60. package/runtime/prompts/lead/okstra-lead-contract.md +113 -4
  61. package/runtime/prompts/lead/plan-body-verification.md +6 -6
  62. package/runtime/prompts/lead/report-writer.md +3 -3
  63. package/runtime/prompts/profiles/_common-contract.md +2 -2
  64. package/runtime/prompts/profiles/_implementation-executor.md +4 -1
  65. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  66. package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
  67. package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
  68. package/runtime/prompts/profiles/error-analysis.json +39 -0
  69. package/runtime/prompts/profiles/error-analysis.md +0 -25
  70. package/runtime/prompts/profiles/feature-analysis.json +31 -0
  71. package/runtime/prompts/profiles/feature-analysis.md +0 -20
  72. package/runtime/prompts/profiles/final-verification.json +30 -0
  73. package/runtime/prompts/profiles/final-verification.md +3 -22
  74. package/runtime/prompts/profiles/forbidden-actions.json +4 -3
  75. package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
  76. package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
  77. package/runtime/prompts/profiles/implementation-planning.json +40 -0
  78. package/runtime/prompts/profiles/implementation-planning.md +4 -29
  79. package/runtime/prompts/profiles/implementation.json +30 -0
  80. package/runtime/prompts/profiles/implementation.md +1 -20
  81. package/runtime/prompts/profiles/improvement-discovery.json +31 -0
  82. package/runtime/prompts/profiles/improvement-discovery.md +0 -20
  83. package/runtime/prompts/profiles/project-analysis.json +31 -0
  84. package/runtime/prompts/profiles/project-analysis.md +0 -20
  85. package/runtime/prompts/profiles/release-handoff.json +5 -0
  86. package/runtime/prompts/profiles/release-handoff.md +71 -73
  87. package/runtime/prompts/profiles/requirements-discovery.json +39 -0
  88. package/runtime/prompts/profiles/requirements-discovery.md +0 -25
  89. package/runtime/prompts/profiles/technical-verification.json +39 -0
  90. package/runtime/prompts/profiles/technical-verification.md +0 -25
  91. package/runtime/prompts/wizard/prompts.ko.json +12 -17
  92. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
  93. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
  94. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
  95. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
  96. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
  97. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
  98. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
  99. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
  100. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
  101. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
  102. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
  103. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
  104. package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
  105. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
  106. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
  107. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
  108. package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
  109. package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
  110. package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
  111. package/runtime/python/okstra_ctl/cmux.py +69 -20
  112. package/runtime/python/okstra_ctl/code_review_target.py +16 -8
  113. package/runtime/python/okstra_ctl/conformance.py +43 -0
  114. package/runtime/python/okstra_ctl/consumers.py +6 -3
  115. package/runtime/python/okstra_ctl/container.py +31 -8
  116. package/runtime/python/okstra_ctl/context_cost.py +11 -15
  117. package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
  118. package/runtime/python/okstra_ctl/convergence_provenance.py +7 -1
  119. package/runtime/python/okstra_ctl/design_prep.py +34 -1
  120. package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
  121. package/runtime/python/okstra_ctl/domain/host.py +5 -0
  122. package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
  123. package/runtime/python/okstra_ctl/error_report.py +4 -3
  124. package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
  125. package/runtime/python/okstra_ctl/handoff.py +167 -277
  126. package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
  127. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
  128. package/runtime/python/okstra_ctl/lead_progress.py +1 -1
  129. package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
  130. package/runtime/python/okstra_ctl/manager_cli.py +92 -4
  131. package/runtime/python/okstra_ctl/manager_launch.py +1 -1
  132. package/runtime/python/okstra_ctl/manager_paths.py +14 -3
  133. package/runtime/python/okstra_ctl/manager_store.py +210 -3
  134. package/runtime/python/okstra_ctl/manager_sync.py +4 -1
  135. package/runtime/python/okstra_ctl/manager_view.py +2 -1
  136. package/runtime/python/okstra_ctl/model_discovery.py +30 -0
  137. package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
  138. package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
  139. package/runtime/python/okstra_ctl/models.py +1 -1
  140. package/runtime/python/okstra_ctl/next_phase.py +16 -6
  141. package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
  142. package/runtime/python/okstra_ctl/option_comparison.py +168 -0
  143. package/runtime/python/okstra_ctl/path_hints.py +9 -0
  144. package/runtime/python/okstra_ctl/paths.py +3 -0
  145. package/runtime/python/okstra_ctl/profile_show.py +42 -1
  146. package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
  147. package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
  148. package/runtime/python/okstra_ctl/render.py +50 -0
  149. package/runtime/python/okstra_ctl/report_contract.py +1 -1
  150. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
  151. package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
  152. package/runtime/python/okstra_ctl/report_translation.py +2 -1
  153. package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
  154. package/runtime/python/okstra_ctl/role_requirements.py +142 -129
  155. package/runtime/python/okstra_ctl/rollup.py +3 -1
  156. package/runtime/python/okstra_ctl/run.py +76 -29
  157. package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
  158. package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
  159. package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
  160. package/runtime/python/okstra_ctl/stage_map.py +16 -2
  161. package/runtime/python/okstra_ctl/stage_targets.py +209 -43
  162. package/runtime/python/okstra_ctl/team.py +22 -13
  163. package/runtime/python/okstra_ctl/time_report.py +2 -1
  164. package/runtime/python/okstra_ctl/usage_report.py +3 -1
  165. package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
  166. package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
  167. package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
  168. package/runtime/python/okstra_ctl/wizard/state.py +3 -5
  169. package/runtime/python/okstra_ctl/wizard/steps_plan.py +3 -23
  170. package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
  171. package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
  172. package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
  173. package/runtime/python/okstra_ctl/workflow.py +1 -1
  174. package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
  175. package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
  176. package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
  177. package/runtime/python/okstra_token_usage/pricing.py +6 -4
  178. package/runtime/schemas/agent-common-v1.schema.json +34 -0
  179. package/runtime/schemas/agent-duty-v1.schema.json +38 -0
  180. package/runtime/schemas/agent-operation-v1.schema.json +11 -0
  181. package/runtime/schemas/agent-profile-v1.schema.json +46 -0
  182. package/runtime/schemas/agent-role-v1.schema.json +29 -0
  183. package/runtime/schemas/final-report-v2.0.schema.json +118 -97
  184. package/runtime/schemas/final-report-v3.0.schema.json +118 -97
  185. package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
  186. package/runtime/skills/okstra-chat/SKILL.md +2 -2
  187. package/runtime/skills/okstra-code-review/SKILL.md +23 -9
  188. package/runtime/skills/okstra-container-build/SKILL.md +10 -10
  189. package/runtime/skills/okstra-inspect/SKILL.md +1 -1
  190. package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
  191. package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
  192. package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
  193. package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
  194. package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
  195. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  196. package/runtime/skills/okstra-inspect/facets/status.md +4 -3
  197. package/runtime/skills/okstra-inspect/facets/time.md +11 -10
  198. package/runtime/skills/okstra-manager/SKILL.md +18 -2
  199. package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
  200. package/runtime/skills/okstra-rollup/SKILL.md +5 -5
  201. package/runtime/skills/okstra-run/SKILL.md +31 -12
  202. package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
  203. package/runtime/skills/okstra-setup/SKILL.md +12 -10
  204. package/runtime/skills/okstra-setup/references/project-config.md +7 -6
  205. package/runtime/skills/okstra-usage/SKILL.md +1 -1
  206. package/runtime/skills/okstra-user-response/SKILL.md +1 -1
  207. package/runtime/templates/manager/view.template.html +1 -0
  208. package/runtime/templates/report-writer-prompt-preamble.md +8 -0
  209. package/runtime/templates/reports/brief.template.md +14 -4
  210. package/runtime/templates/reports/html/i18n/en.json +5 -4
  211. package/runtime/templates/reports/html/i18n/ko.json +5 -4
  212. package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
  213. package/runtime/templates/reports/i18n/en.json +1 -1
  214. package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
  215. package/runtime/templates/reports/release-handoff-input.template.md +6 -4
  216. package/runtime/templates/translator-prompt-preamble.md +36 -0
  217. package/runtime/validators/checks/validate-assets-01.py +7 -8
  218. package/runtime/validators/validate-brief.py +70 -0
  219. package/runtime/validators/validate-implementation-plan-stages.py +2 -1
  220. package/runtime/validators/validate-run.py +59 -9
  221. package/runtime/validators/validate-schedule.py +9 -0
  222. package/docs/for-ai/README.md +0 -68
  223. package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
  224. package/docs/for-ai/skills/okstra-chat.md +0 -34
  225. package/docs/for-ai/skills/okstra-code-review.md +0 -57
  226. package/docs/for-ai/skills/okstra-container-build.md +0 -129
  227. package/docs/for-ai/skills/okstra-inspect.md +0 -262
  228. package/docs/for-ai/skills/okstra-manager.md +0 -86
  229. package/docs/for-ai/skills/okstra-memory.md +0 -126
  230. package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
  231. package/docs/for-ai/skills/okstra-rollup.md +0 -114
  232. package/docs/for-ai/skills/okstra-run.md +0 -250
  233. package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
  234. package/docs/for-ai/skills/okstra-setup.md +0 -167
  235. package/docs/for-ai/skills/okstra-usage.md +0 -29
  236. package/docs/for-ai/skills/okstra-user-response.md +0 -72
  237. package/runtime/agents/workers/claude-worker.md +0 -128
  238. package/runtime/agents/workers/report-writer-worker.md +0 -37
  239. package/runtime/agents/workers/translator-worker.md +0 -63
  240. package/runtime/prompts/duties/acceptance-critic.md +0 -44
  241. package/runtime/prompts/duties/acceptance-verifier.md +0 -44
  242. package/runtime/prompts/duties/analysis-worker.md +0 -44
  243. package/runtime/prompts/duties/code-reviewer.md +0 -44
  244. package/runtime/prompts/duties/common.md +0 -39
  245. package/runtime/prompts/duties/diagnosis-worker.md +0 -44
  246. package/runtime/prompts/duties/direction-selection-worker.md +0 -44
  247. package/runtime/prompts/duties/discovery-worker.md +0 -44
  248. package/runtime/prompts/duties/implementation-executor.md +0 -44
  249. package/runtime/prompts/duties/implementation-verifier.md +0 -44
  250. package/runtime/prompts/duties/lead.md +0 -44
  251. package/runtime/prompts/duties/planning-worker.md +0 -52
  252. package/runtime/prompts/duties/report-writer.md +0 -44
  253. package/runtime/prompts/duties/reverification-worker.md +0 -44
  254. package/runtime/prompts/duties/schedule-verifier.md +0 -44
  255. package/runtime/prompts/duties/scope-critic.md +0 -44
  256. package/runtime/prompts/duties/technical-verification-worker.md +0 -44
  257. package/runtime/prompts/duties/translator.md +0 -44
  258. package/runtime/python/okstra_ctl/pane_title.py +0 -154
@@ -58,56 +58,7 @@ def _validate_run_identity(
58
58
  ).duty_audience
59
59
  except ValueError as exc:
60
60
  raise AgentPromptCliError(str(exc)) from exc
61
- if audience != expected and _manifest_issued_role_execution(
62
- manifest, audience=audience, assignment_ref=assignment_ref
63
- ):
64
- # The prompt plan names a worker's role by worker id, so on an
65
- # implementation run the executor's provider resolves to the
66
- # executor role no matter which role execution is being targeted.
67
- # But the manifest issues one role execution per role, and a
68
- # provider serving as both executor and verifier gets two — a
69
- # normal roster, since the verifier contract accepts reusing the
70
- # executor's model behind its own session. Refusing the second
71
- # audience made a role execution okstra had itself issued
72
- # undispatchable, which costs the run an independent verifier.
73
- expected = audience
74
61
  if audience != expected:
75
62
  raise AgentPromptCliError(
76
63
  f"audience {audience!r} does not match required audience {expected!r}"
77
64
  )
78
-
79
-
80
- def _manifest_issued_role_execution(
81
- manifest: Mapping[str, Any],
82
- *,
83
- audience: str,
84
- assignment_ref: str,
85
- ) -> bool:
86
- """Whether this run issued a role execution for that audience's role.
87
-
88
- Keyed on the manifest's own rows, not on what the prompt plan infers from
89
- a worker id: the question is whether okstra created the execution being
90
- targeted, and only the manifest answers that.
91
- """
92
- from ...domain.role import RoleCatalogError, role_for_duty
93
-
94
- try:
95
- role = role_for_duty(audience)
96
- except RoleCatalogError:
97
- return False
98
- assignments = manifest.get("invocationAssignments")
99
- assignment = (
100
- assignments.get(assignment_ref) if isinstance(assignments, Mapping) else None
101
- )
102
- provider = (
103
- assignment.get("provider") if isinstance(assignment, Mapping) else None
104
- )
105
- if not provider:
106
- return False
107
- executions = manifest.get("roleExecutions")
108
- return any(
109
- isinstance(row, Mapping)
110
- and row.get("role") == role
111
- and row.get("provider") == provider
112
- for row in (executions if isinstance(executions, list) else [])
113
- )
@@ -500,7 +500,10 @@ def _stage_ledger_block(stage_ledger_json: str) -> list[str]:
500
500
  "in it is taken. Never reuse or renumber one: a new stage takes the",
501
501
  "next number after the highest listed here, and reworking a completed",
502
502
  "stage means cancelling it and adding a new number, never editing it",
503
- "in place. `sourcePlan` is the plan the completed stages were built",
503
+ "in place. The new plan declares every stage listed here as well as the",
504
+ "ones it adds — its rows run 1..N with no gap — because a plan whose",
505
+ "rows start above 1 is refused by every consumer that parses a Stage",
506
+ "Map. `sourcePlan` is the plan the completed stages were built",
504
507
  "against; `latestPlan` is the plan this list came from. When the two",
505
508
  "differ, the completed work followed the former and the numbering",
506
509
  "authority is the latter.",
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  from collections.abc import Callable, Sequence
5
+ from dataclasses import replace
5
6
 
6
7
  from ..domain.worker_runtime import (
7
8
  EnvironmentBlocked,
@@ -19,11 +20,15 @@ def open_worker(
19
20
  last_unavailable: SurfaceUnavailable | None = None
20
21
  for port in chain:
21
22
  try:
22
- return port.spawn(request_for(port.surface))
23
+ handle = port.spawn(request_for(port.surface))
23
24
  except EnvironmentBlocked:
24
25
  raise
25
26
  except SurfaceUnavailable as exc:
26
27
  last_unavailable = exc
28
+ continue
29
+ if last_unavailable is None:
30
+ return handle
31
+ return replace(handle, fallback_reason=str(last_unavailable))
27
32
  if last_unavailable is None:
28
33
  raise SurfaceUnavailable("runtime chain is empty")
29
34
  raise last_unavailable
@@ -30,7 +30,7 @@ from .ports.host_model import (
30
30
  HostModelBindingPort,
31
31
  HostModelBindingRequest,
32
32
  )
33
- from .role_requirements import RoleProfile, RoleRequirement
33
+ from .role_requirements import RoleProfile, RoleRequirement, StaticRoleRequirement
34
34
 
35
35
 
36
36
  TerminalBackend = Literal[
@@ -239,8 +239,17 @@ def resolve_model_assignment(
239
239
  host: HostSessionContext,
240
240
  environment: AssignmentEnvironment,
241
241
  entry_mode: Literal["current-session", "new-session"] = "new-session",
242
+ require_selectable: bool = True,
242
243
  ) -> ResolvedAssignment:
243
- """Resolve one canonical model through the selected host execution path."""
244
+ """Resolve one canonical model through the selected host execution path.
245
+
246
+ `require_selectable=False` re-resolves a binding a run already recorded.
247
+ Selectability decides what a user may pick now; a run that picked a model
248
+ before it left the picker still has to finish. 실측 2026-09-23
249
+ (fontradar-v2-api dev-10860 implementation-planning-002): `gpt-5.6-sol` 이
250
+ 카탈로그에서 목록 제외로 바뀌자, 그 모델로 이미 세 라운드를 돈 run 의 다음
251
+ 디스패치가 `model binding changed: ... not-selectable` 로 막혔다.
252
+ """
244
253
  _validate_environment(host, environment)
245
254
  model = pool.resolve(model_ref)
246
255
  availability = pool.availability(
@@ -249,7 +258,9 @@ def resolve_model_assignment(
249
258
  host.host_id,
250
259
  entry_mode,
251
260
  )
252
- if not availability.available:
261
+ if not availability.available and (
262
+ require_selectable or availability.reason != "not-selectable"
263
+ ):
253
264
  raise AssignmentResolutionError(
254
265
  f"model {model_ref!r} is unavailable: {availability.reason}"
255
266
  )
@@ -326,8 +337,8 @@ def resolve_model_selection(
326
337
  return selected
327
338
 
328
339
 
329
- def _static_requirements(profile: RoleProfile) -> tuple[RoleRequirement, ...]:
330
- leader = RoleRequirement("leader", 1, 1, 1, "lead")
340
+ def _static_requirements(profile: RoleProfile) -> tuple[StaticRoleRequirement, ...]:
341
+ leader = StaticRoleRequirement("leader", 1, 1, 1, "lead")
331
342
  return (leader, *(row for row in profile.roles if not row.dynamic))
332
343
 
333
344
 
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  import json
9
9
  import os
10
+ import re
10
11
  import shlex
11
12
  import shutil
12
13
  import socket
@@ -72,6 +73,10 @@ SOCKET_UNREACHABLE = "unreachable"
72
73
 
73
74
  SOCKET_PROBE_TIMEOUT_SECONDS = 2
74
75
 
76
+ # cmux 가 돌려주는 식별자가 UUID 인지 ref 인지 가리는 자리. ref 는 번호가 다시
77
+ # 매겨지므로 원장에 남길 수 없다.
78
+ _UUID_PATTERN = re.compile(r"[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}")
79
+
75
80
 
76
81
  def cmux_cli_path() -> str:
77
82
  """Absolute path to the cmux CLI, or "" when cmux is not installed.
@@ -455,7 +460,11 @@ def spawn_worker_surface(
455
460
  owned_surface_ids=owned_surface_ids,
456
461
  )
457
462
  target = _pane_by_id(panes, placement.pane_id)
458
- surface_uuid = _open_worker_surface(workspace, placement, target)
463
+ try:
464
+ surface_uuid = _create_surface(workspace, placement, target)
465
+ except (RuntimeError, OSError, subprocess.SubprocessError):
466
+ _restore_lead_after_failed_spawn(workspace, owned_surface_ids)
467
+ raise
459
468
  try:
460
469
  renamed = rename_surface(surface_uuid, title)
461
470
  if renamed.returncode != 0:
@@ -475,10 +484,35 @@ def spawn_worker_surface(
475
484
  raise RuntimeError(
476
485
  f"{exc}; cleanup failed for cmux surface {surface_uuid}: {cleanup_error}"
477
486
  ) from exc
487
+ finally:
488
+ _restore_lead_after_failed_spawn(workspace, owned_surface_ids)
478
489
  raise
479
490
  return surface_uuid
480
491
 
481
492
 
493
+ def _restore_lead_after_failed_spawn(
494
+ workspace: str, owned_surface_ids: Collection[str]
495
+ ) -> None:
496
+ """실패한 분할이 리드에서 떼어 간 폭을 돌려준다.
497
+
498
+ 폭을 되돌리는 자리는 창이 열린 뒤의 `_size_lead_pane` 하나뿐이었다. 실패한
499
+ 시도는 리드에서 폭만 떼어 가고 끝났고, 실패가 반복되면 축소가 누적된다 —
500
+ 실측 2026-09-21, jobs dev-10860: 강등 7건 뒤 리드가 64칼럼에서 14칼럼.
501
+
502
+ 닫지 못한 창의 폭까지 되찾지는 않는다. 그 창은 `lead_target_width` 에서
503
+ 남의 창으로 셈해져 usable 에서 빠지고, 리드는 남은 공간에서 제 몫을 가져
504
+ 간다. 되찾으려면 okstra 가 자기 것이라 증명하지 못한 창의 경계를 미는
505
+ 셈이라 그쪽이 더 나쁘다.
506
+
507
+ 크기 조정이 실패해도 삼킨다: 호출자는 창을 열지 못한 사유를 들고 올라가는
508
+ 중이고, 원장에 남아야 할 문장은 그쪽이다.
509
+ """
510
+ try:
511
+ _size_lead_pane(workspace, owned_surface_ids)
512
+ except (RuntimeError, OSError, subprocess.SubprocessError):
513
+ return
514
+
515
+
482
516
  def rename_surface(surface: str, title: str) -> subprocess.CompletedProcess[str]:
483
517
  """Set the tab title cmux shows for one surface.
484
518
 
@@ -686,27 +720,14 @@ def rpc(method: str, params: dict[str, Any]) -> dict[str, Any]:
686
720
  return payload if isinstance(payload, dict) else {}
687
721
 
688
722
 
689
- def _open_worker_surface(
690
- workspace: str, placement: Placement, target: PaneGeometry
691
- ) -> str:
692
- before = open_surface_ids(workspace)
693
- _create_surface(workspace, placement, target)
694
- new_ids = open_surface_ids(workspace) - before
695
- if len(new_ids) != 1:
696
- raise RuntimeError(
697
- f"cmux opened {len(new_ids)} surfaces where exactly one was expected"
698
- )
699
- return new_ids.pop()
700
-
701
-
702
723
  def open_surface_ids(workspace: str) -> set[str]:
703
724
  """Every surface UUID the workspace currently holds.
704
725
 
705
- Dispatch diffs this set across a create to identify the new surface, rather
706
- than translating the `OK surface:N` echo, because `list-pane-surfaces`
707
- reports only the focused pane unless given a `--pane`, and the new pane is
708
- not focused. Teardown intersects its recorded ids with it to tell a surface
709
- that is still open from one that closed earlier in the run.
726
+ Teardown intersects its recorded ids with this set to tell a surface that is
727
+ still open from one that closed earlier in the run. It is teardown's only
728
+ caller: dispatch used to diff this set across a create to name the surface
729
+ it had just opened, and that is now `new-split --id-format uuids`, which
730
+ says the uuid outright.
710
731
  """
711
732
  return {
712
733
  surface_id
@@ -717,9 +738,22 @@ def open_surface_ids(workspace: str) -> set[str]:
717
738
 
718
739
  def _create_surface(
719
740
  workspace: str, placement: Placement, target: PaneGeometry
720
- ) -> None:
741
+ ) -> str:
742
+ """새 창을 열고 그 표면의 UUID 를 준다.
743
+
744
+ UUID 는 cmux 가 스스로 말해 준다. `--id-format uuids` 를 주면 이 명령의 출력이
745
+ `OK <surface-uuid> <workspace-uuid>` 가 되고, 그 첫 UUID 가 새 창의 표면이다
746
+ (실측 2026-09-21, 폐기용 워크스페이스 1회 분할: echo 의 UUID 가 직후
747
+ `pane.list` 가 보여 준 새 창의 `surface_ids[0]` 과 같았다).
748
+
749
+ 앞서는 분할 전후의 표면 목록 차집합으로 알아냈는데, 그 차집합이 실제 run 에서
750
+ 여덟 번 중 여덟 번 비어 워커가 창에 붙지 못하고 빈 창만 남았다. 자기가 만든 것의
751
+ 이름을 만든 쪽에 묻는 편이 두 번 읽어 비교하는 것보다 틀릴 자리가 적다.
752
+ """
721
753
  created = run_cmux(
722
754
  [
755
+ "--id-format",
756
+ "uuids",
723
757
  "new-split",
724
758
  placement.direction,
725
759
  "--workspace",
@@ -730,6 +764,21 @@ def _create_surface(
730
764
  )
731
765
  if created.returncode != 0:
732
766
  raise RuntimeError(created.stderr.strip() or "cmux could not open a pane")
767
+ return _surface_from_split_echo(created.stdout)
768
+
769
+
770
+ def _surface_from_split_echo(stdout: str) -> str:
771
+ """`new-split` 의 출력에서 새 표면 UUID 만 꺼낸다.
772
+
773
+ ref(`surface:2`)는 받지 않는다. okstra 가 기록하고 나중에 닫는 이름이라, cmux 가
774
+ 창을 여닫을 때마다 번호를 다시 매기는 ref 로는 엉뚱한 창을 닫게 된다.
775
+ """
776
+ parts = stdout.split()
777
+ if len(parts) >= 2 and parts[0] == "OK" and _UUID_PATTERN.fullmatch(parts[1]):
778
+ return parts[1]
779
+ raise RuntimeError(
780
+ f"cmux did not name the pane it opened: {stdout.strip()!r}"
781
+ )
733
782
 
734
783
 
735
784
  def _exec_worker(surface_uuid: str, *, cwd: Path, command: Sequence[str]) -> None:
@@ -99,13 +99,13 @@ def _branch_snapshot(repo: Path, branch: str, base: str, review_date: str) -> di
99
99
 
100
100
 
101
101
  _CLI_EPILOG = r"""Usage:
102
- okstra code-review target --task-key <k> --stage <N> [--project-root <dir>] [--cwd <dir>] --json
102
+ okstra code-review target --task-key <k> --stage <N> [--base <ref>] [--project-root <dir>] [--cwd <dir>] --json
103
103
  okstra code-review target --branch <name> [--base <ref>] [--date <YYYY-MM-DD>] [--project-root <dir>] --json
104
104
 
105
105
  Output: JSON { ok, mode, worktreePath, branch, baseCommit, headCommit,
106
- reviewPath, round }. Stage mode reads the base commit from the stage
107
- registry row; branch mode falls back to the merge-base with the default
108
- branch. An empty worktreePath means the review reads the branch ref
106
+ reviewPath, round }. `--base` sets the base in either mode. Without it, stage
107
+ mode reads the base commit from the stage registry row and branch mode falls
108
+ back to the merge-base with the default branch. An empty worktreePath means the review reads the branch ref
109
109
  instead of a checked-out tree. This is read-only — it never creates
110
110
  directories or files.
111
111
  """
@@ -124,7 +124,10 @@ def _build_parser() -> argparse.ArgumentParser:
124
124
  parser.add_argument("--task-key", default="", help="project-id:task-group:task-id")
125
125
  parser.add_argument("--stage", type=int, default=0, help="stage number to review")
126
126
  parser.add_argument("--branch", default="", help="branch to review")
127
- parser.add_argument("--base", default="", help="explicit base ref for branch mode")
127
+ parser.add_argument(
128
+ "--base", default="",
129
+ help="explicit base ref; overrides the stage registry base or the branch merge-base",
130
+ )
128
131
  parser.add_argument("--date", default="", help="YYYY-MM-DD for branch result naming")
129
132
  parser.add_argument("--project-root", default="", help="explicit project root")
130
133
  parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
@@ -138,7 +141,9 @@ def _resolve(args: argparse.Namespace, project_root: Path) -> dict:
138
141
  if args.task_key:
139
142
  if args.stage < 1:
140
143
  raise StateError("--task-key needs --stage <N>, N >= 1", stage="args")
141
- return code_review_target_snapshot(project_root, args.task_key, args.stage)
144
+ return code_review_target_snapshot(
145
+ project_root, args.task_key, args.stage, base=args.base
146
+ )
142
147
  if args.branch:
143
148
  return _branch_snapshot(
144
149
  project_root, args.branch, args.base, _review_date(args.date)
@@ -204,7 +209,7 @@ def main(argv: list[str] | None = None) -> int:
204
209
  # 것으로 합쳐진다.
205
210
 
206
211
  def code_review_target_snapshot(
207
- project_root: Path, task_key: str, stage: int
212
+ project_root: Path, task_key: str, stage: int, *, base: str = ""
208
213
  ) -> dict:
209
214
  """stage 코드 리뷰가 무엇을 읽고 결과를 어디에 쓰는지 해소한다.
210
215
 
@@ -218,6 +223,9 @@ def code_review_target_snapshot(
218
223
  표류한다: 다중의존 stage 는 오늘의 task-key worktree HEAD 를 받게 되어
219
224
  whole-task final-verification 이 stage 들을 병합한 뒤에는 base..head 구간이
220
225
  비거나 뒤집히고, 단일의존 stage 는 선행 stage 가 재실행되면 어긋난다.
226
+
227
+ `base` 가 주어지면 그 값을 쓴다 — 스킬이 기록된 base 를 보여 주고 사용자가
228
+ 다른 ref 를 고른 경우다. 무시하면 사용자가 거부한 범위로 리뷰가 진행된다.
221
229
  """
222
230
  identity = resolve_task_identity(project_root, task_key)
223
231
  task_root = Path(identity["taskRoot"])
@@ -241,7 +249,7 @@ def code_review_target_snapshot(
241
249
  coords = (identity["projectId"], identity["taskGroup"], identity["taskId"])
242
250
  stage_row = worktree_registry.get_stage_row(*coords, stage) or {}
243
251
  worktree_view = _stage_worktree_view(project_root, stage_row, stage)
244
- base_commit = stage_row.get("base_ref") or _legacy_stage_base_commit(
252
+ base_commit = base or stage_row.get("base_ref") or _legacy_stage_base_commit(
245
253
  identity, stages, selected
246
254
  )
247
255
  review_path, round_no = code_review_paths.next_stage_review(
@@ -219,6 +219,49 @@ def exempt_stage_surface_conflicts(
219
219
  return conflicts
220
220
 
221
221
 
222
+ def declared_stage_surface_gaps(
223
+ data: object, patterns: object = None,
224
+ ) -> list[dict[str, object]]:
225
+ """`Conformance tests:` 의 `requires` 가 계획된 경로의 표면을 다 덮지 못한 stage.
226
+
227
+ 구현 게이트는 두 가지를 동시에 요구한다 — diff 가 건드린 표면을 `requires`
228
+ 가 덮을 것(`_validate_conformance_surfaces`), 그리고 선언된 stage 의
229
+ `requires` 가 계획 선언과 같을 것(`_declared_conformance_errors`). 계획이
230
+ `requires` 를 좁게 적었는데 그 stage 가 계획한 경로가 다른 표면을 건드리면
231
+ 구현은 둘 중 하나에 반드시 걸린다. 실측(2026-09-22, fontradar-v2-api dev-10860
232
+ Stage 1~3): `requires=[io]` 로 선언하고 `catalog.controller.ts`(http)를
233
+ 계획해, 구현 run 이 "undeclared surface" 와 "requires mismatch" 를 번갈아 냈다.
234
+ 면제 stage 와 같은 판별을 선언 stage 에도 승인 전에 한다.
235
+
236
+ 반환 행: `{"stage": <int>, "surfaces": [빠진 표면..], "paths": [..],
237
+ "requires": [선언된 것..]}`.
238
+ """
239
+ planning = data.get("implementationPlanning") if isinstance(data, dict) else None
240
+ stages = planning.get("stages") if isinstance(planning, dict) else None
241
+ gaps: list[dict[str, object]] = []
242
+ for stage in stages if isinstance(stages, list) else []:
243
+ if not isinstance(stage, dict):
244
+ continue
245
+ parsed = parse_conformance_tests(stage.get("conformanceTests"))
246
+ number = stage.get("stage")
247
+ if parsed is None or not isinstance(number, int) or isinstance(number, bool):
248
+ continue
249
+ _script, requires = parsed
250
+ touching = sorted(
251
+ path for path in _stage_planned_paths(stage)
252
+ if detect_surfaces([path], patterns) - requires
253
+ )
254
+ missing = detect_surfaces(touching, patterns) - requires
255
+ if missing:
256
+ gaps.append({
257
+ "stage": number,
258
+ "surfaces": sorted(missing),
259
+ "paths": touching,
260
+ "requires": sorted(requires),
261
+ })
262
+ return gaps
263
+
264
+
222
265
  def _stage_planned_paths(stage: dict) -> list[str]:
223
266
  """stage 의 step 들이 계획한 경로 — 경로 모양인 문자열만.
224
267
 
@@ -244,15 +244,18 @@ def append_verified(plan_run_root: Path, *, impl_task_key: str, stage: int,
244
244
 
245
245
 
246
246
  def append_pr(plan_run_root: Path, *, impl_task_key: str, stages: List[int],
247
- branch: str, url: str) -> None:
248
- """handoff 의 PR 생성/재사용 기록. 같은 branch 의 pr 행이 이미 있으면 멱등."""
247
+ branch: str, url: str, base: str = "") -> None:
248
+ """handoff 의 PR 생성/재사용 기록. 같은 branch 의 pr 행이 이미 있으면 멱등.
249
+
250
+ `base` 는 그 PR 이 올라앉은 브랜치다. stage PR 은 스택이라 base 가 릴리스
251
+ 브랜치가 아닐 수 있고, 어느 순서로 머지해야 하는지는 이 값에만 남는다."""
249
252
  with consumers_mutex(plan_run_root):
250
253
  for row in read_consumers(plan_run_root):
251
254
  if row.get("status") == "pr" and row.get("branch") == branch:
252
255
  return
253
256
  _append_row(plan_run_root, {
254
257
  "impl_task_key": impl_task_key, "stages": sorted(stages),
255
- "status": "pr", "branch": branch, "url": url,
258
+ "status": "pr", "branch": branch, "base": base, "url": url,
256
259
  })
257
260
 
258
261
 
@@ -213,8 +213,18 @@ def _compose_ps(project_name: str, cwd: str, include_stopped: bool = False) -> l
213
213
  return rows
214
214
 
215
215
 
216
- def _docker_run(argv: list[str], cwd: str | None = None) -> str:
216
+ def _docker_run(argv: list[str], cwd: str | None = None, *, check: bool = False) -> str:
217
+ """docker 명령을 돌려 stdout 을 돌려준다.
218
+
219
+ `check=True` 면 0 이 아닌 종료를 PrepareError 로 올린다. status 가 빈 목록을,
220
+ down 이 성공을 보고하는데 실제로는 docker 가 실패한 경우를 막는다. config 계열
221
+ 조회는 실패해도 진행하도록 설계돼 있어 기본값은 그대로 둔다."""
217
222
  result = subprocess.run(argv, capture_output=True, text=True, check=False, cwd=cwd)
223
+ if check and result.returncode != 0:
224
+ detail = (result.stderr or result.stdout or "").strip().splitlines()
225
+ raise PrepareError(
226
+ f"`{' '.join(argv[:4])}` 가 exit {result.returncode} 로 실패했습니다: "
227
+ f"{detail[-1] if detail else '출력 없음'}")
218
228
  return result.stdout or ""
219
229
 
220
230
 
@@ -222,7 +232,7 @@ def query_containers_by_label(project_name: str) -> list[dict]:
222
232
  """라벨 SSOT(spec §11): `okstra.project-name` 으로 컨테이너를 질의한다.
223
233
 
224
234
  registry 가 아니라 docker 라벨이 컨테이너 존재의 기준값이다."""
225
- out = _docker_run([
235
+ out = _docker_run(check=True, argv=[
226
236
  "docker", "ps", "-a", "--filter",
227
237
  f"label=okstra.project-name={project_name}",
228
238
  "--format", (
@@ -431,7 +441,7 @@ def provision_container_group(
431
441
  raise PrepareError(
432
442
  f"container up: `up -d` 후 기동된 컨테이너가 없습니다(`docker compose -p "
433
443
  f"{project_name} ps -a` 로 확인). compose 정의·활성 profile 을 점검한 뒤 "
434
- f"`okstra container down --task-key {task_key}` 로 정리하고 다시 시도하세요.")
444
+ f"`{_down_hint(project_root, task_key)}` 로 정리하고 다시 시도하세요.")
435
445
  failed = poll_healthcheck(
436
446
  project_name=project_name, cwd=str(worktree), services=started,
437
447
  healthcheck_timeout_seconds=healthcheck_timeout_seconds,
@@ -445,8 +455,8 @@ def provision_container_group(
445
455
  raise PrepareError(
446
456
  f"container up: 다음 서비스가 시간 내 기동하지 못했습니다: {', '.join(failed)} "
447
457
  f"(timeout={healthcheck_timeout_seconds}s). `docker compose -p "
448
- f"{project_name} logs` 로 원인을 확인한 뒤 `okstra container down "
449
- f"--task-key {task_key}` 로 정리하세요.")
458
+ f"{project_name} logs` 로 원인을 확인한 뒤 "
459
+ f"`{_down_hint(project_root, task_key)}` 로 정리하세요.")
450
460
  # deploy-state 는 실제 기동된 started 만 기록한다 — full services 를 쓰면
451
461
  # profiles 로 안 뜬 서비스가 `down --all` 스윕의 대상으로 남는다.
452
462
  _write_deploy_state(cp, project_name=project_name, task_key=task_key,
@@ -528,12 +538,18 @@ def status_container_group(*, project_root, task_key: str) -> dict:
528
538
  "containers": query_containers_by_label(project_name)}
529
539
 
530
540
 
541
+ def _down_hint(project_root, task_key: str) -> str:
542
+ """실패 메시지가 안내하는 정리 명령. 파서가 요구하는 인자를 모두 싣는다."""
543
+ return f"okstra container down --project-root {project_root} --task-key {task_key}"
544
+
545
+
531
546
  def down_container_group(*, project_root, task_key: str = "", all_groups: bool = False) -> dict:
532
- """라벨 쿼리로 컨테이너를 down 한다."""
547
+ """compose 프로젝트 이름으로 task 의 컨테이너 그룹을 down 한다."""
533
548
  from .ids import compose_project_name
534
549
 
535
550
  project_root = Path(project_root)
536
551
  results = []
552
+ failures: list[str] = []
537
553
  for project_id, group, task_id in _down_targets(project_root, task_key, all_groups):
538
554
  project_name = compose_project_name(project_id, group, task_id)
539
555
  # `docker rm -f` 는 컨테이너만 지우고 compose 가 만든 네트워크를 누수시킨다.
@@ -541,9 +557,16 @@ def down_container_group(*, project_root, task_key: str = "", all_groups: bool =
541
557
  # 단위 컨테이너+네트워크를 회수한다. `-v` 는 의도적으로 빼서 compose 에
542
558
  # 선언된 named volume(예: DB 데이터)을 보존한다 — down 한 번에 사용자
543
559
  # 영속 데이터를 파괴하지 않는다.
544
- _docker_run(["docker", "compose", "-p", project_name, "down",
545
- "--remove-orphans"])
560
+ try:
561
+ _docker_run(["docker", "compose", "-p", project_name, "down",
562
+ "--remove-orphans"], check=True)
563
+ except PrepareError as exc:
564
+ failures.append(f"{project_name}: {exc}")
565
+ continue
546
566
  results.append({"projectName": project_name})
567
+ if failures:
568
+ # 나머지 대상은 끝까지 내리고, 실패한 것만 모아 알린다.
569
+ raise PrepareError("container down 실패 — " + "; ".join(failures))
547
570
  return {"downed": results}
548
571
 
549
572
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  The estimator answers a narrow operational question: how much file/context
4
4
  surface does the current task bundle ask the lead, analysis workers, and
5
- report-writer to absorb? It does not mutate task artifacts.
5
+ report-writer to absorb? It writes no report or run artifact; resolving the task
6
+ may record a finished implementation outcome (see `find_task_root`).
6
7
  """
7
8
  from __future__ import annotations
8
9
 
@@ -38,11 +39,9 @@ HOT_PATH_LEAD_RESOURCES = (
38
39
  "convergence",
39
40
  "report-writer",
40
41
  )
41
- WORKER_AGENT_FILES = (
42
- "claude-worker.md",
43
- "report-writer-worker.md",
44
- "translator-worker.md",
45
- )
42
+ # 모든 디스패치 프롬프트 앞머리에 실리는 계약. 종전에는 `~/.claude/agents` 의
43
+ # 에이전트 정의를 셌는데, 그 정의는 더 이상 설치되지 않는다(ADR-0017).
44
+ HOT_PATH_AGENT_CONTRACTS = ("agents/common.json",)
46
45
  TIMESTAMPED_ARTIFACT_RE = re.compile(r"\d{4}-\d{2}-\d{2}[_T]\d{2}-\d{2}-\d{2}")
47
46
 
48
47
 
@@ -136,7 +135,7 @@ def _duty_contract_files(
136
135
  if not isinstance(duty_root_value, str) or not duty_root_value:
137
136
  return []
138
137
  duty_root = project_root / duty_root_value
139
- files = [duty_root / "common.md", duty_root / f"{audience}.md"]
138
+ files = [duty_root / "common.json", duty_root / f"{audience}.json"]
140
139
  return [path for path in files if path.is_file()]
141
140
 
142
141
 
@@ -239,19 +238,15 @@ def _skill_assets_metric() -> dict:
239
238
  instruction footprint and are the prompt-diet ranking input."""
240
239
  entries = []
241
240
  okstra_home = _okstra_asset_home()
242
- claude_home = Path.home() / ".claude"
243
241
  for name in HOT_PATH_LEAD_RESOURCES:
244
242
  path = _installed_or_dev(
245
243
  okstra_home / "prompts" / "lead" / f"{name}.md",
246
244
  f"prompts/lead/{name}.md",
247
245
  )
248
246
  entries.append((f"resource:{name}", path))
249
- for fname in WORKER_AGENT_FILES:
250
- path = _installed_or_dev(
251
- claude_home / "agents" / fname,
252
- f"runtime/agents/workers/{fname}",
253
- )
254
- entries.append((f"agent:{fname}", path))
247
+ for relative in HOT_PATH_AGENT_CONTRACTS:
248
+ path = _installed_or_dev(okstra_home / relative, relative)
249
+ entries.append((f"contract:{relative}", path))
255
250
 
256
251
  files = []
257
252
  for label, path in entries:
@@ -512,7 +507,8 @@ _CLI_EPILOG = r"""Usage:
512
507
 
513
508
  Output: JSON with task file counts, current-run counts, lead Phase 1 read
514
509
  surface, analysis-worker initial read surface, and report-writer synthesis
515
- surface. This is read-only; it never mutates task artifacts.
510
+ surface. Resolving a task may record a finished implementation outcome in its task
511
+ manifest, the task catalog, and consumers.jsonl; nothing else is written.
516
512
  """
517
513
 
518
514