okstra 0.189.3 → 0.190.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 +1 -1
- package/dist/cli-registry.mjs +9 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/chat/chat.mjs +54 -4
- package/dist/commands/chat/chat.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +12 -5
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/lifecycle/uninstall.mjs +2 -1
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
- package/dist/lib/host-config.d.mts +5 -3
- package/dist/lib/host-config.mjs +89 -31
- package/dist/lib/host-config.mjs.map +1 -1
- package/docs/architecture/storage-model.md +4 -1
- package/docs/architecture.md +3 -3
- package/docs/cli.md +11 -9
- package/docs/for-ai/skills/okstra-brief-gen.md +1 -1
- package/docs/for-ai/skills/okstra-chat.md +3 -3
- package/docs/for-ai/skills/okstra-inspect.md +11 -1
- package/docs/for-ai/skills/okstra-rollup.md +1 -0
- package/docs/for-ai/skills/okstra-run.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +1 -1
- package/docs/project-structure-overview.md +17 -5
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/error-analysis.md +2 -2
- package/docs/task-process/final-verification.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +8 -6
- package/docs/task-process/requirements-discovery.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -0
- package/runtime/bin/okstra-report-translate.py +10 -4
- package/runtime/prompts/launch.template.md +11 -5
- package/runtime/prompts/lead/adapters/cmux.md +4 -7
- package/runtime/prompts/lead/context-loader.md +2 -0
- package/runtime/prompts/lead/convergence.md +20 -83
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -6
- package/runtime/prompts/lead/plan-body-verification.md +10 -3
- package/runtime/prompts/lead/report-writer.md +15 -5
- package/runtime/prompts/lead/team-contract.md +15 -9
- package/runtime/prompts/profiles/_common-contract.md +2 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -0
- package/runtime/prompts/profiles/_implementation-diff-review.md +4 -0
- package/runtime/prompts/profiles/_implementation-executor.md +9 -5
- package/runtime/prompts/profiles/_implementation-self-check.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -4
- package/runtime/prompts/profiles/_stage-discipline.md +12 -5
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/prompts/profiles/implementation.md +2 -0
- package/runtime/prompts/profiles/release-handoff.md +4 -3
- package/runtime/prompts/wizard/prompts.ko.json +2 -1
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +9 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +17 -3
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +3 -3
- package/runtime/python/okstra_ctl/agent/invocation.py +107 -5
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +16 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +3 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +87 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +72 -3
- package/runtime/python/okstra_ctl/analysis_packet.py +47 -6
- package/runtime/python/okstra_ctl/cmux.py +34 -13
- package/runtime/python/okstra_ctl/consumers.py +30 -7
- package/runtime/python/okstra_ctl/convergence.py +110 -21
- package/runtime/python/okstra_ctl/convergence_provenance.py +83 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +127 -5
- package/runtime/python/okstra_ctl/dispatch_state.py +17 -9
- package/runtime/python/okstra_ctl/domain/host.py +5 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +1 -0
- package/runtime/python/okstra_ctl/error_log_write.py +44 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +84 -7
- package/runtime/python/okstra_ctl/fixed_text.py +25 -7
- package/runtime/python/okstra_ctl/group_context.py +475 -10
- package/runtime/python/okstra_ctl/handoff.py +109 -32
- package/runtime/python/okstra_ctl/handoff_verification.py +80 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +52 -10
- package/runtime/python/okstra_ctl/lead_progress.py +291 -0
- package/runtime/python/okstra_ctl/model_io/renderers.py +60 -1
- package/runtime/python/okstra_ctl/model_io_cli.py +6 -1
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
- package/runtime/python/okstra_ctl/recap.py +179 -17
- package/runtime/python/okstra_ctl/reconcile.py +55 -4
- package/runtime/python/okstra_ctl/render.py +34 -8
- package/runtime/python/okstra_ctl/report_assembly.py +17 -10
- package/runtime/python/okstra_ctl/report_finalize.py +204 -11
- package/runtime/python/okstra_ctl/report_html/common.py +241 -113
- package/runtime/python/okstra_ctl/report_html/context_links.py +121 -0
- package/runtime/python/okstra_ctl/report_html/models.py +11 -5
- package/runtime/python/okstra_ctl/report_html/render.py +69 -17
- package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +8 -1
- package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +8 -1
- package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +11 -1
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +13 -1
- package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +9 -1
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +29 -2
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -9
- package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +8 -1
- package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +15 -1
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +7 -1
- package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +9 -1
- package/runtime/python/okstra_ctl/report_translation.py +58 -1
- package/runtime/python/okstra_ctl/run.py +54 -14
- package/runtime/python/okstra_ctl/stage_targets.py +66 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +39 -12
- package/runtime/python/okstra_ctl/wizard/__init__.py +148 -0
- package/runtime/python/okstra_ctl/wizard/__main__.py +13 -0
- package/runtime/python/okstra_ctl/wizard/cli.py +176 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +331 -0
- package/runtime/python/okstra_ctl/wizard/engine.py +363 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +411 -0
- package/runtime/python/okstra_ctl/wizard/prompts.py +202 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +836 -0
- package/runtime/python/okstra_ctl/wizard/render.py +189 -0
- package/runtime/python/okstra_ctl/wizard/roles.py +737 -0
- package/runtime/python/okstra_ctl/wizard/sources.py +703 -0
- package/runtime/python/okstra_ctl/wizard/state.py +689 -0
- package/runtime/python/okstra_ctl/wizard/statefile.py +145 -0
- package/runtime/python/okstra_ctl/wizard/steps_analysis.py +558 -0
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +828 -0
- package/runtime/python/okstra_ctl/wizard/steps_options.py +340 -0
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +959 -0
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +672 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +43 -3
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +14 -0
- package/runtime/python/okstra_ctl/workflow.py +2 -2
- package/runtime/python/okstra_ctl/worktree/__init__.py +2 -0
- package/runtime/python/okstra_ctl/worktree/cleanliness.py +7 -4
- package/runtime/python/okstra_ctl/worktree/git_ops.py +7 -0
- package/runtime/python/okstra_ctl/worktree/naming.py +15 -6
- package/runtime/python/okstra_ctl/worktree/provision.py +2 -1
- package/runtime/python/okstra_ctl/worktree_registry.py +27 -0
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -0
- package/runtime/schemas/final-report-v2.0.schema.json +4 -0
- package/runtime/schemas/final-report-v3.0.schema.json +4 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +22 -3
- package/runtime/skills/okstra-chat/SKILL.md +8 -6
- package/runtime/skills/okstra-inspect/SKILL.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +52 -4
- package/runtime/skills/okstra-rollup/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +17 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +2 -0
- package/runtime/skills/okstra-user-response/SKILL.md +1 -1
- package/runtime/templates/reports/brief.template.md +15 -7
- package/runtime/templates/reports/final-verification-input.template.md +2 -0
- package/runtime/templates/reports/group-context.template.md +7 -0
- package/runtime/templates/reports/html/base.template.html +13 -3
- package/runtime/templates/reports/html/i18n/en.json +43 -5
- package/runtime/templates/reports/html/i18n/ko.json +43 -5
- package/runtime/templates/reports/html/macros/forms.html +4 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +11 -1
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +16 -15
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +5 -5
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +2 -2
- package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +1 -1
- package/runtime/templates/reports/implementation-planning-input.template.md +3 -0
- package/runtime/templates/reports/improvement-discovery-input.template.md +3 -0
- package/runtime/templates/reports/release-handoff-input.template.md +5 -3
- package/runtime/templates/reports/schedule.template.md +13 -4
- package/runtime/templates/reports/task-brief.template.md +1 -1
- package/runtime/templates/reverify-output-contract.md +5 -0
- package/runtime/templates/worker-error-contract.md +2 -0
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-report-views.py +30 -1
- package/runtime/validators/validate-run.py +194 -25
- package/runtime/validators/validate_session_conformance.py +102 -2
- package/runtime/python/okstra_ctl/wizard.py +0 -7168
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
"""위저드 상태 — `WizardState`·`Prompt`·`Option`·`Step` 데이터 타입, v1 provider 선택의 v2 변환, 상태 술어."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import asdict, dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from okstra_ctl.registry.provider_registry import default_provider_registry
|
|
9
|
+
from okstra_ctl.model_pool import ModelPool
|
|
10
|
+
from okstra_ctl.role_requirements import (
|
|
11
|
+
RoleProfile,
|
|
12
|
+
RoleProfileError,
|
|
13
|
+
RoleRequirement,
|
|
14
|
+
load_role_profile,
|
|
15
|
+
)
|
|
16
|
+
from okstra_ctl.ids import slugify_task_segment
|
|
17
|
+
|
|
18
|
+
from .ids import (
|
|
19
|
+
PICK_TYPE_CUSTOM,
|
|
20
|
+
S_BASE_REF_PICK,
|
|
21
|
+
S_HANDOFF_STAGE_PICK,
|
|
22
|
+
_ABORT_OPTION,
|
|
23
|
+
_SLUG_OK,
|
|
24
|
+
_STAGE_SCOPED_TASK_TYPES,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---- Data types ----------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class WizardState:
|
|
32
|
+
# execution identity v2: role-first canonical model selections
|
|
33
|
+
execution_identity_version: int = 1
|
|
34
|
+
role_counts: dict[str, int] = field(default_factory=dict)
|
|
35
|
+
role_models: dict[str, list[str]] = field(default_factory=dict)
|
|
36
|
+
role_selection_order: list[str] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
# bootstrap
|
|
39
|
+
workspace_root: str = ""
|
|
40
|
+
project_root: str = ""
|
|
41
|
+
project_id: str = ""
|
|
42
|
+
host_runtime: str = "claude-code"
|
|
43
|
+
host_entry_mode: str = "current-session"
|
|
44
|
+
available_functions: list[str] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
# task identity
|
|
47
|
+
is_new_task: Optional[bool] = None
|
|
48
|
+
task_group: str = ""
|
|
49
|
+
task_id: str = ""
|
|
50
|
+
existing_brief_path: str = ""
|
|
51
|
+
# brief-derived suggestions (new-task flow only; set when brief is
|
|
52
|
+
# accepted, cleared if the user picks "type custom" so the next
|
|
53
|
+
# `_build_*` falls back to plain text input)
|
|
54
|
+
task_group_suggestion: str = ""
|
|
55
|
+
task_id_suggestion: str = ""
|
|
56
|
+
task_group_pending_text: bool = False
|
|
57
|
+
task_id_pending_text: bool = False
|
|
58
|
+
|
|
59
|
+
# task-type + dependents
|
|
60
|
+
task_type: str = ""
|
|
61
|
+
profile_workers: list[str] = field(default_factory=list)
|
|
62
|
+
profile_optional_workers: list[str] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
# brief
|
|
65
|
+
keep_existing_brief: Optional[bool] = None
|
|
66
|
+
brief_path: str = ""
|
|
67
|
+
brief_path_pending_text: bool = False
|
|
68
|
+
|
|
69
|
+
# analysis evidence and target
|
|
70
|
+
project_evidence_path: str = ""
|
|
71
|
+
project_evidence_pending_text: bool = False
|
|
72
|
+
feature_evidence_path: str = ""
|
|
73
|
+
feature_evidence_pending_text: bool = False
|
|
74
|
+
analysis_target: str = ""
|
|
75
|
+
analysis_target_pending_text: bool = False
|
|
76
|
+
|
|
77
|
+
# worktree
|
|
78
|
+
reuse_worktree: Optional[bool] = None
|
|
79
|
+
base_ref: str = ""
|
|
80
|
+
base_ref_pending_text: bool = False
|
|
81
|
+
|
|
82
|
+
# impl extras
|
|
83
|
+
approved_plan_path: str = ""
|
|
84
|
+
approved_plan_pending_text: bool = False
|
|
85
|
+
# A plan that is approvable (gate ok, no blockers) but not yet `approved`.
|
|
86
|
+
# Set when the user selects such a plan; the approve-confirm step reads it.
|
|
87
|
+
approve_plan_candidate: str = ""
|
|
88
|
+
# HTML Plan Approval 위젯이 남긴 sidecar 감지 결과. 선택된 plan 과
|
|
89
|
+
# source-report·seq 가 일치하는 APPROVAL 블록이 있을 때만 채워지고,
|
|
90
|
+
# approve-confirm 단계가 3-옵션(yes_apply/yes/no)으로 확장된다.
|
|
91
|
+
html_approval_sidecar: str = ""
|
|
92
|
+
html_approval_option: str = ""
|
|
93
|
+
design_prep_queue: list[str] = field(default_factory=list)
|
|
94
|
+
design_prep_current: str = ""
|
|
95
|
+
design_prep_decision: str = ""
|
|
96
|
+
design_prep_overrides_json: str = ""
|
|
97
|
+
design_prep_notes: str = ""
|
|
98
|
+
selected_stage: str = "auto"
|
|
99
|
+
selected_stages: str = "" # implementation 다중선택 위상정렬 CSV
|
|
100
|
+
executor: str = ""
|
|
101
|
+
critic: str = ""
|
|
102
|
+
critic_pending_text: bool = False
|
|
103
|
+
|
|
104
|
+
# release-handoff: PR 로 내보낼 범위. mode 는 stages 선택의 파생값이다 —
|
|
105
|
+
# whole-task = 빈 stages(whole-task 검증 기반 단일 PR), stage-group = csv.
|
|
106
|
+
handoff_mode: str = "" # "" | "whole-task" | "stage-group"
|
|
107
|
+
handoff_stages: str = "" # csv ("2,3"), whole-task 면 ""
|
|
108
|
+
|
|
109
|
+
# resume: 직전 run-inputs 재사용 여부 (None=미응답, True=재사용, False=재입력)
|
|
110
|
+
reuse_previous: Optional[bool] = None
|
|
111
|
+
|
|
112
|
+
# customize
|
|
113
|
+
use_defaults: Optional[bool] = None
|
|
114
|
+
workers_override: str = ""
|
|
115
|
+
workers_custom_pending: bool = False
|
|
116
|
+
lead_provider: str = ""
|
|
117
|
+
lead_model: str = ""
|
|
118
|
+
claude_model: str = ""
|
|
119
|
+
codex_model: str = ""
|
|
120
|
+
antigravity_model: str = ""
|
|
121
|
+
grok_model: str = ""
|
|
122
|
+
kimi_model: str = ""
|
|
123
|
+
report_writer_provider: str = ""
|
|
124
|
+
report_writer_model: str = ""
|
|
125
|
+
directive: str = ""
|
|
126
|
+
directive_pending_text: bool = False
|
|
127
|
+
last_directive_cached: str = ""
|
|
128
|
+
related_tasks_raw: str = ""
|
|
129
|
+
related_tasks_pending_text: bool = False
|
|
130
|
+
last_siblings_cached: str = ""
|
|
131
|
+
clarification_response_path: str = ""
|
|
132
|
+
clarification_pending_text: bool = False
|
|
133
|
+
last_final_report_cached: str = ""
|
|
134
|
+
selected_direction_path: str = ""
|
|
135
|
+
# "" | "auto" | "full" | "<stage csv>" — 사용자가 고른 이번 재실행의 재검증
|
|
136
|
+
# 범위. implementation-planning 재실행에서 좁힐 여지가 있을 때만 채워진다.
|
|
137
|
+
reverify_scope: str = ""
|
|
138
|
+
reverify_scope_pending_text: bool = False
|
|
139
|
+
pr_template_path: str = ""
|
|
140
|
+
pr_template_pending_text: bool = False
|
|
141
|
+
pr_template_scope: str = "" # "once" | "project" | "global"
|
|
142
|
+
last_pr_template_cached: str = ""
|
|
143
|
+
|
|
144
|
+
# confirm / edit
|
|
145
|
+
# "" | "yes" | "no" — done(release-handoff) task 재진입의 fix-cycle 기록 여부
|
|
146
|
+
fix_cycle: str = ""
|
|
147
|
+
confirmed: Optional[bool] = None
|
|
148
|
+
edit_target: str = ""
|
|
149
|
+
# terminal: user picked 중단 — no further prompt ever applies
|
|
150
|
+
aborted: bool = False
|
|
151
|
+
|
|
152
|
+
# bookkeeping
|
|
153
|
+
answered: list[str] = field(default_factory=list)
|
|
154
|
+
|
|
155
|
+
def to_json(self) -> dict[str, Any]:
|
|
156
|
+
payload = asdict(self)
|
|
157
|
+
payload.pop("execution_identity_version")
|
|
158
|
+
payload["executionIdentityVersion"] = 2
|
|
159
|
+
payload["roleCounts"] = payload.pop("role_counts")
|
|
160
|
+
payload["roleModels"] = payload.pop("role_models")
|
|
161
|
+
payload["roleSelectionOrder"] = payload.pop("role_selection_order")
|
|
162
|
+
for field_name in _V1_PROVIDER_STATE_FIELDS:
|
|
163
|
+
payload.pop(field_name, None)
|
|
164
|
+
return payload
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_json(cls, d: dict[str, Any]) -> "WizardState":
|
|
168
|
+
return _wizard_state_from_json(d)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class Option:
|
|
173
|
+
value: str
|
|
174
|
+
label: str
|
|
175
|
+
description: str = ""
|
|
176
|
+
# 이 run 이 계산한 추천. 표시는 이 한 곳에서만 나온다 — 종전에는 추천이
|
|
177
|
+
# 순서로만 암시됐고(첫 항목), 리드는 그와 별개로 산문에서 자기 추천을
|
|
178
|
+
# 적으라는 지시를 받았다. 둘이 갈리면 사용자는 서로 다른 두 추천을 동시에
|
|
179
|
+
# 본다(실측: 계획 run 의 clarification 단계 — 리드는 `직접 입력` 을 권했고
|
|
180
|
+
# 화면 첫 줄은 위저드의 캐시된 리포트였다).
|
|
181
|
+
recommended: bool = False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class Prompt:
|
|
186
|
+
step: str
|
|
187
|
+
kind: str # "pick" | "text" | "pick_group" | "done" | "aborted"
|
|
188
|
+
label: str = ""
|
|
189
|
+
options: list[Option] = field(default_factory=list)
|
|
190
|
+
help: str = ""
|
|
191
|
+
echo_template: str = "" # e.g. "task-group: {value}"
|
|
192
|
+
multi: bool = False # only meaningful when kind == "pick"
|
|
193
|
+
# only meaningful when kind == "pick_group": one entry per AskUserQuestion tab
|
|
194
|
+
questions: list["Prompt"] = field(default_factory=list)
|
|
195
|
+
|
|
196
|
+
def __post_init__(self) -> None:
|
|
197
|
+
"""자유 입력 선택지는 언제나 마지막이다.
|
|
198
|
+
|
|
199
|
+
목록이 자유 입력으로 열리면 질문을 통째로 사용자에게 되돌려준 것이다 —
|
|
200
|
+
앞선 선택지들이 이 run 이 이미 알아낸 것이고, 자유 입력은 그것들이
|
|
201
|
+
전부 맞지 않을 때의 탈출구다. 실측: 재검증 범위 질문의 `unlinked`
|
|
202
|
+
분기가 `직접 입력` 을 첫 선택지로 내놓았고, 리드가 preamble 에 해당
|
|
203
|
+
stage 번호를 이미 적어 놓은 화면에서도 사용자는 그 번호를 다시
|
|
204
|
+
타이핑해야 했다.
|
|
205
|
+
|
|
206
|
+
중단(abort)만 그 뒤에 올 수 있다. 중단은 탈출구가 아니라 취소이고,
|
|
207
|
+
`brief_carry` 가 실제로 그 모양이다 — 전환(추천) / 직접 입력 / 중단.
|
|
208
|
+
|
|
209
|
+
배치 지점이 12곳이라 관례로는 지켜지지 않는다. 값을 만드는 자리에서
|
|
210
|
+
막는다.
|
|
211
|
+
"""
|
|
212
|
+
values = [option.value for option in self.options]
|
|
213
|
+
if PICK_TYPE_CUSTOM not in values:
|
|
214
|
+
return
|
|
215
|
+
after = values[values.index(PICK_TYPE_CUSTOM) + 1:]
|
|
216
|
+
offenders = [value for value in after if value != _ABORT_OPTION]
|
|
217
|
+
if offenders:
|
|
218
|
+
raise WizardError(
|
|
219
|
+
f"wizard step {self.step!r}: the free-input option "
|
|
220
|
+
f"({PICK_TYPE_CUSTOM!r}) must come after every real choice — "
|
|
221
|
+
f"only {_ABORT_OPTION!r} may follow it, but {offenders} do"
|
|
222
|
+
)
|
|
223
|
+
self._check_recommendations()
|
|
224
|
+
|
|
225
|
+
def _check_recommendations(self) -> None:
|
|
226
|
+
"""추천은 목록 앞머리에 모여 있고, 탈출구는 추천이 아니다.
|
|
227
|
+
|
|
228
|
+
추천을 목록 중간에 흩어 놓으면 화면에서 어디까지가 이 run 의 판단인지
|
|
229
|
+
읽히지 않는다. 그리고 `직접 입력` / `중단` 은 앞의 선택지가 전부 맞지
|
|
230
|
+
않을 때의 탈출구이므로 추천 대상이 될 수 없다.
|
|
231
|
+
"""
|
|
232
|
+
flags = [option.recommended for option in self.options]
|
|
233
|
+
if any(flags) and any(flags[index] for index in range(len(flags))
|
|
234
|
+
if index > 0 and not flags[index - 1]):
|
|
235
|
+
raise WizardError(
|
|
236
|
+
f"wizard step {self.step!r}: recommended options must be the "
|
|
237
|
+
"leading run of the list"
|
|
238
|
+
)
|
|
239
|
+
escapes = {PICK_TYPE_CUSTOM, _ABORT_OPTION}
|
|
240
|
+
marked = [
|
|
241
|
+
option.value for option in self.options
|
|
242
|
+
if option.recommended and option.value in escapes
|
|
243
|
+
]
|
|
244
|
+
if marked:
|
|
245
|
+
raise WizardError(
|
|
246
|
+
f"wizard step {self.step!r}: {marked} is an escape hatch, "
|
|
247
|
+
"not a recommendation"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def id(self) -> str:
|
|
252
|
+
"""Stable public identifier shared by static and role-instance prompts."""
|
|
253
|
+
return self.step
|
|
254
|
+
|
|
255
|
+
def to_json(self) -> dict[str, Any]:
|
|
256
|
+
out = {
|
|
257
|
+
"id": self.step,
|
|
258
|
+
"step": self.step,
|
|
259
|
+
"kind": self.kind,
|
|
260
|
+
"label": self.label,
|
|
261
|
+
"options": [asdict(o) for o in self.options],
|
|
262
|
+
"help": self.help,
|
|
263
|
+
"echoTemplate": self.echo_template,
|
|
264
|
+
"multi": self.multi,
|
|
265
|
+
}
|
|
266
|
+
if self.kind == "pick_group":
|
|
267
|
+
out["questions"] = [
|
|
268
|
+
{"step": q.step, "label": q.label,
|
|
269
|
+
"options": [asdict(o) for o in q.options],
|
|
270
|
+
"multi": q.multi}
|
|
271
|
+
for q in self.questions
|
|
272
|
+
]
|
|
273
|
+
return out
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class WizardError(Exception):
|
|
277
|
+
"""validation failure surfaced to user."""
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _slug_or_die(value: str, field_name: str) -> str:
|
|
281
|
+
slug = slugify_task_segment(value or "")
|
|
282
|
+
if not slug or not _SLUG_OK.search(slug):
|
|
283
|
+
raise WizardError(
|
|
284
|
+
f"{field_name} must contain at least one alphanumeric character "
|
|
285
|
+
f"(got: {value!r})"
|
|
286
|
+
)
|
|
287
|
+
return slug
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ---- Roster / profile helpers -------------------------------------------
|
|
291
|
+
|
|
292
|
+
def _profile_path(workspace_root: Path, task_type: str) -> Path:
|
|
293
|
+
return workspace_root / "prompts" / "profiles" / f"{task_type}.md"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
_V2_STATE_FIELDS = (
|
|
297
|
+
"roleCounts",
|
|
298
|
+
"roleModels",
|
|
299
|
+
"roleSelectionOrder",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
_V1_PROVIDER_STATE_FIELDS = (
|
|
304
|
+
"workers_override",
|
|
305
|
+
"lead_provider",
|
|
306
|
+
"lead_model",
|
|
307
|
+
"claude_model",
|
|
308
|
+
"codex_model",
|
|
309
|
+
"antigravity_model",
|
|
310
|
+
"grok_model",
|
|
311
|
+
"kimi_model",
|
|
312
|
+
"report_writer_provider",
|
|
313
|
+
"report_writer_model",
|
|
314
|
+
"executor",
|
|
315
|
+
"critic",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
_V1_PROVIDER_STATE_JSON_FIELDS = frozenset({
|
|
320
|
+
*_V1_PROVIDER_STATE_FIELDS,
|
|
321
|
+
"workers",
|
|
322
|
+
"leadProvider",
|
|
323
|
+
"leadModel",
|
|
324
|
+
"claudeModel",
|
|
325
|
+
"codexModel",
|
|
326
|
+
"antigravityModel",
|
|
327
|
+
"grokModel",
|
|
328
|
+
"kimiModel",
|
|
329
|
+
"reportWriterProvider",
|
|
330
|
+
"reportWriterModel",
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _load_role_profile_for_state(state: WizardState) -> RoleProfile:
|
|
335
|
+
try:
|
|
336
|
+
return load_role_profile(
|
|
337
|
+
_profile_path(Path(state.workspace_root), state.task_type)
|
|
338
|
+
)
|
|
339
|
+
except RoleProfileError as exc:
|
|
340
|
+
raise WizardError(str(exc)) from exc
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _role_selection_enabled(state: WizardState) -> bool:
|
|
344
|
+
if (
|
|
345
|
+
state.execution_identity_version != 2
|
|
346
|
+
or not state.workspace_root
|
|
347
|
+
or not state.task_type
|
|
348
|
+
):
|
|
349
|
+
return False
|
|
350
|
+
try:
|
|
351
|
+
load_role_profile(_profile_path(Path(state.workspace_root), state.task_type))
|
|
352
|
+
except (OSError, RoleProfileError):
|
|
353
|
+
return False
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _legacy_value(payload: dict[str, Any], snake: str, camel: str) -> Any:
|
|
358
|
+
return payload.get(snake, payload.get(camel, ""))
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _legacy_model_ref(
|
|
362
|
+
pool: ModelPool,
|
|
363
|
+
provider: str,
|
|
364
|
+
role: str,
|
|
365
|
+
raw_model: Any,
|
|
366
|
+
) -> str | None:
|
|
367
|
+
provider_id = provider.strip().lower()
|
|
368
|
+
if not provider_id:
|
|
369
|
+
return None
|
|
370
|
+
model_value = raw_model.strip() if isinstance(raw_model, str) else ""
|
|
371
|
+
try:
|
|
372
|
+
model = (
|
|
373
|
+
pool.resolve_alias(provider_id, model_value)
|
|
374
|
+
if model_value
|
|
375
|
+
else pool.default_candidate(provider_id, role)
|
|
376
|
+
)
|
|
377
|
+
except (ValueError, RuntimeError):
|
|
378
|
+
return None
|
|
379
|
+
if model is None:
|
|
380
|
+
return None
|
|
381
|
+
availability = pool.availability(str(model.model_ref), role)
|
|
382
|
+
return str(model.model_ref) if availability.available else None
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _legacy_worker_providers(payload: dict[str, Any]) -> list[str]:
|
|
386
|
+
raw = _legacy_value(payload, "workers_override", "workers")
|
|
387
|
+
if isinstance(raw, list):
|
|
388
|
+
values = raw
|
|
389
|
+
elif isinstance(raw, str):
|
|
390
|
+
values = raw.split(",")
|
|
391
|
+
else:
|
|
392
|
+
values = []
|
|
393
|
+
return [
|
|
394
|
+
value.strip().lower()
|
|
395
|
+
for value in values
|
|
396
|
+
if isinstance(value, str)
|
|
397
|
+
and value.strip()
|
|
398
|
+
and value.strip().lower() != "report-writer"
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _initial_cross_verification_requirement(
|
|
403
|
+
profile: RoleProfile,
|
|
404
|
+
) -> RoleRequirement | None:
|
|
405
|
+
return next((
|
|
406
|
+
requirement
|
|
407
|
+
for requirement in profile.roles
|
|
408
|
+
if not requirement.dynamic
|
|
409
|
+
and requirement.role in {
|
|
410
|
+
"analyser",
|
|
411
|
+
"designer",
|
|
412
|
+
"planner",
|
|
413
|
+
"verifier",
|
|
414
|
+
}
|
|
415
|
+
), None)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _convert_v1_provider_selections(
|
|
419
|
+
state: WizardState,
|
|
420
|
+
payload: dict[str, Any],
|
|
421
|
+
) -> None:
|
|
422
|
+
if not _role_selection_enabled(state):
|
|
423
|
+
return
|
|
424
|
+
profile = _load_role_profile_for_state(state)
|
|
425
|
+
pool = ModelPool.from_registry(default_provider_registry())
|
|
426
|
+
cross_requirement = _initial_cross_verification_requirement(profile)
|
|
427
|
+
providers = _legacy_worker_providers(payload)
|
|
428
|
+
if cross_requirement is not None:
|
|
429
|
+
selected_count = cross_requirement.recommended_count
|
|
430
|
+
if providers and cross_requirement.min_count < cross_requirement.max_count:
|
|
431
|
+
selected_count = min(
|
|
432
|
+
cross_requirement.max_count,
|
|
433
|
+
max(cross_requirement.min_count, len(providers)),
|
|
434
|
+
)
|
|
435
|
+
if cross_requirement.min_count < cross_requirement.max_count:
|
|
436
|
+
state.role_counts[cross_requirement.role] = selected_count
|
|
437
|
+
count_id = f"role-count:{cross_requirement.role}"
|
|
438
|
+
state.role_selection_order.append(count_id)
|
|
439
|
+
if not providers:
|
|
440
|
+
providers = [
|
|
441
|
+
model.provider_id
|
|
442
|
+
for model in pool.default_candidates(cross_requirement.role)
|
|
443
|
+
]
|
|
444
|
+
for ordinal, provider in enumerate(providers[:selected_count], start=1):
|
|
445
|
+
raw_model = _legacy_value(
|
|
446
|
+
payload,
|
|
447
|
+
f"{provider}_model",
|
|
448
|
+
f"{provider}Model",
|
|
449
|
+
)
|
|
450
|
+
model_ref = _legacy_model_ref(
|
|
451
|
+
pool,
|
|
452
|
+
provider,
|
|
453
|
+
cross_requirement.role,
|
|
454
|
+
raw_model,
|
|
455
|
+
)
|
|
456
|
+
if model_ref is None:
|
|
457
|
+
break
|
|
458
|
+
state.role_models.setdefault(cross_requirement.role, []).append(model_ref)
|
|
459
|
+
state.role_selection_order.append(
|
|
460
|
+
f"role-model:{cross_requirement.role}:{ordinal}"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
provider_rows = (
|
|
464
|
+
(
|
|
465
|
+
"leader",
|
|
466
|
+
_legacy_value(payload, "lead_provider", "leadProvider") or "claude",
|
|
467
|
+
_legacy_value(payload, "lead_model", "leadModel"),
|
|
468
|
+
),
|
|
469
|
+
(
|
|
470
|
+
"report-writer",
|
|
471
|
+
_legacy_value(
|
|
472
|
+
payload,
|
|
473
|
+
"report_writer_provider",
|
|
474
|
+
"reportWriterProvider",
|
|
475
|
+
) or "claude",
|
|
476
|
+
_legacy_value(
|
|
477
|
+
payload,
|
|
478
|
+
"report_writer_model",
|
|
479
|
+
"reportWriterModel",
|
|
480
|
+
),
|
|
481
|
+
),
|
|
482
|
+
(
|
|
483
|
+
"implementer",
|
|
484
|
+
_legacy_value(payload, "executor", "executor") or "claude",
|
|
485
|
+
_legacy_value(
|
|
486
|
+
payload,
|
|
487
|
+
f"{_legacy_value(payload, 'executor', 'executor') or 'claude'}_model",
|
|
488
|
+
f"{_legacy_value(payload, 'executor', 'executor') or 'claude'}Model",
|
|
489
|
+
),
|
|
490
|
+
),
|
|
491
|
+
(
|
|
492
|
+
"critic",
|
|
493
|
+
_legacy_value(payload, "critic", "critic"),
|
|
494
|
+
_legacy_value(
|
|
495
|
+
payload,
|
|
496
|
+
f"{_legacy_value(payload, 'critic', 'critic')}_model",
|
|
497
|
+
f"{_legacy_value(payload, 'critic', 'critic')}Model",
|
|
498
|
+
),
|
|
499
|
+
),
|
|
500
|
+
)
|
|
501
|
+
static_roles = {
|
|
502
|
+
requirement.role
|
|
503
|
+
for requirement in profile.roles
|
|
504
|
+
if not requirement.dynamic and requirement.min_count > 0
|
|
505
|
+
}
|
|
506
|
+
for role, provider, raw_model in provider_rows:
|
|
507
|
+
if role not in static_roles or not isinstance(provider, str) or not provider:
|
|
508
|
+
continue
|
|
509
|
+
model_ref = _legacy_model_ref(pool, provider, role, raw_model)
|
|
510
|
+
if model_ref is None:
|
|
511
|
+
continue
|
|
512
|
+
state.role_models[role] = [model_ref]
|
|
513
|
+
state.role_selection_order.append(f"role-model:{role}:1")
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _wizard_state_from_json(payload: dict[str, Any]) -> WizardState:
|
|
517
|
+
if not isinstance(payload, dict):
|
|
518
|
+
raise WizardError("wizard state must be a JSON object")
|
|
519
|
+
data = dict(payload)
|
|
520
|
+
version = data.pop("executionIdentityVersion", None)
|
|
521
|
+
has_v2_fields = any(field_name in data for field_name in _V2_STATE_FIELDS)
|
|
522
|
+
has_v1_fields = any(
|
|
523
|
+
field_name in data for field_name in _V1_PROVIDER_STATE_JSON_FIELDS
|
|
524
|
+
)
|
|
525
|
+
if version == 2:
|
|
526
|
+
if has_v1_fields or not all(field_name in data for field_name in _V2_STATE_FIELDS):
|
|
527
|
+
raise WizardError("mixed wizard state versions are not allowed")
|
|
528
|
+
data["role_counts"] = data.pop("roleCounts")
|
|
529
|
+
data["role_models"] = data.pop("roleModels")
|
|
530
|
+
data["role_selection_order"] = data.pop("roleSelectionOrder")
|
|
531
|
+
data["execution_identity_version"] = 2
|
|
532
|
+
_validate_v2_state_fields(data)
|
|
533
|
+
elif version in (None, 1):
|
|
534
|
+
if has_v2_fields:
|
|
535
|
+
raise WizardError("mixed wizard state versions are not allowed")
|
|
536
|
+
data.pop("execution_identity_version", None)
|
|
537
|
+
else:
|
|
538
|
+
raise WizardError(f"unsupported wizard state version: {version!r}")
|
|
539
|
+
allowed = WizardState.__dataclass_fields__.keys()
|
|
540
|
+
state = WizardState(**{key: value for key, value in data.items() if key in allowed})
|
|
541
|
+
if version != 2:
|
|
542
|
+
state.execution_identity_version = 2
|
|
543
|
+
_convert_v1_provider_selections(state, payload)
|
|
544
|
+
_discard_implicit_leader_selection(state)
|
|
545
|
+
return state
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _validate_v2_state_fields(data: dict[str, Any]) -> None:
|
|
549
|
+
role_counts = data.get("role_counts")
|
|
550
|
+
role_models = data.get("role_models")
|
|
551
|
+
selection_order = data.get("role_selection_order")
|
|
552
|
+
if not isinstance(role_counts, dict) or not all(
|
|
553
|
+
isinstance(role, str)
|
|
554
|
+
and isinstance(count, int)
|
|
555
|
+
and not isinstance(count, bool)
|
|
556
|
+
for role, count in role_counts.items()
|
|
557
|
+
):
|
|
558
|
+
raise WizardError("wizard state roleCounts must map roles to integers")
|
|
559
|
+
if not isinstance(role_models, dict) or not all(
|
|
560
|
+
isinstance(role, str)
|
|
561
|
+
and isinstance(models, list)
|
|
562
|
+
and all(isinstance(model_ref, str) for model_ref in models)
|
|
563
|
+
for role, models in role_models.items()
|
|
564
|
+
):
|
|
565
|
+
raise WizardError("wizard state roleModels must map roles to modelRef arrays")
|
|
566
|
+
if not isinstance(selection_order, list) or not all(
|
|
567
|
+
isinstance(step_id, str) for step_id in selection_order
|
|
568
|
+
):
|
|
569
|
+
raise WizardError("wizard state roleSelectionOrder must be a string array")
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _is_role_selection_step(step_id: str) -> bool:
|
|
573
|
+
return step_id.startswith(("role-count:", "role-model:", "role-add:"))
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _discard_implicit_leader_selection(state: WizardState) -> None:
|
|
577
|
+
"""호스트에 고정된 리더의 구형 선택값을 상태에서 제거한다."""
|
|
578
|
+
state.role_models.pop("leader", None)
|
|
579
|
+
leader_steps = {
|
|
580
|
+
step_id
|
|
581
|
+
for step_id in state.role_selection_order
|
|
582
|
+
if step_id.startswith("role-model:leader:")
|
|
583
|
+
}
|
|
584
|
+
state.role_selection_order = [
|
|
585
|
+
step_id for step_id in state.role_selection_order
|
|
586
|
+
if step_id not in leader_steps
|
|
587
|
+
]
|
|
588
|
+
state.answered = [
|
|
589
|
+
step_id for step_id in state.answered
|
|
590
|
+
if step_id not in leader_steps
|
|
591
|
+
]
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _base_ref_required(state: WizardState) -> bool:
|
|
595
|
+
return state.task_type != "final-verification" and state.reuse_worktree is False
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _base_ref_ready(state: WizardState) -> bool:
|
|
599
|
+
return not _base_ref_required(state) or S_BASE_REF_PICK in state.answered
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
# ---- Step descriptors ---------------------------------------------------
|
|
603
|
+
|
|
604
|
+
@dataclass
|
|
605
|
+
class Step:
|
|
606
|
+
id: str
|
|
607
|
+
applies: Callable[[WizardState], bool]
|
|
608
|
+
build: Callable[[WizardState], Prompt]
|
|
609
|
+
submit: Callable[[WizardState, str], Optional[str]]
|
|
610
|
+
# Field names this step owns; resetting the step clears them.
|
|
611
|
+
owns: tuple[str, ...] = ()
|
|
612
|
+
repeatable: bool = False
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _identity_ready(s: WizardState) -> bool:
|
|
616
|
+
"""All identity questions (task pick → executor for impl) answered."""
|
|
617
|
+
if not s.task_type:
|
|
618
|
+
return False
|
|
619
|
+
# release-handoff 는 brief 가 없다 — prepare 가 검증 보고서 인용 input 을 생성한다.
|
|
620
|
+
if not s.brief_path and s.task_type != "release-handoff":
|
|
621
|
+
return False
|
|
622
|
+
if _base_ref_required(s) and S_BASE_REF_PICK not in s.answered:
|
|
623
|
+
return False
|
|
624
|
+
if s.base_ref_pending_text:
|
|
625
|
+
return False
|
|
626
|
+
if s.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
627
|
+
if not s.approved_plan_path:
|
|
628
|
+
return False
|
|
629
|
+
if s.task_type == "implementation" and not _role_selection_enabled(s):
|
|
630
|
+
if not s.executor:
|
|
631
|
+
return False
|
|
632
|
+
if (s.task_type == "release-handoff"
|
|
633
|
+
and S_HANDOFF_STAGE_PICK not in s.answered):
|
|
634
|
+
return False
|
|
635
|
+
return True
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
_FIELD_DEFAULTS: dict[str, Any] = {
|
|
639
|
+
"is_new_task": None, "task_group": "", "task_id": "",
|
|
640
|
+
"existing_brief_path": "", "task_type": "",
|
|
641
|
+
"task_group_suggestion": "", "task_id_suggestion": "",
|
|
642
|
+
"task_group_pending_text": False, "task_id_pending_text": False,
|
|
643
|
+
"profile_workers": [], "profile_optional_workers": [],
|
|
644
|
+
"keep_existing_brief": None,
|
|
645
|
+
"brief_path": "", "brief_path_pending_text": False,
|
|
646
|
+
"project_evidence_path": "", "project_evidence_pending_text": False,
|
|
647
|
+
"feature_evidence_path": "", "feature_evidence_pending_text": False,
|
|
648
|
+
"analysis_target": "", "analysis_target_pending_text": False,
|
|
649
|
+
"reuse_worktree": None, "base_ref": "",
|
|
650
|
+
"base_ref_pending_text": False, "approved_plan_path": "",
|
|
651
|
+
"approved_plan_pending_text": False, "approve_plan_candidate": "",
|
|
652
|
+
"html_approval_sidecar": "", "html_approval_option": "",
|
|
653
|
+
"design_prep_queue": [], "design_prep_current": "",
|
|
654
|
+
"design_prep_decision": "", "design_prep_overrides_json": "",
|
|
655
|
+
"design_prep_notes": "",
|
|
656
|
+
"selected_stage": "auto",
|
|
657
|
+
"selected_stages": "",
|
|
658
|
+
"handoff_mode": "", "handoff_stages": "",
|
|
659
|
+
"executor": "", "critic": "", "critic_pending_text": False,
|
|
660
|
+
"execution_identity_version": 1,
|
|
661
|
+
"role_counts": {}, "role_models": {}, "role_selection_order": [],
|
|
662
|
+
"reuse_previous": None,
|
|
663
|
+
"use_defaults": None, "workers_override": "",
|
|
664
|
+
"workers_custom_pending": False,
|
|
665
|
+
"lead_provider": "", "lead_model": "", "claude_model": "", "codex_model": "",
|
|
666
|
+
"antigravity_model": "", "grok_model": "", "kimi_model": "",
|
|
667
|
+
"report_writer_provider": "", "report_writer_model": "", "directive": "",
|
|
668
|
+
"directive_pending_text": False,
|
|
669
|
+
"related_tasks_raw": "", "related_tasks_pending_text": False,
|
|
670
|
+
"clarification_response_path": "", "clarification_pending_text": False,
|
|
671
|
+
"selected_direction_path": "",
|
|
672
|
+
"reverify_scope": "", "reverify_scope_pending_text": False,
|
|
673
|
+
"pr_template_path": "", "pr_template_pending_text": False,
|
|
674
|
+
"pr_template_scope": "",
|
|
675
|
+
"fix_cycle": "",
|
|
676
|
+
"confirmed": None, "edit_target": "",
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _reset_field(state: WizardState, fname: str) -> None:
|
|
681
|
+
if fname in _FIELD_DEFAULTS:
|
|
682
|
+
default = _FIELD_DEFAULTS[fname]
|
|
683
|
+
# copy mutable defaults
|
|
684
|
+
if isinstance(default, list):
|
|
685
|
+
setattr(state, fname, [])
|
|
686
|
+
elif isinstance(default, dict):
|
|
687
|
+
setattr(state, fname, {})
|
|
688
|
+
else:
|
|
689
|
+
setattr(state, fname, default)
|