okstra 0.144.0 → 0.146.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 +4 -1
- package/docs/architecture.md +22 -4
- package/docs/cli.md +53 -6
- package/docs/project-structure-overview.md +19 -9
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +5 -6
- package/runtime/bin/okstra-trace-cleanup.sh +28 -2
- package/runtime/prompts/lead/adapters/claude-code.md +3 -3
- package/runtime/prompts/lead/convergence.md +30 -5
- package/runtime/prompts/lead/okstra-lead-contract.md +9 -3
- package/runtime/prompts/lead/report-writer.md +20 -14
- package/runtime/prompts/lead/team-contract.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/change-impact-analysis.md +24 -0
- package/runtime/prompts/profiles/feature-analysis.md +24 -0
- package/runtime/prompts/profiles/forbidden-actions.json +18 -0
- package/runtime/prompts/profiles/project-analysis.md +24 -0
- package/runtime/prompts/wizard/prompts.ko.json +44 -1
- package/runtime/python/okstra_ctl/analysis_inputs.py +369 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +4 -10
- package/runtime/python/okstra_ctl/clarification_items.py +74 -1
- package/runtime/python/okstra_ctl/codex_dispatch.py +117 -58
- package/runtime/python/okstra_ctl/convergence_engine.py +3 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +19 -56
- package/runtime/python/okstra_ctl/dispatch_state.py +167 -3
- package/runtime/python/okstra_ctl/path_hints.py +6 -0
- package/runtime/python/okstra_ctl/paths.py +7 -44
- package/runtime/python/okstra_ctl/render.py +79 -4
- package/runtime/python/okstra_ctl/render_final_report.py +13 -4
- package/runtime/python/okstra_ctl/report_views.py +134 -3
- package/runtime/python/okstra_ctl/run.py +118 -0
- package/runtime/python/okstra_ctl/run_context.py +34 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +12 -4
- package/runtime/python/okstra_ctl/user_response.py +309 -3
- package/runtime/python/okstra_ctl/wizard.py +579 -32
- package/runtime/python/okstra_ctl/worker_liveness.py +84 -21
- package/runtime/python/okstra_ctl/worker_prompt_body.py +24 -4
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +57 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -0
- package/runtime/python/okstra_ctl/worker_state.py +65 -0
- package/runtime/python/okstra_ctl/workflow.py +22 -0
- package/runtime/python/okstra_token_usage/antigravity.py +3 -0
- package/runtime/python/okstra_token_usage/codex.py +54 -23
- package/runtime/python/okstra_token_usage/collect.py +141 -33
- package/runtime/python/okstra_token_usage/paths.py +27 -0
- package/runtime/python/okstra_vendor/__init__.py +15 -2
- package/runtime/schemas/convergence-groups-v1.0.schema.json +0 -1
- package/runtime/schemas/final-report-v1.0.schema.json +849 -3
- package/runtime/skills/okstra-run/SKILL.md +27 -5
- package/runtime/skills/okstra-setup/references/project-config.md +13 -4
- package/runtime/templates/reports/change-impact-analysis-input.template.md +58 -0
- package/runtime/templates/reports/feature-analysis-input.template.md +59 -0
- package/runtime/templates/reports/final-report.template.md +220 -0
- package/runtime/templates/reports/i18n/en.json +8 -0
- package/runtime/templates/reports/i18n/ko.json +8 -0
- package/runtime/templates/reports/project-analysis-input.template.md +58 -0
- package/runtime/templates/reports/report.js +84 -5
- package/runtime/templates/reports/user-response.template.md +19 -1
- package/runtime/validators/lib/fixtures.sh +1 -1
- package/runtime/validators/validate-report-views.py +61 -7
- package/runtime/validators/validate-run.py +94 -1
- package/runtime/validators/validate_analysis_report.py +895 -0
- package/src/cli-registry.mjs +7 -10
- package/src/commands/execute/render-bundle.mjs +3 -0
- package/src/commands/execute/worker-state.mjs +29 -0
- package/src/commands/inspect/worker-liveness.mjs +5 -3
- package/src/commands/lifecycle/preflight.mjs +13 -3
- package/src/lib/runtime-readiness.mjs +90 -0
- package/runtime/python/okstra_ctl/phase_cleanup.py +0 -235
- package/src/commands/execute/phase-cleanup.mjs +0 -38
|
@@ -31,13 +31,26 @@ from typing import Any, Callable, Optional
|
|
|
31
31
|
|
|
32
32
|
from okstra_ctl.ids import slugify_task_segment
|
|
33
33
|
from okstra_ctl.brief_frontmatter import read_brief_frontmatter
|
|
34
|
+
from okstra_ctl.analysis_inputs import (
|
|
35
|
+
ANALYSIS_TASK_TYPES,
|
|
36
|
+
AnalysisInputError,
|
|
37
|
+
AnalysisReportCandidate,
|
|
38
|
+
_resolved_within,
|
|
39
|
+
list_evidence_candidates,
|
|
40
|
+
load_analysis_report_candidate,
|
|
41
|
+
resolve_analysis_target,
|
|
42
|
+
resolve_evidence_inputs,
|
|
43
|
+
)
|
|
34
44
|
from okstra_ctl.models import (
|
|
35
45
|
PROVIDER_MAPPINGS,
|
|
36
46
|
UnknownModelError,
|
|
37
47
|
picker_options,
|
|
38
48
|
resolve_model_metadata,
|
|
39
49
|
)
|
|
40
|
-
from okstra_ctl.clarification_items import
|
|
50
|
+
from okstra_ctl.clarification_items import (
|
|
51
|
+
scan_approval_gate,
|
|
52
|
+
user_response_sidecars,
|
|
53
|
+
)
|
|
41
54
|
from okstra_ctl.design_prep import (
|
|
42
55
|
DesignPrepError,
|
|
43
56
|
load_design_prep_items,
|
|
@@ -100,9 +113,16 @@ from okstra_project.state import (
|
|
|
100
113
|
|
|
101
114
|
# ---- Constants -----------------------------------------------------------
|
|
102
115
|
|
|
116
|
+
_ANALYSIS_TASK_TYPE_DESCRIPTIONS = (
|
|
117
|
+
"Map project structure and bounded codebase scope (read-only)",
|
|
118
|
+
"Analyse feature behavior and test coverage scope (read-only)",
|
|
119
|
+
"Assess change impact and preserved behavior (read-only)",
|
|
120
|
+
)
|
|
121
|
+
|
|
103
122
|
TASK_TYPES: list[tuple[str, str]] = [
|
|
104
123
|
("requirements-discovery", "Classify request and route to next safe phase"),
|
|
105
124
|
("improvement-discovery", "Find improvement candidates within a codebase scope and lens whitelist"),
|
|
125
|
+
*zip(ANALYSIS_TASK_TYPES, _ANALYSIS_TASK_TYPE_DESCRIPTIONS),
|
|
106
126
|
("error-analysis", "Evidence-based root-cause analysis (no code changes)"),
|
|
107
127
|
("implementation-planning", "Plan options + request user approval"),
|
|
108
128
|
("implementation", "Execute approved plan (requires approved final-report)"),
|
|
@@ -131,11 +151,16 @@ _STAGE_SCOPED_TASK_TYPES = ("implementation", "final-verification")
|
|
|
131
151
|
# downstream phase 는 task manifest 의 taskBriefPath 를 자동 carry-in 하며
|
|
132
152
|
# 위저드에서 brief 를 묻지 않는다 (release-handoff 는 prepare 가 검증 보고서
|
|
133
153
|
# 인용 input 문서를 자동 생성하므로 brief 자체가 없다).
|
|
134
|
-
_BRIEF_ENTRY_TASK_TYPES = (
|
|
135
|
-
|
|
154
|
+
_BRIEF_ENTRY_TASK_TYPES = (
|
|
155
|
+
"requirements-discovery",
|
|
156
|
+
"improvement-discovery",
|
|
157
|
+
*ANALYSIS_TASK_TYPES,
|
|
158
|
+
"error-analysis",
|
|
159
|
+
)
|
|
136
160
|
|
|
137
161
|
CANONICAL_BASE_REFS = ["main", "dev", "staging", "preprod", "prod"]
|
|
138
162
|
BASE_REF_FREE_INPUT_TOKEN = "__free_input__"
|
|
163
|
+
HOST_PICK_OPTION_CAP = 4
|
|
139
164
|
|
|
140
165
|
CLAUDE_MODEL_OPTIONS = ["default", *picker_options("claude")]
|
|
141
166
|
CODEX_MODEL_OPTIONS = ["default", *picker_options("codex")]
|
|
@@ -263,6 +288,12 @@ S_BRIEF_KEEP = "brief_keep"
|
|
|
263
288
|
S_BRIEF_PATH_PICK = "brief_path_pick"
|
|
264
289
|
S_BRIEF_PATH = "brief_path"
|
|
265
290
|
S_BRIEF_CARRY = "brief_carry"
|
|
291
|
+
S_FEATURE_EVIDENCE_PICK = "feature_evidence_pick"
|
|
292
|
+
S_FEATURE_EVIDENCE = "feature_evidence"
|
|
293
|
+
S_PROJECT_EVIDENCE_PICK = "project_evidence_pick"
|
|
294
|
+
S_PROJECT_EVIDENCE = "project_evidence"
|
|
295
|
+
S_ANALYSIS_TARGET_PICK = "analysis_target_pick"
|
|
296
|
+
S_ANALYSIS_TARGET = "analysis_target"
|
|
266
297
|
S_BASE_REF_PICK = "base_ref_pick"
|
|
267
298
|
S_BASE_REF_TEXT = "base_ref_text"
|
|
268
299
|
S_APPROVED_PLAN_PICK = "approved_plan_pick"
|
|
@@ -355,6 +386,14 @@ class WizardState:
|
|
|
355
386
|
brief_path: str = ""
|
|
356
387
|
brief_path_pending_text: bool = False
|
|
357
388
|
|
|
389
|
+
# analysis evidence and target
|
|
390
|
+
project_evidence_path: str = ""
|
|
391
|
+
project_evidence_pending_text: bool = False
|
|
392
|
+
feature_evidence_path: str = ""
|
|
393
|
+
feature_evidence_pending_text: bool = False
|
|
394
|
+
analysis_target: str = ""
|
|
395
|
+
analysis_target_pending_text: bool = False
|
|
396
|
+
|
|
358
397
|
# worktree
|
|
359
398
|
reuse_worktree: Optional[bool] = None
|
|
360
399
|
base_ref: str = ""
|
|
@@ -460,6 +499,8 @@ class Prompt:
|
|
|
460
499
|
"echoTemplate": self.echo_template,
|
|
461
500
|
"multi": self.multi,
|
|
462
501
|
}
|
|
502
|
+
if self.kind == "pick" and len(self.options) > HOST_PICK_OPTION_CAP:
|
|
503
|
+
out["presentation"] = "numbered-text"
|
|
463
504
|
if self.kind == "pick_group":
|
|
464
505
|
out["questions"] = [
|
|
465
506
|
{"step": q.step, "label": q.label,
|
|
@@ -474,6 +515,35 @@ class WizardError(Exception):
|
|
|
474
515
|
"""validation failure surfaced to user."""
|
|
475
516
|
|
|
476
517
|
|
|
518
|
+
def _normalize_numbered_pick_item(prompt: Prompt, answer: str) -> str:
|
|
519
|
+
candidate = (answer or "").strip()
|
|
520
|
+
exact_values = [option.value for option in prompt.options if option.value == candidate]
|
|
521
|
+
if exact_values:
|
|
522
|
+
return exact_values[0]
|
|
523
|
+
|
|
524
|
+
if candidate.isdecimal():
|
|
525
|
+
number = int(candidate)
|
|
526
|
+
if 1 <= number <= len(prompt.options):
|
|
527
|
+
return prompt.options[number - 1].value
|
|
528
|
+
raise WizardError(f"numbered-text answer is out of range: {candidate}")
|
|
529
|
+
|
|
530
|
+
label_values = [option.value for option in prompt.options if option.label == candidate]
|
|
531
|
+
if len(label_values) == 1:
|
|
532
|
+
return label_values[0]
|
|
533
|
+
if len(label_values) > 1:
|
|
534
|
+
raise WizardError(f"numbered-text answer label is ambiguous: {candidate}")
|
|
535
|
+
raise WizardError(f"numbered-text answer is not an option: {candidate}")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _normalize_numbered_pick_answer(prompt: Prompt, answer: str) -> str:
|
|
539
|
+
if prompt.multi:
|
|
540
|
+
return ",".join(
|
|
541
|
+
_normalize_numbered_pick_item(prompt, item)
|
|
542
|
+
for item in (answer or "").split(",")
|
|
543
|
+
)
|
|
544
|
+
return _normalize_numbered_pick_item(prompt, answer)
|
|
545
|
+
|
|
546
|
+
|
|
477
547
|
# ---- Validation helpers --------------------------------------------------
|
|
478
548
|
|
|
479
549
|
_SLUG_OK = re.compile(r"[a-z0-9]")
|
|
@@ -1445,6 +1515,101 @@ def _recent_task_types(state: WizardState) -> list[str]:
|
|
|
1445
1515
|
return out
|
|
1446
1516
|
|
|
1447
1517
|
|
|
1518
|
+
def _contained_final_report_data_path(report: Path, task_root: Path) -> Path:
|
|
1519
|
+
_resolved_within(report, task_root, "final report path")
|
|
1520
|
+
return _resolved_within(
|
|
1521
|
+
final_report_data_path(report),
|
|
1522
|
+
task_root,
|
|
1523
|
+
"final report data path",
|
|
1524
|
+
)
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def _newest_contained_final_report(
|
|
1528
|
+
runs_base: Path,
|
|
1529
|
+
task_root: Path,
|
|
1530
|
+
glob_pattern: str,
|
|
1531
|
+
project_root: Path,
|
|
1532
|
+
) -> Path | None:
|
|
1533
|
+
candidates: list[Path] = []
|
|
1534
|
+
for report in runs_base.glob(glob_pattern):
|
|
1535
|
+
if not report.is_file():
|
|
1536
|
+
continue
|
|
1537
|
+
try:
|
|
1538
|
+
_contained_final_report_data_path(report, task_root)
|
|
1539
|
+
if report.parent.parent.name in ANALYSIS_TASK_TYPES:
|
|
1540
|
+
load_analysis_report_candidate(project_root, report)
|
|
1541
|
+
except AnalysisInputError:
|
|
1542
|
+
continue
|
|
1543
|
+
candidates.append(report)
|
|
1544
|
+
|
|
1545
|
+
def mtime_safe(report: Path) -> float:
|
|
1546
|
+
try:
|
|
1547
|
+
return report.stat().st_mtime
|
|
1548
|
+
except OSError:
|
|
1549
|
+
return -1.0
|
|
1550
|
+
|
|
1551
|
+
return max(candidates, key=mtime_safe) if candidates else None
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
def _analysis_revision_candidate(
|
|
1555
|
+
state: WizardState,
|
|
1556
|
+
report: Path,
|
|
1557
|
+
task_root: Path,
|
|
1558
|
+
expected_task_key: str,
|
|
1559
|
+
) -> tuple[int, Path] | None:
|
|
1560
|
+
try:
|
|
1561
|
+
_contained_final_report_data_path(report, task_root)
|
|
1562
|
+
candidate = load_analysis_report_candidate(
|
|
1563
|
+
Path(state.project_root), report
|
|
1564
|
+
)
|
|
1565
|
+
except AnalysisInputError:
|
|
1566
|
+
return None
|
|
1567
|
+
if (
|
|
1568
|
+
candidate.task_key != expected_task_key
|
|
1569
|
+
or candidate.task_type != state.task_type
|
|
1570
|
+
or candidate.review_status != "revision-requested"
|
|
1571
|
+
):
|
|
1572
|
+
return None
|
|
1573
|
+
return int(candidate.run_seq), report
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def _latest_revision_requested_analysis_report(
|
|
1577
|
+
state: WizardState,
|
|
1578
|
+
) -> Path | None:
|
|
1579
|
+
if state.task_type not in ANALYSIS_TASK_TYPES:
|
|
1580
|
+
return None
|
|
1581
|
+
reports = task_runs_dir(
|
|
1582
|
+
Path(state.project_root), state.task_group, state.task_id
|
|
1583
|
+
) / state.task_type / "reports"
|
|
1584
|
+
try:
|
|
1585
|
+
task_root = _resolved_within(
|
|
1586
|
+
reports.parents[2],
|
|
1587
|
+
Path(state.project_root).resolve(),
|
|
1588
|
+
"analysis task root",
|
|
1589
|
+
)
|
|
1590
|
+
except AnalysisInputError:
|
|
1591
|
+
return None
|
|
1592
|
+
expected_task_key = f"{state.project_id}:{state.task_group}:{state.task_id}"
|
|
1593
|
+
candidates: list[tuple[int, Path]] = []
|
|
1594
|
+
for report in reports.glob("final-report-*.md"):
|
|
1595
|
+
candidate = _analysis_revision_candidate(
|
|
1596
|
+
state,
|
|
1597
|
+
report,
|
|
1598
|
+
task_root,
|
|
1599
|
+
expected_task_key,
|
|
1600
|
+
)
|
|
1601
|
+
if candidate is not None:
|
|
1602
|
+
candidates.append(candidate)
|
|
1603
|
+
if not candidates:
|
|
1604
|
+
return None
|
|
1605
|
+
return max(candidates, key=lambda candidate: candidate[0])[1]
|
|
1606
|
+
|
|
1607
|
+
|
|
1608
|
+
def _latest_revision_requested_analysis_type(state: WizardState) -> str:
|
|
1609
|
+
report = _latest_revision_requested_analysis_report(state)
|
|
1610
|
+
return state.task_type if report is not None else ""
|
|
1611
|
+
|
|
1612
|
+
|
|
1448
1613
|
def _build_task_type(state: WizardState) -> Prompt:
|
|
1449
1614
|
t = _p(state.workspace_root, "task_type")
|
|
1450
1615
|
recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
|
|
@@ -1464,7 +1629,9 @@ def _build_task_type(state: WizardState) -> Prompt:
|
|
|
1464
1629
|
description_by_type[task_type]))
|
|
1465
1630
|
|
|
1466
1631
|
workflow = _existing_task_workflow(state)
|
|
1467
|
-
|
|
1632
|
+
revision_requested = _latest_revision_requested_analysis_type(state)
|
|
1633
|
+
recommended = (revision_requested or state.task_type
|
|
1634
|
+
or workflow.get("nextRecommendedPhase") or "")
|
|
1468
1635
|
if not recommended and not workflow:
|
|
1469
1636
|
recommended = TASK_TYPE_VALUES[0]
|
|
1470
1637
|
add(recommended, recommended_suffix)
|
|
@@ -1677,6 +1844,225 @@ def _submit_brief_carry(state: WizardState, value: str) -> Optional[str]:
|
|
|
1677
1844
|
)
|
|
1678
1845
|
|
|
1679
1846
|
|
|
1847
|
+
def _analysis_current_commit(state: WizardState) -> str:
|
|
1848
|
+
try:
|
|
1849
|
+
return subprocess.check_output(
|
|
1850
|
+
["git", "-C", state.project_root, "rev-parse", "HEAD"],
|
|
1851
|
+
text=True,
|
|
1852
|
+
stderr=subprocess.DEVNULL,
|
|
1853
|
+
).strip()
|
|
1854
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
1855
|
+
raise WizardError(
|
|
1856
|
+
f"cannot resolve current project commit for analysis evidence: {state.project_root}"
|
|
1857
|
+
) from exc
|
|
1858
|
+
|
|
1859
|
+
|
|
1860
|
+
def _analysis_evidence_paths(state: WizardState) -> list[Path]:
|
|
1861
|
+
return [
|
|
1862
|
+
Path(path)
|
|
1863
|
+
for path in (state.feature_evidence_path, state.project_evidence_path)
|
|
1864
|
+
if path
|
|
1865
|
+
]
|
|
1866
|
+
|
|
1867
|
+
|
|
1868
|
+
def _resolve_analysis_evidence(state: WizardState):
|
|
1869
|
+
try:
|
|
1870
|
+
return resolve_evidence_inputs(
|
|
1871
|
+
Path(state.project_root),
|
|
1872
|
+
state.task_type,
|
|
1873
|
+
_analysis_evidence_paths(state),
|
|
1874
|
+
_analysis_current_commit(state),
|
|
1875
|
+
)
|
|
1876
|
+
except AnalysisInputError as exc:
|
|
1877
|
+
raise WizardError(str(exc)) from exc
|
|
1878
|
+
|
|
1879
|
+
|
|
1880
|
+
def _accept_analysis_evidence(
|
|
1881
|
+
state: WizardState, raw_path: str, field_name: str
|
|
1882
|
+
) -> AnalysisReportCandidate:
|
|
1883
|
+
report_path = _resolve_path(raw_path, Path(state.project_root))
|
|
1884
|
+
try:
|
|
1885
|
+
candidate = load_analysis_report_candidate(
|
|
1886
|
+
Path(state.project_root), report_path
|
|
1887
|
+
)
|
|
1888
|
+
except AnalysisInputError as exc:
|
|
1889
|
+
raise WizardError(str(exc)) from exc
|
|
1890
|
+
previous = getattr(state, field_name)
|
|
1891
|
+
setattr(state, field_name, str(candidate.report_path))
|
|
1892
|
+
try:
|
|
1893
|
+
_resolve_analysis_evidence(state)
|
|
1894
|
+
except WizardError:
|
|
1895
|
+
setattr(state, field_name, previous)
|
|
1896
|
+
raise
|
|
1897
|
+
return candidate
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
def _evidence_option_label(candidate: AnalysisReportCandidate) -> str:
|
|
1901
|
+
return f"{candidate.task_key} · {candidate.task_type} · run {candidate.run_seq}"
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
def _build_analysis_evidence_pick(
|
|
1905
|
+
state: WizardState, *, step: str, relation: str
|
|
1906
|
+
) -> Prompt:
|
|
1907
|
+
t = _p(state.workspace_root, step)
|
|
1908
|
+
candidates = list_evidence_candidates(
|
|
1909
|
+
Path(state.project_root), state.task_type, relation
|
|
1910
|
+
)[:2]
|
|
1911
|
+
options = [
|
|
1912
|
+
_opt(str(candidate.report_path), _evidence_option_label(candidate))
|
|
1913
|
+
for candidate in candidates
|
|
1914
|
+
]
|
|
1915
|
+
options.extend([
|
|
1916
|
+
_opt(PICK_SKIP, t["options"][PICK_SKIP]),
|
|
1917
|
+
_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]),
|
|
1918
|
+
])
|
|
1919
|
+
return Prompt(step=step, kind="pick", label=t["label"], options=options,
|
|
1920
|
+
echo_template=t["echo_template"])
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
def _submit_analysis_evidence_pick(
|
|
1924
|
+
state: WizardState,
|
|
1925
|
+
value: str,
|
|
1926
|
+
*,
|
|
1927
|
+
step: str,
|
|
1928
|
+
field_name: str,
|
|
1929
|
+
pending_name: str,
|
|
1930
|
+
target_after_skip: bool = False,
|
|
1931
|
+
) -> Optional[str]:
|
|
1932
|
+
t = _p(state.workspace_root, step)
|
|
1933
|
+
if value == PICK_SKIP:
|
|
1934
|
+
setattr(state, field_name, "")
|
|
1935
|
+
setattr(state, pending_name, False)
|
|
1936
|
+
if target_after_skip:
|
|
1937
|
+
state.analysis_target_pending_text = True
|
|
1938
|
+
return t["echo_variants"]["skip"]
|
|
1939
|
+
if value == PICK_TYPE_CUSTOM:
|
|
1940
|
+
setattr(state, pending_name, True)
|
|
1941
|
+
return None
|
|
1942
|
+
candidate = _accept_analysis_evidence(state, value, field_name)
|
|
1943
|
+
setattr(state, pending_name, False)
|
|
1944
|
+
return t["echo_variants"]["selected"].format(path=candidate.report_path)
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
def _build_feature_evidence_pick(state: WizardState) -> Prompt:
|
|
1948
|
+
return _build_analysis_evidence_pick(
|
|
1949
|
+
state, step=S_FEATURE_EVIDENCE_PICK, relation="feature-baseline"
|
|
1950
|
+
)
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
def _submit_feature_evidence_pick(state: WizardState, value: str) -> Optional[str]:
|
|
1954
|
+
return _submit_analysis_evidence_pick(
|
|
1955
|
+
state, value, step=S_FEATURE_EVIDENCE_PICK,
|
|
1956
|
+
field_name="feature_evidence_path",
|
|
1957
|
+
pending_name="feature_evidence_pending_text",
|
|
1958
|
+
)
|
|
1959
|
+
|
|
1960
|
+
|
|
1961
|
+
def _build_feature_evidence(state: WizardState) -> Prompt:
|
|
1962
|
+
t = _p(state.workspace_root, S_FEATURE_EVIDENCE)
|
|
1963
|
+
return Prompt(step=S_FEATURE_EVIDENCE, kind="text", label=t["label"],
|
|
1964
|
+
echo_template=t["echo_template"])
|
|
1965
|
+
|
|
1966
|
+
|
|
1967
|
+
def _submit_feature_evidence(state: WizardState, value: str) -> Optional[str]:
|
|
1968
|
+
candidate = _accept_analysis_evidence(state, value, "feature_evidence_path")
|
|
1969
|
+
state.feature_evidence_pending_text = False
|
|
1970
|
+
return f"feature-evidence: {candidate.report_path}"
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
def _build_project_evidence_pick(state: WizardState) -> Prompt:
|
|
1974
|
+
return _build_analysis_evidence_pick(
|
|
1975
|
+
state, step=S_PROJECT_EVIDENCE_PICK, relation="project-context"
|
|
1976
|
+
)
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
def _submit_project_evidence_pick(state: WizardState, value: str) -> Optional[str]:
|
|
1980
|
+
return _submit_analysis_evidence_pick(
|
|
1981
|
+
state, value, step=S_PROJECT_EVIDENCE_PICK,
|
|
1982
|
+
field_name="project_evidence_path",
|
|
1983
|
+
pending_name="project_evidence_pending_text",
|
|
1984
|
+
target_after_skip=state.task_type == "feature-analysis",
|
|
1985
|
+
)
|
|
1986
|
+
|
|
1987
|
+
|
|
1988
|
+
def _build_project_evidence(state: WizardState) -> Prompt:
|
|
1989
|
+
t = _p(state.workspace_root, S_PROJECT_EVIDENCE)
|
|
1990
|
+
return Prompt(step=S_PROJECT_EVIDENCE, kind="text", label=t["label"],
|
|
1991
|
+
echo_template=t["echo_template"])
|
|
1992
|
+
|
|
1993
|
+
|
|
1994
|
+
def _submit_project_evidence(state: WizardState, value: str) -> Optional[str]:
|
|
1995
|
+
candidate = _accept_analysis_evidence(state, value, "project_evidence_path")
|
|
1996
|
+
state.project_evidence_pending_text = False
|
|
1997
|
+
return f"project-evidence: {candidate.report_path}"
|
|
1998
|
+
|
|
1999
|
+
|
|
2000
|
+
def _feature_description(feature: dict[object, object]) -> str:
|
|
2001
|
+
name = str(feature.get("name") or "")
|
|
2002
|
+
summary = str(feature.get("summary") or "")
|
|
2003
|
+
entry_point = str(
|
|
2004
|
+
feature.get("entryPoint") or feature.get("entrypoint") or ""
|
|
2005
|
+
)
|
|
2006
|
+
return " · ".join(value for value in (name, summary, entry_point) if value)
|
|
2007
|
+
|
|
2008
|
+
|
|
2009
|
+
def _build_analysis_target_pick(state: WizardState) -> Prompt:
|
|
2010
|
+
t = _p(state.workspace_root, S_ANALYSIS_TARGET_PICK)
|
|
2011
|
+
try:
|
|
2012
|
+
candidate = load_analysis_report_candidate(
|
|
2013
|
+
Path(state.project_root), Path(state.project_evidence_path)
|
|
2014
|
+
)
|
|
2015
|
+
except AnalysisInputError as exc:
|
|
2016
|
+
raise WizardError(str(exc)) from exc
|
|
2017
|
+
options = [
|
|
2018
|
+
_opt(str(feature["id"]), str(feature["id"]), _feature_description(feature))
|
|
2019
|
+
for feature in candidate.feature_index
|
|
2020
|
+
if isinstance(feature, dict) and isinstance(feature.get("id"), str)
|
|
2021
|
+
][:3]
|
|
2022
|
+
options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
|
|
2023
|
+
return Prompt(step=S_ANALYSIS_TARGET_PICK, kind="pick", label=t["label"],
|
|
2024
|
+
options=options, echo_template=t["echo_template"])
|
|
2025
|
+
|
|
2026
|
+
|
|
2027
|
+
def _accept_analysis_target(state: WizardState, value: str) -> str:
|
|
2028
|
+
candidates: dict[Path, AnalysisReportCandidate] = {}
|
|
2029
|
+
for path in _analysis_evidence_paths(state):
|
|
2030
|
+
try:
|
|
2031
|
+
candidate = load_analysis_report_candidate(Path(state.project_root), path)
|
|
2032
|
+
except AnalysisInputError as exc:
|
|
2033
|
+
raise WizardError(str(exc)) from exc
|
|
2034
|
+
candidates[candidate.report_path] = candidate
|
|
2035
|
+
try:
|
|
2036
|
+
target = resolve_analysis_target(
|
|
2037
|
+
value, _resolve_analysis_evidence(state), candidates
|
|
2038
|
+
)
|
|
2039
|
+
except AnalysisInputError as exc:
|
|
2040
|
+
raise WizardError(str(exc)) from exc
|
|
2041
|
+
requested_value = target["requestedValue"]
|
|
2042
|
+
if not isinstance(requested_value, str):
|
|
2043
|
+
raise WizardError("analysis target resolver returned an invalid requestedValue")
|
|
2044
|
+
state.analysis_target = requested_value
|
|
2045
|
+
state.analysis_target_pending_text = False
|
|
2046
|
+
return requested_value
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def _submit_analysis_target_pick(state: WizardState, value: str) -> Optional[str]:
|
|
2050
|
+
if value == PICK_TYPE_CUSTOM:
|
|
2051
|
+
state.analysis_target_pending_text = True
|
|
2052
|
+
return None
|
|
2053
|
+
return f"analysis-target: {_accept_analysis_target(state, value)}"
|
|
2054
|
+
|
|
2055
|
+
|
|
2056
|
+
def _build_analysis_target(state: WizardState) -> Prompt:
|
|
2057
|
+
t = _p(state.workspace_root, S_ANALYSIS_TARGET)
|
|
2058
|
+
return Prompt(step=S_ANALYSIS_TARGET, kind="text", label=t["label"],
|
|
2059
|
+
echo_template=t["echo_template"])
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
def _submit_analysis_target(state: WizardState, value: str) -> Optional[str]:
|
|
2063
|
+
return f"analysis-target: {_accept_analysis_target(state, value)}"
|
|
2064
|
+
|
|
2065
|
+
|
|
1680
2066
|
def _build_base_ref_pick(state: WizardState) -> Prompt:
|
|
1681
2067
|
t = _p(state.workspace_root, "base_ref_pick")
|
|
1682
2068
|
recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
|
|
@@ -2383,24 +2769,34 @@ def _suggest_latest_final_report(state: WizardState) -> str:
|
|
|
2383
2769
|
runs_base = task_runs_dir(state.project_root, state.task_group, state.task_id)
|
|
2384
2770
|
if not runs_base.is_dir():
|
|
2385
2771
|
return ""
|
|
2772
|
+
try:
|
|
2773
|
+
task_root = _resolved_within(
|
|
2774
|
+
runs_base.parent,
|
|
2775
|
+
Path(state.project_root).resolve(),
|
|
2776
|
+
"task root",
|
|
2777
|
+
)
|
|
2778
|
+
except AnalysisInputError:
|
|
2779
|
+
return ""
|
|
2386
2780
|
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
def _newest(glob_pattern: str) -> Optional[Path]:
|
|
2394
|
-
cands = [p for p in runs_base.glob(glob_pattern) if p.is_file()]
|
|
2395
|
-
return max(cands, key=_mtime_safe) if cands else None
|
|
2396
|
-
|
|
2397
|
-
# 산출물 구조는 runs/<task-type>/reports/final-report-*.md (1단계).
|
|
2398
|
-
best: Optional[Path] = None
|
|
2399
|
-
if state.task_type:
|
|
2781
|
+
# A revision request outranks ordinary recency. Carry the newest requested
|
|
2782
|
+
# revision for the selected analysis type into that exact rerun.
|
|
2783
|
+
revision = _latest_revision_requested_analysis_report(state)
|
|
2784
|
+
best = revision
|
|
2785
|
+
if best is None and state.task_type:
|
|
2400
2786
|
seg = slugify_task_segment(state.task_type)
|
|
2401
|
-
best =
|
|
2787
|
+
best = _newest_contained_final_report(
|
|
2788
|
+
runs_base,
|
|
2789
|
+
task_root,
|
|
2790
|
+
f"{seg}/reports/final-report-*.md",
|
|
2791
|
+
Path(state.project_root),
|
|
2792
|
+
)
|
|
2402
2793
|
if best is None:
|
|
2403
|
-
best =
|
|
2794
|
+
best = _newest_contained_final_report(
|
|
2795
|
+
runs_base,
|
|
2796
|
+
task_root,
|
|
2797
|
+
"*/reports/final-report-*.md",
|
|
2798
|
+
Path(state.project_root),
|
|
2799
|
+
)
|
|
2404
2800
|
if best is None:
|
|
2405
2801
|
return ""
|
|
2406
2802
|
# The approved plan is already wired via --approved-plan. On the first run
|
|
@@ -2735,10 +3131,15 @@ def _submit_executor(state: WizardState, value: str) -> Optional[str]:
|
|
|
2735
3131
|
# §1 clarification 을 갖는 비-implementation phase 로 한정한다(implementation 은
|
|
2736
3132
|
# approved-plan/stage 가 매 run 고유라 재사용 대상이 아니다).
|
|
2737
3133
|
_RESUME_REUSE_PHASES = (
|
|
2738
|
-
"requirements-discovery", "error-analysis", "implementation-planning"
|
|
3134
|
+
"requirements-discovery", "error-analysis", "implementation-planning",
|
|
3135
|
+
*ANALYSIS_TASK_TYPES,
|
|
3136
|
+
)
|
|
2739
3137
|
|
|
2740
3138
|
# YES 선택 시 prefill 로 충족되어 다시 묻지 않는 설정 단계들.
|
|
2741
3139
|
_REUSE_FILLED_STEPS = (
|
|
3140
|
+
S_FEATURE_EVIDENCE_PICK, S_FEATURE_EVIDENCE,
|
|
3141
|
+
S_PROJECT_EVIDENCE_PICK, S_PROJECT_EVIDENCE,
|
|
3142
|
+
S_ANALYSIS_TARGET_PICK, S_ANALYSIS_TARGET,
|
|
2742
3143
|
S_DEFAULTS_OR_CUSTOM, S_WORKERS_OVERRIDE, S_LEAD_MODEL, S_EXECUTOR_MODEL,
|
|
2743
3144
|
S_CLAUDE_MODEL, S_CODEX_MODEL, S_ANTIGRAVITY_MODEL, S_REPORT_WRITER_MODEL,
|
|
2744
3145
|
S_DIRECTIVE_PICK, S_DIRECTIVE, S_RELATED_TASKS_PICK, S_RELATED_TASKS,
|
|
@@ -2750,6 +3151,49 @@ def _has_prior_run_inputs(state: WizardState) -> bool:
|
|
|
2750
3151
|
return bool(_latest_run_inputs(state, phase_segment=seg))
|
|
2751
3152
|
|
|
2752
3153
|
|
|
3154
|
+
def _analysis_inputs_after_reuse(state: WizardState) -> bool:
|
|
3155
|
+
return (
|
|
3156
|
+
state.reuse_previous is not None
|
|
3157
|
+
or state.task_type not in _RESUME_REUSE_PHASES
|
|
3158
|
+
or not _has_prior_run_inputs(state)
|
|
3159
|
+
)
|
|
3160
|
+
|
|
3161
|
+
|
|
3162
|
+
def _restore_analysis_evidence_inputs(
|
|
3163
|
+
state: WizardState, evidence_raw: object
|
|
3164
|
+
) -> tuple[tuple[Any, ...], dict[Path, AnalysisReportCandidate]]:
|
|
3165
|
+
if state.task_type not in ANALYSIS_TASK_TYPES or not isinstance(evidence_raw, str):
|
|
3166
|
+
return (), {}
|
|
3167
|
+
report_paths = [
|
|
3168
|
+
_resolve_path(item.strip(), Path(state.project_root))
|
|
3169
|
+
for item in evidence_raw.split(",")
|
|
3170
|
+
if item.strip()
|
|
3171
|
+
]
|
|
3172
|
+
try:
|
|
3173
|
+
candidates = [
|
|
3174
|
+
load_analysis_report_candidate(Path(state.project_root), path)
|
|
3175
|
+
for path in report_paths
|
|
3176
|
+
]
|
|
3177
|
+
evidence_inputs = resolve_evidence_inputs(
|
|
3178
|
+
Path(state.project_root),
|
|
3179
|
+
state.task_type,
|
|
3180
|
+
[candidate.report_path for candidate in candidates],
|
|
3181
|
+
_analysis_current_commit(state),
|
|
3182
|
+
)
|
|
3183
|
+
except AnalysisInputError as exc:
|
|
3184
|
+
raise WizardError(str(exc)) from exc
|
|
3185
|
+
state.feature_evidence_path = ""
|
|
3186
|
+
state.project_evidence_path = ""
|
|
3187
|
+
for evidence in evidence_inputs:
|
|
3188
|
+
if evidence.relation == "feature-baseline":
|
|
3189
|
+
state.feature_evidence_path = str(evidence.report_path)
|
|
3190
|
+
elif evidence.relation == "project-context":
|
|
3191
|
+
state.project_evidence_path = str(evidence.report_path)
|
|
3192
|
+
return evidence_inputs, {
|
|
3193
|
+
candidate.report_path: candidate for candidate in candidates
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
|
|
2753
3197
|
def _safe_model(provider: str, raw: object) -> str:
|
|
2754
3198
|
"""run-inputs 의 모델 display 값을 wizard 필드로 환원. 알 수 없는 값은
|
|
2755
3199
|
빈 문자열(phase 기본값)로 안전하게 떨어뜨린다."""
|
|
@@ -2795,6 +3239,25 @@ def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
|
|
|
2795
3239
|
state.directive = directive if isinstance(directive, str) else ""
|
|
2796
3240
|
related = inputs.get("relatedTasks")
|
|
2797
3241
|
state.related_tasks_raw = related if isinstance(related, str) else ""
|
|
3242
|
+
evidence_inputs, candidates = _restore_analysis_evidence_inputs(
|
|
3243
|
+
state, inputs.get("evidenceInputs")
|
|
3244
|
+
)
|
|
3245
|
+
target = inputs.get("analysisTarget")
|
|
3246
|
+
if state.task_type == "feature-analysis":
|
|
3247
|
+
try:
|
|
3248
|
+
resolved_target = resolve_analysis_target(
|
|
3249
|
+
target if isinstance(target, str) else "",
|
|
3250
|
+
evidence_inputs,
|
|
3251
|
+
candidates,
|
|
3252
|
+
)
|
|
3253
|
+
except AnalysisInputError as exc:
|
|
3254
|
+
raise WizardError(str(exc)) from exc
|
|
3255
|
+
requested_value = resolved_target.get("requestedValue")
|
|
3256
|
+
if not isinstance(requested_value, str):
|
|
3257
|
+
raise WizardError("analysis target resolver returned an invalid requestedValue")
|
|
3258
|
+
state.analysis_target = requested_value
|
|
3259
|
+
else:
|
|
3260
|
+
state.analysis_target = target if isinstance(target, str) else ""
|
|
2798
3261
|
# clarification 은 직전 run 의 입력이 아니라 "직전 run 의 산출물(가장 최근
|
|
2799
3262
|
# final-report)" 을 재실행 입력으로 자동 선택한다 — resume-clarification 의 본질.
|
|
2800
3263
|
state.clarification_response_path = _suggest_latest_final_report(state)
|
|
@@ -3304,6 +3767,63 @@ STEPS: list[Step] = [
|
|
|
3304
3767
|
and s.task_id_pending_text),
|
|
3305
3768
|
build=_build_task_id_text, submit=_submit_task_id_text,
|
|
3306
3769
|
owns=("task_id", "task_id_pending_text")),
|
|
3770
|
+
Step(S_REUSE_PREVIOUS,
|
|
3771
|
+
applies=lambda s: (_identity_ready(s)
|
|
3772
|
+
and s.use_defaults is None
|
|
3773
|
+
and s.reuse_previous is None
|
|
3774
|
+
and s.task_type in _RESUME_REUSE_PHASES
|
|
3775
|
+
and _has_prior_run_inputs(s)),
|
|
3776
|
+
build=_build_reuse_previous, submit=_submit_reuse_previous,
|
|
3777
|
+
owns=("reuse_previous",)),
|
|
3778
|
+
Step(S_FEATURE_EVIDENCE_PICK,
|
|
3779
|
+
applies=lambda s: (s.task_type == "change-impact-analysis"
|
|
3780
|
+
and not s.feature_evidence_path
|
|
3781
|
+
and not s.feature_evidence_pending_text
|
|
3782
|
+
and S_FEATURE_EVIDENCE_PICK not in s.answered
|
|
3783
|
+
and _brief_resolved(s)
|
|
3784
|
+
and _analysis_inputs_after_reuse(s)),
|
|
3785
|
+
build=_build_feature_evidence_pick, submit=_submit_feature_evidence_pick,
|
|
3786
|
+
owns=("feature_evidence_path", "feature_evidence_pending_text")),
|
|
3787
|
+
Step(S_FEATURE_EVIDENCE,
|
|
3788
|
+
applies=lambda s: (s.task_type == "change-impact-analysis"
|
|
3789
|
+
and s.feature_evidence_pending_text
|
|
3790
|
+
and S_FEATURE_EVIDENCE not in s.answered),
|
|
3791
|
+
build=_build_feature_evidence, submit=_submit_feature_evidence,
|
|
3792
|
+
owns=("feature_evidence_path", "feature_evidence_pending_text")),
|
|
3793
|
+
Step(S_PROJECT_EVIDENCE_PICK,
|
|
3794
|
+
applies=lambda s: (s.task_type in ("feature-analysis", "change-impact-analysis")
|
|
3795
|
+
and not s.project_evidence_path
|
|
3796
|
+
and not s.project_evidence_pending_text
|
|
3797
|
+
and S_PROJECT_EVIDENCE_PICK not in s.answered
|
|
3798
|
+
and _brief_resolved(s)
|
|
3799
|
+
and _analysis_inputs_after_reuse(s)
|
|
3800
|
+
and (s.task_type != "change-impact-analysis"
|
|
3801
|
+
or S_FEATURE_EVIDENCE_PICK in s.answered)),
|
|
3802
|
+
build=_build_project_evidence_pick, submit=_submit_project_evidence_pick,
|
|
3803
|
+
owns=("project_evidence_path", "project_evidence_pending_text",
|
|
3804
|
+
"analysis_target", "analysis_target_pending_text")),
|
|
3805
|
+
Step(S_PROJECT_EVIDENCE,
|
|
3806
|
+
applies=lambda s: (s.task_type in ("feature-analysis", "change-impact-analysis")
|
|
3807
|
+
and s.project_evidence_pending_text
|
|
3808
|
+
and S_PROJECT_EVIDENCE not in s.answered),
|
|
3809
|
+
build=_build_project_evidence, submit=_submit_project_evidence,
|
|
3810
|
+
owns=("project_evidence_path", "project_evidence_pending_text",
|
|
3811
|
+
"analysis_target", "analysis_target_pending_text")),
|
|
3812
|
+
Step(S_ANALYSIS_TARGET_PICK,
|
|
3813
|
+
applies=lambda s: (s.task_type == "feature-analysis"
|
|
3814
|
+
and bool(s.project_evidence_path)
|
|
3815
|
+
and not s.analysis_target
|
|
3816
|
+
and not s.analysis_target_pending_text
|
|
3817
|
+
and S_ANALYSIS_TARGET_PICK not in s.answered),
|
|
3818
|
+
build=_build_analysis_target_pick, submit=_submit_analysis_target_pick,
|
|
3819
|
+
owns=("analysis_target", "analysis_target_pending_text")),
|
|
3820
|
+
Step(S_ANALYSIS_TARGET,
|
|
3821
|
+
applies=lambda s: (s.task_type == "feature-analysis"
|
|
3822
|
+
and not s.analysis_target
|
|
3823
|
+
and s.analysis_target_pending_text
|
|
3824
|
+
and S_ANALYSIS_TARGET not in s.answered),
|
|
3825
|
+
build=_build_analysis_target, submit=_submit_analysis_target,
|
|
3826
|
+
owns=("analysis_target", "analysis_target_pending_text")),
|
|
3307
3827
|
Step(S_BASE_REF_PICK,
|
|
3308
3828
|
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
3309
3829
|
and _base_ref_required(s)
|
|
@@ -3395,18 +3915,6 @@ STEPS: list[Step] = [
|
|
|
3395
3915
|
applies=lambda s: (s.critic_pending_text and S_CRITIC_TEXT not in s.answered),
|
|
3396
3916
|
build=_build_critic_text, submit=_submit_critic_text,
|
|
3397
3917
|
owns=("critic", "critic_pending_text")),
|
|
3398
|
-
# resume: identity 가 모두 정해진 뒤(직전 run 과 같은 task/phase 가 확정),
|
|
3399
|
-
# 모델/워커/directive/related/clarification 설정 단계로 들어가기 직전에
|
|
3400
|
-
# "직전 run 설정 그대로 재사용?" 를 한 번 묻는다. YES 면 그 단계들을 prefill 로
|
|
3401
|
-
# 충족시켜 곧장 confirm 으로 직행하고, NO 면 현행대로 단계별 입력을 받는다.
|
|
3402
|
-
Step(S_REUSE_PREVIOUS,
|
|
3403
|
-
applies=lambda s: (_identity_ready(s)
|
|
3404
|
-
and s.use_defaults is None
|
|
3405
|
-
and s.reuse_previous is None
|
|
3406
|
-
and s.task_type in _RESUME_REUSE_PHASES
|
|
3407
|
-
and _has_prior_run_inputs(s)),
|
|
3408
|
-
build=_build_reuse_previous, submit=_submit_reuse_previous,
|
|
3409
|
-
owns=("reuse_previous",)),
|
|
3410
3918
|
Step(S_DEFAULTS_OR_CUSTOM,
|
|
3411
3919
|
applies=lambda s: (_identity_ready(s)
|
|
3412
3920
|
and s.use_defaults is None),
|
|
@@ -3609,6 +4117,14 @@ def _reset_from(state: WizardState, target_step: str) -> None:
|
|
|
3609
4117
|
for fname in step.owns:
|
|
3610
4118
|
_reset_field(state, fname)
|
|
3611
4119
|
state.answered = [a for a in state.answered if a not in cleared_ids]
|
|
4120
|
+
direct_input_pending = {
|
|
4121
|
+
S_FEATURE_EVIDENCE: "feature_evidence_pending_text",
|
|
4122
|
+
S_PROJECT_EVIDENCE: "project_evidence_pending_text",
|
|
4123
|
+
S_ANALYSIS_TARGET: "analysis_target_pending_text",
|
|
4124
|
+
}
|
|
4125
|
+
pending_field = direct_input_pending.get(target_step)
|
|
4126
|
+
if pending_field is not None:
|
|
4127
|
+
setattr(state, pending_field, True)
|
|
3612
4128
|
|
|
3613
4129
|
|
|
3614
4130
|
_FIELD_DEFAULTS: dict[str, Any] = {
|
|
@@ -3619,6 +4135,9 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
3619
4135
|
"profile_workers": [], "profile_optional_workers": [],
|
|
3620
4136
|
"keep_existing_brief": None,
|
|
3621
4137
|
"brief_path": "", "brief_path_pending_text": False,
|
|
4138
|
+
"project_evidence_path": "", "project_evidence_pending_text": False,
|
|
4139
|
+
"feature_evidence_path": "", "feature_evidence_pending_text": False,
|
|
4140
|
+
"analysis_target": "", "analysis_target_pending_text": False,
|
|
3622
4141
|
"reuse_worktree": None, "base_ref": "",
|
|
3623
4142
|
"base_ref_pending_text": False, "approved_plan_path": "",
|
|
3624
4143
|
"approved_plan_pending_text": False, "approve_plan_candidate": "",
|
|
@@ -3841,6 +4360,8 @@ def submit(state: WizardState, value: str) -> dict[str, Any]:
|
|
|
3841
4360
|
return {"echo": "", "next": prompt_payload(state, prompt)}
|
|
3842
4361
|
if prompt.kind == "pick_group":
|
|
3843
4362
|
return _submit_group(state, prompt, value)
|
|
4363
|
+
if prompt.kind == "pick" and len(prompt.options) > HOST_PICK_OPTION_CAP:
|
|
4364
|
+
value = _normalize_numbered_pick_answer(prompt, value)
|
|
3844
4365
|
step = STEP_BY_ID[prompt.step]
|
|
3845
4366
|
echo = step.submit(state, value or "")
|
|
3846
4367
|
if prompt.step not in state.answered:
|
|
@@ -3876,6 +4397,11 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
3876
4397
|
if state.task_type == "release-handoff"
|
|
3877
4398
|
else ""
|
|
3878
4399
|
)
|
|
4400
|
+
evidence_paths = [
|
|
4401
|
+
path
|
|
4402
|
+
for path in (state.feature_evidence_path, state.project_evidence_path)
|
|
4403
|
+
if path
|
|
4404
|
+
]
|
|
3879
4405
|
return {
|
|
3880
4406
|
"project-root": state.project_root,
|
|
3881
4407
|
"project-id": state.project_id,
|
|
@@ -3883,6 +4409,8 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
3883
4409
|
"task-id": state.task_id,
|
|
3884
4410
|
"task-type": state.task_type,
|
|
3885
4411
|
"task-brief": state.brief_path,
|
|
4412
|
+
"analysis-target": state.analysis_target,
|
|
4413
|
+
"evidence-inputs": ",".join(evidence_paths),
|
|
3886
4414
|
"executor": state.executor,
|
|
3887
4415
|
"critic": state.critic,
|
|
3888
4416
|
"approved-plan": state.approved_plan_path,
|
|
@@ -3911,6 +4439,25 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
3911
4439
|
lines.append(f" task-type : {state.task_type}")
|
|
3912
4440
|
lines.append(f" task-key : {state.task_group}/{state.task_id}")
|
|
3913
4441
|
lines.append(f" brief : {state.brief_path or '(none)'}")
|
|
4442
|
+
if state.analysis_target:
|
|
4443
|
+
lines.append(f" analysis-target: {state.analysis_target}")
|
|
4444
|
+
if state.feature_evidence_path or state.project_evidence_path:
|
|
4445
|
+
evidence_status = {
|
|
4446
|
+
str(item.report_path): item.review_status
|
|
4447
|
+
for item in _resolve_analysis_evidence(state)
|
|
4448
|
+
}
|
|
4449
|
+
if state.feature_evidence_path:
|
|
4450
|
+
lines.append(
|
|
4451
|
+
" feature-evidence: "
|
|
4452
|
+
f"{state.feature_evidence_path} "
|
|
4453
|
+
f"({evidence_status.get(state.feature_evidence_path, 'unknown')})"
|
|
4454
|
+
)
|
|
4455
|
+
if state.project_evidence_path:
|
|
4456
|
+
lines.append(
|
|
4457
|
+
" project-evidence: "
|
|
4458
|
+
f"{state.project_evidence_path} "
|
|
4459
|
+
f"({evidence_status.get(state.project_evidence_path, 'unknown')})"
|
|
4460
|
+
)
|
|
3914
4461
|
if state.task_type == "final-verification":
|
|
3915
4462
|
lines.append(" base-ref : (selected stage worktree)")
|
|
3916
4463
|
elif state.task_type == "implementation" and state.reuse_worktree:
|