okstra 0.207.1 → 0.209.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 (133) hide show
  1. package/README.md +3 -2
  2. package/dist/cli-registry.mjs +6 -0
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/execute/render-bundle.mjs +1 -1
  5. package/dist/commands/lifecycle/doctor.mjs +1 -1
  6. package/dist/lib/skill-catalog.mjs +1 -0
  7. package/dist/lib/skill-catalog.mjs.map +1 -1
  8. package/docs/architecture/storage-model.md +14 -0
  9. package/docs/architecture.md +30 -9
  10. package/docs/cli.md +26 -22
  11. package/docs/contributor-change-matrix.md +1 -1
  12. package/docs/project-structure-overview.md +15 -8
  13. package/package.json +1 -1
  14. package/runtime/BUILD.json +2 -2
  15. package/runtime/agents/operations/explain-flow.json +6 -0
  16. package/runtime/bin/lib/okstra/cli.sh +1 -5
  17. package/runtime/bin/lib/okstra/globals.sh +0 -2
  18. package/runtime/bin/lib/okstra/usage.sh +5 -3
  19. package/runtime/bin/okstra.sh +0 -2
  20. package/runtime/prompts/duties/business-flow-investigator.json +14 -0
  21. package/runtime/prompts/lead/context-loader.md +1 -1
  22. package/runtime/prompts/lead/convergence.md +22 -7
  23. package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
  24. package/runtime/prompts/lead/report-writer.md +1 -1
  25. package/runtime/prompts/lead/team-contract.md +12 -17
  26. package/runtime/prompts/profiles/_common-contract.md +3 -3
  27. package/runtime/prompts/wizard/prompts.ko.json +0 -91
  28. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +6 -0
  29. package/runtime/python/okstra_ctl/agent/invocation.py +1 -1
  30. package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +1 -0
  31. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +1 -0
  32. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +11 -125
  33. package/runtime/python/okstra_ctl/agent/standalone.py +183 -0
  34. package/runtime/python/okstra_ctl/analysis_packet.py +39 -8
  35. package/runtime/python/okstra_ctl/assignment_resolver.py +7 -1
  36. package/runtime/python/okstra_ctl/brief_frontmatter.py +10 -0
  37. package/runtime/python/okstra_ctl/business_flow/__init__.py +4 -0
  38. package/runtime/python/okstra_ctl/business_flow/cli.py +134 -0
  39. package/runtime/python/okstra_ctl/business_flow/contracts.py +268 -0
  40. package/runtime/python/okstra_ctl/business_flow/engine.py +518 -0
  41. package/runtime/python/okstra_ctl/business_flow/hooks.py +221 -0
  42. package/runtime/python/okstra_ctl/business_flow/invocation.py +170 -0
  43. package/runtime/python/okstra_ctl/business_flow/report.py +49 -0
  44. package/runtime/python/okstra_ctl/business_flow/source.py +206 -0
  45. package/runtime/python/okstra_ctl/business_flow/store.py +388 -0
  46. package/runtime/python/okstra_ctl/cmux.py +21 -16
  47. package/runtime/python/okstra_ctl/convergence.py +173 -2
  48. package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +18 -0
  49. package/runtime/python/okstra_ctl/convergence_provenance.py +8 -0
  50. package/runtime/python/okstra_ctl/coverage_census.py +603 -0
  51. package/runtime/python/okstra_ctl/design_surfaces.py +4 -0
  52. package/runtime/python/okstra_ctl/direct_work.py +1 -1
  53. package/runtime/python/okstra_ctl/dispatch_core.py +7 -3
  54. package/runtime/python/okstra_ctl/doctor.py +12 -6
  55. package/runtime/python/okstra_ctl/domain/role.py +1 -0
  56. package/runtime/python/okstra_ctl/group_context.py +5 -4
  57. package/runtime/python/okstra_ctl/legacy_model_selection.py +7 -51
  58. package/runtime/python/okstra_ctl/manager_split.py +4 -1
  59. package/runtime/python/okstra_ctl/model_io/lines.py +1 -24
  60. package/runtime/python/okstra_ctl/model_io/renderers.py +54 -41
  61. package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.json +1 -1
  62. package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.md +0 -8
  63. package/runtime/python/okstra_ctl/phases/error_analysis/profile.json +1 -1
  64. package/runtime/python/okstra_ctl/phases/error_analysis/profile.md +6 -8
  65. package/runtime/python/okstra_ctl/phases/feature_analysis/profile.json +1 -1
  66. package/runtime/python/okstra_ctl/phases/feature_analysis/profile.md +0 -8
  67. package/runtime/python/okstra_ctl/phases/final_verification/profile.json +1 -1
  68. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -8
  69. package/runtime/python/okstra_ctl/phases/implementation/boundary.json +1 -1
  70. package/runtime/python/okstra_ctl/phases/implementation/profile.json +1 -1
  71. package/runtime/python/okstra_ctl/phases/implementation/profile.md +0 -6
  72. package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation-input.template.md +1 -1
  73. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +4 -3
  74. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +1 -14
  75. package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.json +1 -1
  76. package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +5 -8
  77. package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +3 -3
  78. package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +15 -5
  79. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +9 -1
  80. package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +1 -1
  81. package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +4 -2
  82. package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +25 -9
  83. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.json +1 -1
  84. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +7 -10
  85. package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.json +1 -1
  86. package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.md +4 -11
  87. package/runtime/python/okstra_ctl/phases/project_analysis/profile.json +1 -1
  88. package/runtime/python/okstra_ctl/phases/project_analysis/profile.md +0 -8
  89. package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +1 -1
  90. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
  91. package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.json +1 -1
  92. package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.md +14 -8
  93. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +3 -3
  94. package/runtime/python/okstra_ctl/phases/technical_verification/profile.json +1 -1
  95. package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +0 -4
  96. package/runtime/python/okstra_ctl/plan_items.py +1 -1
  97. package/runtime/python/okstra_ctl/render.py +10 -43
  98. package/runtime/python/okstra_ctl/render_final_report.py +3 -0
  99. package/runtime/python/okstra_ctl/report_assembly.py +15 -1
  100. package/runtime/python/okstra_ctl/report_finalize.py +50 -0
  101. package/runtime/python/okstra_ctl/report_html/render.py +3 -0
  102. package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -2
  103. package/runtime/python/okstra_ctl/run.py +78 -409
  104. package/runtime/python/okstra_ctl/wizard/__init__.py +2 -24
  105. package/runtime/python/okstra_ctl/wizard/cli.py +3 -6
  106. package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -35
  107. package/runtime/python/okstra_ctl/wizard/engine.py +2 -4
  108. package/runtime/python/okstra_ctl/wizard/ids.py +1 -88
  109. package/runtime/python/okstra_ctl/wizard/registry.py +36 -228
  110. package/runtime/python/okstra_ctl/wizard/render.py +2 -2
  111. package/runtime/python/okstra_ctl/wizard/roles.py +1 -3
  112. package/runtime/python/okstra_ctl/wizard/sources.py +9 -40
  113. package/runtime/python/okstra_ctl/wizard/state.py +36 -145
  114. package/runtime/python/okstra_ctl/wizard/statefile.py +27 -128
  115. package/runtime/python/okstra_ctl/wizard/steps_identity.py +22 -10
  116. package/runtime/python/okstra_ctl/wizard/steps_options.py +5 -4
  117. package/runtime/python/okstra_ctl/wizard/steps_roles.py +15 -565
  118. package/runtime/python/okstra_ctl/worker_prompt_policy.py +9 -2
  119. package/runtime/schemas/business-flow-v1.schema.json +847 -0
  120. package/runtime/schemas/convergence-groups-v2.0.schema.json +7 -0
  121. package/runtime/skills/okstra-explain-flow/SKILL.md +42 -0
  122. package/runtime/skills/okstra-inspect/facets/history.md +5 -5
  123. package/runtime/skills/okstra-run/SKILL.md +2 -2
  124. package/runtime/templates/reports/business-flow.template.md +106 -0
  125. package/runtime/templates/reports/html/base.template.html +14 -1
  126. package/runtime/templates/reports/html/business-flow.template.html +31 -0
  127. package/runtime/templates/reports/html/i18n/en.json +1 -0
  128. package/runtime/templates/reports/html/i18n/ko.json +1 -0
  129. package/runtime/templates/worker-prompt-preamble.md +11 -2
  130. package/runtime/validators/checks/validate-prompt-metadata-01.py +10 -10
  131. package/runtime/validators/validate-run.py +70 -21
  132. package/runtime/validators/validate_analysis_report.py +21 -21
  133. package/runtime/python/okstra_ctl/workers.py +0 -133
@@ -1,70 +1,22 @@
1
- """roles 단계 — reuse previous, executor, critic, defaults/custom, workers, 역할별 모델 픽의 build/submit."""
1
+ """roles 단계 — 직전 run 입력 재사용(reuse previous)의 build/submit."""
2
2
  from __future__ import annotations
3
3
 
4
4
  from typing import Optional
5
5
 
6
- from okstra_ctl.application.resolve_assignment import resolve_lead_provider
7
- from okstra_ctl.domain.host import (
8
- HostCapabilityMismatch,
9
- HostNotRegistered,
10
- ProviderUnavailable,
11
- )
12
- from okstra_ctl.registry.host_registry import default_host_registry
13
- from okstra_ctl.registry.provider_registry import default_provider_registry
14
6
  from okstra_ctl.ids import slugify_task_segment
15
7
  from okstra_ctl.analysis_inputs import AnalysisInputError
16
8
  from okstra_ctl.phases.feature_analysis.entry import resolve_analysis_target
17
- from okstra_ctl.models import (
18
- UnknownModelError,
19
- UnknownProviderError,
20
- provider_ids,
21
- provider_supports_role,
22
- resolve_model_metadata,
23
- )
24
- from okstra_ctl.run import recommended_role_models
25
- from okstra_ctl.workers import (
26
- WorkersError,
27
- normalize_workers,
28
- validate_workers_against_profile,
29
- )
9
+ from okstra_ctl.models import UnknownModelError, resolve_model_metadata
30
10
 
31
- from .ids import (
32
- ANTIGRAVITY_MODEL_OPTIONS,
33
- CLAUDE_MODEL_OPTIONS,
34
- CODEX_MODEL_OPTIONS,
35
- EXECUTORS,
36
- GROK_MODEL_OPTIONS,
37
- KIMI_MODEL_OPTIONS,
38
- PICK_TYPE_CUSTOM,
39
- ROLE_BLURBS,
40
- S_ANTIGRAVITY_MODEL,
41
- S_CLAUDE_MODEL,
42
- S_CODEX_MODEL,
43
- S_CRITIC_PICK,
44
- S_CRITIC_TEXT,
45
- S_DEFAULTS_OR_CUSTOM,
46
- S_EXECUTOR,
47
- S_EXECUTOR_MODEL,
48
- S_GROK_MODEL,
49
- S_KIMI_MODEL,
50
- S_LEAD_MODEL,
51
- S_REPORT_WRITER_MODEL,
52
- S_REUSE_PREVIOUS,
53
- S_WORKERS_CUSTOM,
54
- S_WORKERS_OVERRIDE,
55
- _DEFAULT_ROSTER_TOKEN,
56
- _REUSE_FILLED_STEPS,
57
- )
11
+ from .ids import S_REUSE_PREVIOUS, _REUSE_FILLED_STEPS
58
12
  from .state import (
59
- Option,
60
13
  Prompt,
61
14
  WizardError,
62
15
  WizardState,
63
- _convert_v1_provider_selections,
64
- _role_selection_enabled,
16
+ _role_selections_from_run_inputs,
65
17
  )
66
18
  from .prompts import _opt, _p
67
- from .sources import _latest_run_inputs, _resolved_roster, _suggest_latest_final_report
19
+ from .sources import _latest_run_inputs, _suggest_latest_final_report
68
20
  from .steps_analysis import _restore_analysis_evidence_inputs
69
21
 
70
22
 
@@ -86,103 +38,6 @@ def _validate_model(provider: str, raw: str) -> str:
86
38
  return raw
87
39
 
88
40
 
89
- def _executor_model_field(executor: str) -> str:
90
- return {"claude": "claude_model",
91
- "codex": "codex_model",
92
- "antigravity": "antigravity_model"}[executor]
93
-
94
-
95
- def _executor_model_options(executor: str) -> list[str]:
96
- return {"claude": CLAUDE_MODEL_OPTIONS,
97
- "codex": CODEX_MODEL_OPTIONS,
98
- "antigravity": ANTIGRAVITY_MODEL_OPTIONS}[executor]
99
-
100
-
101
- def _critic_provider_choices() -> list[str]:
102
- return list(EXECUTORS)
103
-
104
-
105
- def _critic_choices() -> list[str]:
106
- return list(_critic_provider_choices())
107
-
108
-
109
- def _critic_provider_label(provider: str, t: dict) -> str:
110
- labels = t.get("labels", {})
111
- if provider == "claude":
112
- template = labels.get("provider_recommended", "{provider} critic (recommended)")
113
- else:
114
- template = labels.get("provider", "{provider} critic")
115
- return template.format(provider=provider)
116
-
117
-
118
- def _build_critic_pick(state: WizardState) -> Prompt:
119
- t = _p(state.workspace_root, "critic_pick")
120
- options = [
121
- _opt(provider, _critic_provider_label(provider, t))
122
- for provider in _critic_provider_choices()
123
- ]
124
- return Prompt(
125
- step=S_CRITIC_PICK, kind="pick",
126
- label=t["label"],
127
- options=options,
128
- echo_template=t["echo_template"],
129
- )
130
-
131
-
132
- def _submit_critic_pick(state: WizardState, value: str) -> Optional[str]:
133
- choice = (value or "").strip().lower()
134
- choices = _critic_choices()
135
- if choice not in choices:
136
- raise WizardError(f"critic must be one of {choices}, got: {value!r}")
137
- state.critic = choice
138
- state.critic_pending_text = False
139
- return f"critic: {choice}"
140
-
141
-
142
- def _build_critic_text(state: WizardState) -> Prompt:
143
- t = _p(state.workspace_root, "critic_text")
144
- options = [
145
- _opt(provider, _critic_provider_label(provider, t))
146
- for provider in _critic_provider_choices()
147
- ]
148
- return Prompt(
149
- step=S_CRITIC_TEXT, kind="pick",
150
- label=t["label"],
151
- options=options,
152
- echo_template=t["echo_template"],
153
- )
154
-
155
-
156
- def _submit_critic_text(state: WizardState, value: str) -> Optional[str]:
157
- choice = (value or "").strip().lower()
158
- providers = _critic_provider_choices()
159
- if choice not in providers:
160
- raise WizardError(f"critic must be one of {providers}, got: {value!r}")
161
- state.critic = choice
162
- state.critic_pending_text = False
163
- return f"critic: {choice}"
164
-
165
-
166
- def _build_executor(state: WizardState) -> Prompt:
167
- t = _p(state.workspace_root, "executor")
168
- default_suffix = t["options"].get("_DEFAULT_SUFFIX", "")
169
- options = [_opt(e, e + (default_suffix if e == "claude" else ""))
170
- for e in EXECUTORS]
171
- return Prompt(
172
- step=S_EXECUTOR, kind="pick",
173
- label=t["label"],
174
- options=options,
175
- echo_template=t["echo_template"],
176
- )
177
-
178
-
179
- def _submit_executor(state: WizardState, value: str) -> Optional[str]:
180
- if value not in EXECUTORS:
181
- raise WizardError(f"executor must be one of {EXECUTORS}, got: {value!r}")
182
- state.executor = value
183
- return f"executor: {value}"
184
-
185
-
186
41
  def _safe_model(provider: str, raw: object) -> str:
187
42
  """run-inputs 의 모델 display 값을 wizard 필드로 환원. 알 수 없는 값은
188
43
  빈 문자열(phase 기본값)로 안전하게 떨어뜨린다."""
@@ -194,13 +49,6 @@ def _safe_model(provider: str, raw: object) -> str:
194
49
  return ""
195
50
 
196
51
 
197
- def _safe_provider(raw: object, role: str) -> str:
198
- provider = raw.strip().lower() if isinstance(raw, str) else ""
199
- if not provider:
200
- return ""
201
- return provider if provider_supports_role(provider, role) else ""
202
-
203
-
204
52
  def _assignment_model(inputs: dict, worker_id: str, provider: str) -> str:
205
53
  assignments = inputs.get("workerAssignments")
206
54
  if not isinstance(assignments, list):
@@ -224,44 +72,6 @@ def _build_reuse_previous(state: WizardState) -> Prompt:
224
72
  )
225
73
 
226
74
 
227
- def _restore_reused_lead(state: WizardState, inputs: dict) -> None:
228
- """리드는 지금 host 에서 파생한다. 직전 run 의 기록을 물려받지 않는다.
229
-
230
- run-inputs 의 ``leadProvider`` 는 그 run 이 어느 host 에서 돌았는지의 기록이지
231
- 사용자가 고른 값이 아니다. wizard 에는 리드 provider 를 묻는 단계 자체가 없고
232
- ``resolve_lead_provider`` 가 host 의 네이티브 provider 로 파생한다. 그 기록을
233
- 그대로 요청으로 되먹이면 Codex 에서 돌린 run 을 Claude Code 에서 이어받을 때
234
- host 검사에 걸려 brief/workers/모델 복원까지 통째로 막힌다.
235
-
236
- 네이티브 리드가 없는 host(``external``) 에서만 기록된 값이 유일한 단서이므로
237
- 그때만 참고한다.
238
- """
239
- try:
240
- host = default_host_registry().resolve(state.host_runtime).descriptor
241
- requested_provider = (
242
- ""
243
- if host.native_provider_id
244
- else _safe_provider(inputs.get("leadProvider"), "lead")
245
- )
246
- assignment = resolve_lead_provider(
247
- host_id=state.host_runtime,
248
- requested_provider=requested_provider,
249
- host_registry=default_host_registry(),
250
- provider_registry=default_provider_registry(),
251
- )
252
- except (
253
- HostCapabilityMismatch,
254
- HostNotRegistered,
255
- ProviderUnavailable,
256
- UnknownProviderError,
257
- ) as exc:
258
- raise WizardError(
259
- f"previous run cannot be reused: {exc}; choose step-by-step re-entry"
260
- ) from exc
261
- state.lead_provider = assignment.provider
262
- state.lead_model = _safe_model(assignment.provider, inputs.get("leadModel"))
263
-
264
-
265
75
  def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
266
76
  if value not in ("yes", "no"):
267
77
  raise WizardError(f"expected 'yes' or 'no', got: {value!r}")
@@ -271,31 +81,13 @@ def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
271
81
  state.reuse_previous = True
272
82
  seg = slugify_task_segment(state.task_type)
273
83
  inputs = _latest_run_inputs(state, phase_segment=seg)
274
- state.use_defaults = False
275
- workers = inputs.get("workers")
276
- state.workers_override = (
277
- ",".join(w for w in workers if isinstance(w, str))
278
- if isinstance(workers, list) else "")
279
- _restore_reused_lead(state, inputs)
280
- state.claude_model = _safe_model("claude", inputs.get("claudeModel"))
281
- state.codex_model = _safe_model("codex", inputs.get("codexModel"))
282
- state.antigravity_model = _safe_model("antigravity", inputs.get("antigravityModel"))
283
- state.grok_model = _assignment_model(inputs, "grok", "grok")
284
- state.kimi_model = _assignment_model(inputs, "kimi", "kimi")
285
- state.report_writer_provider = _safe_provider(
286
- inputs.get("reportWriterProvider"), "report-writer",
287
- )
288
- report_writer_provider = state.report_writer_provider or "claude"
289
- state.report_writer_model = _safe_model(
290
- report_writer_provider, inputs.get("reportWriterModel"))
291
- if _role_selection_enabled(state):
292
- conversion_payload = dict(inputs)
293
- conversion_payload.setdefault("grokModel", state.grok_model)
294
- conversion_payload.setdefault("kimiModel", state.kimi_model)
295
- state.role_counts.clear()
296
- state.role_models.clear()
297
- state.role_selection_order.clear()
298
- _convert_v1_provider_selections(state, conversion_payload)
84
+ conversion_payload = dict(inputs)
85
+ conversion_payload.setdefault("grokModel", _assignment_model(inputs, "grok", "grok"))
86
+ conversion_payload.setdefault("kimiModel", _assignment_model(inputs, "kimi", "kimi"))
87
+ state.role_counts.clear()
88
+ state.role_models.clear()
89
+ state.role_selection_order.clear()
90
+ _role_selections_from_run_inputs(state, conversion_payload)
299
91
  directive = inputs.get("directive")
300
92
  state.directive = directive if isinstance(directive, str) else ""
301
93
  related = inputs.get("relatedTasks")
@@ -325,349 +117,7 @@ def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
325
117
  for sid in _REUSE_FILLED_STEPS:
326
118
  if sid not in state.answered:
327
119
  state.answered.append(sid)
328
- return (f"reuse-previous: yes "
329
- f"(workers={state.workers_override or 'profile-default'}, "
330
- f"lead-model={state.lead_model or 'default'})")
331
-
332
-
333
- def _role_model_lines(state: WizardState) -> str:
334
- """이번 run 에서 실제로 모델을 고르게 되는 역할만, 추천 모델과 함께 나열한다.
335
- 뒤따르는 *_model 단계의 등장 조건과 1:1 로 맞춰 안내와 실제 화면이 어긋나지
336
- 않게 한다 (그래서 분석에 참여하지 않는 antigravity 는 executor 일 때만 나온다)."""
337
- # 이 화면의 lead 줄은 뒤따르는 lead-model picker 와 같은 provider 를
338
- # 봐야 한다 — picker 는 호스트의 native provider 만 제시한다.
339
- rec = recommended_role_models(
340
- lead_provider=default_host_registry()
341
- .resolve(state.host_runtime)
342
- .descriptor.native_provider_id
343
- or "",
344
- report_writer_provider=state.report_writer_provider or "claude",
345
- )
346
- roster = _resolved_roster(state)
347
- impl = state.task_type == "implementation"
348
-
349
- def line(role_label: str, blurb: str, model: str) -> str:
350
- return f" · {role_label} ({blurb}) — 추천 {model}"
351
-
352
- lines = [line("lead", ROLE_BLURBS["lead"], rec["lead"])]
353
- if impl:
354
- ex = state.executor or "claude"
355
- lines.append(line(f"executor={ex}", "코드 구현 실행자", rec.get(ex, "auto")))
356
- else:
357
- for w in provider_ids("analyser"):
358
- if w in roster:
359
- lines.append(line(w, ROLE_BLURBS[w], rec[w]))
360
- if impl or "report-writer" in roster:
361
- lines.append(line("report-writer", ROLE_BLURBS["report-writer"],
362
- rec["report-writer"]))
363
- return "\n".join(lines)
364
-
365
-
366
- def _build_defaults_or_custom(state: WizardState) -> Prompt:
367
- t = _p(state.workspace_root, "defaults_or_custom",
368
- role_models=_role_model_lines(state))
369
- return Prompt(
370
- step=S_DEFAULTS_OR_CUSTOM, kind="pick",
371
- label=t["label"],
372
- options=[_opt(k, v) for k, v in t["options"].items()],
373
- echo_template=t["echo_template"],
374
- )
375
-
376
-
377
- def _submit_defaults_or_custom(state: WizardState, value: str) -> Optional[str]:
378
- if value not in ("defaults", "customize"):
379
- raise WizardError(f"expected 'defaults' or 'customize', got: {value!r}")
380
- state.use_defaults = value == "defaults"
381
- mode = ("defaults (recommended models as-is)"
382
- if state.use_defaults
383
- else "customize (manual model pick)")
384
- return f"model-mode: {mode}"
385
-
386
-
387
- def _analyser_choices(state: WizardState) -> list[str]:
388
- """프로필이 분석 워커로 허용하는 전체 후보 (report-writer 제외)."""
389
- return [
390
- w for w in (state.profile_workers + state.profile_optional_workers)
391
- if w != "report-writer"
392
- ]
393
-
394
-
395
- def _default_analysers(state: WizardState) -> list[str]:
396
- """옵션 워커를 하나도 고르지 않았을 때 쓰는 기본 분석 로스터."""
397
- return [w for w in state.profile_workers if w != "report-writer"]
398
-
399
-
400
- def _analyser_option(state: WizardState, worker: str, suffix: str) -> Option:
401
- label = f"{worker}{suffix}" if worker in state.profile_optional_workers else worker
402
- return _opt(value=worker, label=label)
403
-
404
-
405
- def _finalize_workers(state: WizardState, workers: list[str]) -> str:
406
- """정규화 → 프로필 allowlist 검증 → report-writer 강제 포함까지의 확정 경로.
407
- 두 워커 단계가 공유한다."""
408
- try:
409
- chosen = normalize_workers(",".join(workers))
410
- validate_workers_against_profile(
411
- chosen,
412
- state.profile_workers,
413
- state.profile_optional_workers,
414
- )
415
- except WorkersError as exc:
416
- raise WizardError(str(exc))
417
- # report-writer 는 프로필이 Required 로 선언했을 때만 강제 포함.
418
- if ("report-writer" in state.profile_workers
419
- and "report-writer" not in chosen):
420
- chosen.append("report-writer")
421
- state.workers_override = ",".join(chosen)
422
- return f"workers: {state.workers_override}"
423
-
424
-
425
- def _build_workers_override(state: WizardState) -> Prompt:
426
- """분석 워커 멀티픽. 기본 로스터(예: claude·codex)는 매 run 사실상 고정이라
427
- 옵션에서 빼고 결과에 항상 포함시킨다 — 화면에는 '기본 그대로' + 옵션 워커 +
428
- '직접 선택'만 남는다. report-writer 도 같은 이유로 빠진다. 기본 로스터에서
429
- 워커를 빼는 축소는 '직접 선택'(`workers_custom`)에서만 가능하다."""
430
- t = _p(state.workspace_root, "workers_override")
431
- labels = t["labels"]
432
- options = [_opt(
433
- _DEFAULT_ROSTER_TOKEN,
434
- labels["default_roster"].format(
435
- workers=" + ".join(_default_analysers(state))),
436
- )]
437
- for w in state.profile_optional_workers:
438
- options.append(_opt(w, labels["add_optional"].format(worker=w)))
439
- options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
440
- return Prompt(
441
- step=S_WORKERS_OVERRIDE, kind="pick", multi=True,
442
- label=t["label"],
443
- options=options,
444
- echo_template=t["echo_template"],
445
- )
446
-
447
-
448
- def _submit_workers_override(state: WizardState, value: str) -> Optional[str]:
449
- t = _p(state.workspace_root, "workers_override")
450
- picked = [v.strip() for v in (value or "").split(",") if v.strip()]
451
- if not picked:
452
- raise WizardError(t["errors"]["min_one_required"])
453
- if PICK_TYPE_CUSTOM in picked:
454
- if len(picked) > 1:
455
- raise WizardError(t["errors"]["custom_must_be_alone"])
456
- state.workers_custom_pending = True
457
- return None
458
- allowed = {_DEFAULT_ROSTER_TOKEN, *state.profile_optional_workers}
459
- unknown = [w for w in picked if w not in allowed]
460
- if unknown:
461
- raise WizardError(
462
- t["errors"]["unknown_option"].format(values=",".join(unknown)))
463
- state.workers_custom_pending = False
464
- added = [w for w in picked if w != _DEFAULT_ROSTER_TOKEN]
465
- return _finalize_workers(state, _default_analysers(state) + added)
466
-
467
-
468
- def _build_workers_custom(state: WizardState) -> Prompt:
469
- """'직접 선택' 화면 — 기본 로스터까지 포함한 전체 분석 워커 후보.
470
- 기본 워커를 빼는 축소는 이 화면에서만 가능하다."""
471
- t = _p(state.workspace_root, "workers_custom")
472
- suffix = t["options"].get("_OPTIONAL_SUFFIX", "")
473
- return Prompt(
474
- step=S_WORKERS_CUSTOM, kind="pick", multi=True,
475
- label=t["label"],
476
- options=[_analyser_option(state, w, suffix)
477
- for w in _analyser_choices(state)],
478
- echo_template=t["echo_template"],
479
- )
480
-
481
-
482
- def _submit_workers_custom(state: WizardState, value: str) -> Optional[str]:
483
- picked = [v.strip() for v in (value or "").split(",") if v.strip()]
484
- if not picked:
485
- t = _p(state.workspace_root, "workers_custom")
486
- raise WizardError(t["errors"]["min_one_required"])
487
- state.workers_custom_pending = False
488
- return _finalize_workers(state, picked)
489
-
490
-
491
- def _model_pick(step: str, label: str, options: list[str], echo: str) -> Prompt:
492
- # "default" picks the role's recommended model — leaving it here yields
493
- # the SAME result as the 'Use defaults' branch. Spell that out on the
494
- # label so default ↔ customize never reads as "no difference".
495
- opts = [
496
- _opt(o, "default (recommended model)" if o == "default" else o)
497
- for o in options
498
- ]
499
- return Prompt(step=step, kind="pick", label=label,
500
- options=opts, echo_template=echo)
501
-
502
-
503
- def _qualified_role_model_options(role: str) -> list[str]:
504
- registry = default_provider_registry()
505
- options = ["default", *_provider_picker_options("claude", registry)]
506
- for provider in registry.ids(role):
507
- if provider == "claude":
508
- continue
509
- options.extend(
510
- f"{provider}:{model}"
511
- for model in _provider_picker_options(provider, registry)
512
- )
513
- return options
514
-
515
-
516
- def _provider_picker_options(provider: str, registry=None) -> list[str]:
517
- provider_registry = registry or default_provider_registry()
518
- return [
519
- alias
520
- for alias, model in provider_registry.resolve(provider).models.items()
521
- if model.in_picker
522
- ]
523
-
524
-
525
- def _submit_qualified_role_model(
526
- state: WizardState, value: str, role: str, model_field: str, provider_field: str,
527
- ) -> str:
528
- raw = (value or "").strip()
529
- if ":" not in raw:
530
- setattr(state, provider_field, "")
531
- setattr(state, model_field, _validate_model("claude", raw))
532
- return getattr(state, model_field) or "default"
533
- provider, model = (part.strip().lower() for part in raw.split(":", 1))
534
- if not provider_supports_role(provider, role):
535
- raise WizardError(f"provider {provider!r} does not support the {role} role")
536
- setattr(state, provider_field, provider)
537
- setattr(state, model_field, _validate_model(provider, model))
538
- return raw
539
-
540
-
541
- def _build_lead_model(state: WizardState) -> Prompt:
542
- t = _p(state.workspace_root, "lead_model")
543
- provider = default_host_registry().resolve(
544
- state.host_runtime
545
- ).descriptor.native_provider_id
546
- options = (
547
- ["default", *_provider_picker_options(provider)]
548
- if provider else _qualified_role_model_options("lead")
549
- )
550
- return _model_pick(S_LEAD_MODEL, t["label"], options, t["echo_template"])
551
-
552
-
553
- def _submit_lead_model(state: WizardState, value: str) -> Optional[str]:
554
- native_provider = default_host_registry().resolve(
555
- state.host_runtime
556
- ).descriptor.native_provider_id
557
- if native_provider:
558
- raw = (value or "").strip()
559
- requested_provider = ""
560
- if ":" in raw:
561
- requested_provider, raw = (
562
- part.strip().lower() for part in raw.split(":", 1)
563
- )
564
- try:
565
- assignment = resolve_lead_provider(
566
- host_id=state.host_runtime,
567
- requested_provider=requested_provider,
568
- host_registry=default_host_registry(),
569
- provider_registry=default_provider_registry(),
570
- )
571
- except (
572
- HostCapabilityMismatch,
573
- HostNotRegistered,
574
- ProviderUnavailable,
575
- UnknownProviderError,
576
- ) as exc:
577
- raise WizardError(str(exc)) from exc
578
- state.lead_provider = assignment.provider
579
- state.lead_model = _validate_model(assignment.provider, raw)
580
- return f"lead-model: {assignment.provider}:{state.lead_model or 'default'}"
581
- selected = _submit_qualified_role_model(
582
- state, value, "lead", "lead_model", "lead_provider",
583
- )
584
- return f"lead-model: {selected}"
585
-
586
-
587
- def _build_executor_model(state: WizardState) -> Prompt:
588
- t = _p(state.workspace_root, "executor_model", executor=state.executor)
589
- return _model_pick(
590
- S_EXECUTOR_MODEL,
591
- t["label"],
592
- _executor_model_options(state.executor),
593
- t["echo_template"].replace("{executor}", state.executor),
594
- )
595
-
596
-
597
- def _submit_executor_model(state: WizardState, value: str) -> Optional[str]:
598
- resolved = _validate_model(state.executor, value)
599
- setattr(state, _executor_model_field(state.executor), resolved)
600
- return f"{state.executor}-model: {resolved or 'default'}"
601
-
602
-
603
- def _build_claude_model(state: WizardState) -> Prompt:
604
- t = _p(state.workspace_root, "claude_model")
605
- return _model_pick(S_CLAUDE_MODEL, t["label"],
606
- CLAUDE_MODEL_OPTIONS, t["echo_template"])
607
-
608
-
609
- def _submit_claude_model(state: WizardState, value: str) -> Optional[str]:
610
- state.claude_model = _validate_model("claude", value)
611
- return f"claude-model: {state.claude_model or 'default'}"
612
-
613
-
614
- def _build_codex_model(state: WizardState) -> Prompt:
615
- t = _p(state.workspace_root, "codex_model")
616
- return _model_pick(S_CODEX_MODEL, t["label"],
617
- CODEX_MODEL_OPTIONS, t["echo_template"])
618
-
619
-
620
- def _submit_codex_model(state: WizardState, value: str) -> Optional[str]:
621
- state.codex_model = _validate_model("codex", value)
622
- return f"codex-model: {state.codex_model or 'default'}"
623
-
624
-
625
- def _build_antigravity_model(state: WizardState) -> Prompt:
626
- t = _p(state.workspace_root, "antigravity_model")
627
- return _model_pick(S_ANTIGRAVITY_MODEL, t["label"],
628
- ANTIGRAVITY_MODEL_OPTIONS, t["echo_template"])
629
-
630
-
631
- def _submit_antigravity_model(state: WizardState, value: str) -> Optional[str]:
632
- state.antigravity_model = _validate_model("antigravity", value)
633
- return f"antigravity-model: {state.antigravity_model or 'default'}"
634
-
635
-
636
- def _build_grok_model(state: WizardState) -> Prompt:
637
- t = _p(state.workspace_root, "grok_model")
638
- return _model_pick(
639
- S_GROK_MODEL, t["label"], GROK_MODEL_OPTIONS, t["echo_template"],
640
- )
641
-
642
-
643
- def _submit_grok_model(state: WizardState, value: str) -> Optional[str]:
644
- state.grok_model = _validate_model("grok", value)
645
- return f"grok-model: {state.grok_model or 'default'}"
646
-
647
-
648
- def _build_kimi_model(state: WizardState) -> Prompt:
649
- t = _p(state.workspace_root, "kimi_model")
650
- return _model_pick(
651
- S_KIMI_MODEL, t["label"], KIMI_MODEL_OPTIONS, t["echo_template"],
652
- )
653
-
654
-
655
- def _submit_kimi_model(state: WizardState, value: str) -> Optional[str]:
656
- state.kimi_model = _validate_model("kimi", value)
657
- return f"kimi-model: {state.kimi_model or 'default'}"
658
-
659
-
660
- def _build_report_writer_model(state: WizardState) -> Prompt:
661
- t = _p(state.workspace_root, "report_writer_model")
662
- return _model_pick(S_REPORT_WRITER_MODEL,
663
- t["label"],
664
- _qualified_role_model_options("report-writer"),
665
- t["echo_template"])
666
-
667
-
668
- def _submit_report_writer_model(state: WizardState, value: str) -> Optional[str]:
669
- selected = _submit_qualified_role_model(
670
- state, value, "report-writer", "report_writer_model",
671
- "report_writer_provider",
120
+ selected = ", ".join(
121
+ f"{role}={','.join(models)}" for role, models in state.role_models.items()
672
122
  )
673
- return f"report-writer-model: {selected}"
123
+ return f"reuse-previous: yes (roles: {selected or 'profile-default'})"
@@ -100,6 +100,9 @@ REVERIFY_DISPATCH_KIND_PREFIX = "reverify-r"
100
100
  # 렌더러는 `okstra plan-items prompt` 라 통과할 값이 하나도 없었다. 라운드가
101
101
  # 한 번도 열리지 못한 채 `planBodyVerification.roundCount` 가 0 으로 남았다.
102
102
  PLAN_VERIFY_DISPATCH_KIND_PREFIX = "plan-verify-r"
103
+ # The one coverage-census retry: the same analyser, only the cells its first
104
+ # result left unjudged. Unnumbered because the retry count is fixed at one.
105
+ CENSUS_GAPFILL_DISPATCH_KIND = "census-gapfill"
103
106
 
104
107
 
105
108
  def _numbered_round(dispatch_kind: str, prefix: str) -> int | None:
@@ -202,8 +205,12 @@ def resolve_prompt_plan(
202
205
  # Its body is deliberately unlike the initial one (it asks for coverage gaps
203
206
  # and unrequested work, not findings), so grouping it with the initial
204
207
  # analysis prompts makes `validate_analysis_prompt_set` compare two prompts
205
- # that are *supposed* to differ and fail every critic-enabled run.
206
- analysis_equality_group = None if dispatch_kind == "critic" else "analysis-core"
208
+ # that are *supposed* to differ and fail every critic-enabled run. A census
209
+ # gap-fill body lists one worker's missing cells, so it differs the same way.
210
+ analysis_equality_group = (
211
+ None if dispatch_kind in ("critic", CENSUS_GAPFILL_DISPATCH_KIND)
212
+ else "analysis-core"
213
+ )
207
214
  analysis_duty: AgentAudience = (
208
215
  critic_duty_for_task_type(task_type)
209
216
  if dispatch_kind == "critic"