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.
- package/README.md +3 -2
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +1 -1
- package/dist/commands/lifecycle/doctor.mjs +1 -1
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/docs/architecture/storage-model.md +14 -0
- package/docs/architecture.md +30 -9
- package/docs/cli.md +26 -22
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +15 -8
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/operations/explain-flow.json +6 -0
- package/runtime/bin/lib/okstra/cli.sh +1 -5
- package/runtime/bin/lib/okstra/globals.sh +0 -2
- package/runtime/bin/lib/okstra/usage.sh +5 -3
- package/runtime/bin/okstra.sh +0 -2
- package/runtime/prompts/duties/business-flow-investigator.json +14 -0
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +22 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +12 -17
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/wizard/prompts.ko.json +0 -91
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +6 -0
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +11 -125
- package/runtime/python/okstra_ctl/agent/standalone.py +183 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +39 -8
- package/runtime/python/okstra_ctl/assignment_resolver.py +7 -1
- package/runtime/python/okstra_ctl/brief_frontmatter.py +10 -0
- package/runtime/python/okstra_ctl/business_flow/__init__.py +4 -0
- package/runtime/python/okstra_ctl/business_flow/cli.py +134 -0
- package/runtime/python/okstra_ctl/business_flow/contracts.py +268 -0
- package/runtime/python/okstra_ctl/business_flow/engine.py +518 -0
- package/runtime/python/okstra_ctl/business_flow/hooks.py +221 -0
- package/runtime/python/okstra_ctl/business_flow/invocation.py +170 -0
- package/runtime/python/okstra_ctl/business_flow/report.py +49 -0
- package/runtime/python/okstra_ctl/business_flow/source.py +206 -0
- package/runtime/python/okstra_ctl/business_flow/store.py +388 -0
- package/runtime/python/okstra_ctl/cmux.py +21 -16
- package/runtime/python/okstra_ctl/convergence.py +173 -2
- package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +18 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +8 -0
- package/runtime/python/okstra_ctl/coverage_census.py +603 -0
- package/runtime/python/okstra_ctl/design_surfaces.py +4 -0
- package/runtime/python/okstra_ctl/direct_work.py +1 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +7 -3
- package/runtime/python/okstra_ctl/doctor.py +12 -6
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/group_context.py +5 -4
- package/runtime/python/okstra_ctl/legacy_model_selection.py +7 -51
- package/runtime/python/okstra_ctl/manager_split.py +4 -1
- package/runtime/python/okstra_ctl/model_io/lines.py +1 -24
- package/runtime/python/okstra_ctl/model_io/renderers.py +54 -41
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/error_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/error_analysis/profile.md +6 -8
- package/runtime/python/okstra_ctl/phases/feature_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/feature_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/final_verification/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -8
- package/runtime/python/okstra_ctl/phases/implementation/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +0 -6
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +4 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +1 -14
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +5 -8
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +3 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +15 -5
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +9 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +4 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +25 -9
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +7 -10
- package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.md +4 -11
- package/runtime/python/okstra_ctl/phases/project_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/project_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.md +14 -8
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +3 -3
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +0 -4
- package/runtime/python/okstra_ctl/plan_items.py +1 -1
- package/runtime/python/okstra_ctl/render.py +10 -43
- package/runtime/python/okstra_ctl/render_final_report.py +3 -0
- package/runtime/python/okstra_ctl/report_assembly.py +15 -1
- package/runtime/python/okstra_ctl/report_finalize.py +50 -0
- package/runtime/python/okstra_ctl/report_html/render.py +3 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -2
- package/runtime/python/okstra_ctl/run.py +78 -409
- package/runtime/python/okstra_ctl/wizard/__init__.py +2 -24
- package/runtime/python/okstra_ctl/wizard/cli.py +3 -6
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -35
- package/runtime/python/okstra_ctl/wizard/engine.py +2 -4
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -88
- package/runtime/python/okstra_ctl/wizard/registry.py +36 -228
- package/runtime/python/okstra_ctl/wizard/render.py +2 -2
- package/runtime/python/okstra_ctl/wizard/roles.py +1 -3
- package/runtime/python/okstra_ctl/wizard/sources.py +9 -40
- package/runtime/python/okstra_ctl/wizard/state.py +36 -145
- package/runtime/python/okstra_ctl/wizard/statefile.py +27 -128
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +22 -10
- package/runtime/python/okstra_ctl/wizard/steps_options.py +5 -4
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +15 -565
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +9 -2
- package/runtime/schemas/business-flow-v1.schema.json +847 -0
- package/runtime/schemas/convergence-groups-v2.0.schema.json +7 -0
- package/runtime/skills/okstra-explain-flow/SKILL.md +42 -0
- package/runtime/skills/okstra-inspect/facets/history.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/templates/reports/business-flow.template.md +106 -0
- package/runtime/templates/reports/html/base.template.html +14 -1
- package/runtime/templates/reports/html/business-flow.template.html +31 -0
- package/runtime/templates/reports/html/i18n/en.json +1 -0
- package/runtime/templates/reports/html/i18n/ko.json +1 -0
- package/runtime/templates/worker-prompt-preamble.md +11 -2
- package/runtime/validators/checks/validate-prompt-metadata-01.py +10 -10
- package/runtime/validators/validate-run.py +70 -21
- package/runtime/validators/validate_analysis_report.py +21 -21
- package/runtime/python/okstra_ctl/workers.py +0 -133
|
@@ -1,70 +1,22 @@
|
|
|
1
|
-
"""roles 단계 —
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
state
|
|
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
|
-
|
|
329
|
-
|
|
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"
|
|
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
|
-
|
|
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"
|