okstra 0.143.0 → 0.145.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 +18 -2
- package/docs/cli.md +39 -2
- package/docs/project-structure-overview.md +19 -6
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/coding-preflight/overview.md +1 -1
- package/runtime/prompts/lead/convergence.md +11 -3
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -1
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +48 -2
- 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/clarification_items.py +74 -1
- package/runtime/python/okstra_ctl/mutation_probe.py +1263 -0
- package/runtime/python/okstra_ctl/render.py +77 -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/self_mock_signals.py +183 -0
- package/runtime/python/okstra_ctl/user_response.py +309 -3
- package/runtime/python/okstra_ctl/wizard.py +545 -32
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -0
- package/runtime/python/okstra_ctl/workflow.py +22 -0
- package/runtime/schemas/final-report-v1.0.schema.json +849 -3
- package/runtime/skills/okstra-run/SKILL.md +13 -1
- 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/detect_self_mock.py +220 -0
- package/runtime/validators/validate-report-views.py +61 -7
- package/runtime/validators/validate-run.py +518 -0
- package/runtime/validators/validate_analysis_report.py +864 -0
- package/src/commands/execute/render-bundle.mjs +3 -0
|
@@ -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,8 +151,12 @@ _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__"
|
|
@@ -263,6 +287,12 @@ S_BRIEF_KEEP = "brief_keep"
|
|
|
263
287
|
S_BRIEF_PATH_PICK = "brief_path_pick"
|
|
264
288
|
S_BRIEF_PATH = "brief_path"
|
|
265
289
|
S_BRIEF_CARRY = "brief_carry"
|
|
290
|
+
S_FEATURE_EVIDENCE_PICK = "feature_evidence_pick"
|
|
291
|
+
S_FEATURE_EVIDENCE = "feature_evidence"
|
|
292
|
+
S_PROJECT_EVIDENCE_PICK = "project_evidence_pick"
|
|
293
|
+
S_PROJECT_EVIDENCE = "project_evidence"
|
|
294
|
+
S_ANALYSIS_TARGET_PICK = "analysis_target_pick"
|
|
295
|
+
S_ANALYSIS_TARGET = "analysis_target"
|
|
266
296
|
S_BASE_REF_PICK = "base_ref_pick"
|
|
267
297
|
S_BASE_REF_TEXT = "base_ref_text"
|
|
268
298
|
S_APPROVED_PLAN_PICK = "approved_plan_pick"
|
|
@@ -355,6 +385,14 @@ class WizardState:
|
|
|
355
385
|
brief_path: str = ""
|
|
356
386
|
brief_path_pending_text: bool = False
|
|
357
387
|
|
|
388
|
+
# analysis evidence and target
|
|
389
|
+
project_evidence_path: str = ""
|
|
390
|
+
project_evidence_pending_text: bool = False
|
|
391
|
+
feature_evidence_path: str = ""
|
|
392
|
+
feature_evidence_pending_text: bool = False
|
|
393
|
+
analysis_target: str = ""
|
|
394
|
+
analysis_target_pending_text: bool = False
|
|
395
|
+
|
|
358
396
|
# worktree
|
|
359
397
|
reuse_worktree: Optional[bool] = None
|
|
360
398
|
base_ref: str = ""
|
|
@@ -1445,6 +1483,101 @@ def _recent_task_types(state: WizardState) -> list[str]:
|
|
|
1445
1483
|
return out
|
|
1446
1484
|
|
|
1447
1485
|
|
|
1486
|
+
def _contained_final_report_data_path(report: Path, task_root: Path) -> Path:
|
|
1487
|
+
_resolved_within(report, task_root, "final report path")
|
|
1488
|
+
return _resolved_within(
|
|
1489
|
+
final_report_data_path(report),
|
|
1490
|
+
task_root,
|
|
1491
|
+
"final report data path",
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def _newest_contained_final_report(
|
|
1496
|
+
runs_base: Path,
|
|
1497
|
+
task_root: Path,
|
|
1498
|
+
glob_pattern: str,
|
|
1499
|
+
project_root: Path,
|
|
1500
|
+
) -> Path | None:
|
|
1501
|
+
candidates: list[Path] = []
|
|
1502
|
+
for report in runs_base.glob(glob_pattern):
|
|
1503
|
+
if not report.is_file():
|
|
1504
|
+
continue
|
|
1505
|
+
try:
|
|
1506
|
+
_contained_final_report_data_path(report, task_root)
|
|
1507
|
+
if report.parent.parent.name in ANALYSIS_TASK_TYPES:
|
|
1508
|
+
load_analysis_report_candidate(project_root, report)
|
|
1509
|
+
except AnalysisInputError:
|
|
1510
|
+
continue
|
|
1511
|
+
candidates.append(report)
|
|
1512
|
+
|
|
1513
|
+
def mtime_safe(report: Path) -> float:
|
|
1514
|
+
try:
|
|
1515
|
+
return report.stat().st_mtime
|
|
1516
|
+
except OSError:
|
|
1517
|
+
return -1.0
|
|
1518
|
+
|
|
1519
|
+
return max(candidates, key=mtime_safe) if candidates else None
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
def _analysis_revision_candidate(
|
|
1523
|
+
state: WizardState,
|
|
1524
|
+
report: Path,
|
|
1525
|
+
task_root: Path,
|
|
1526
|
+
expected_task_key: str,
|
|
1527
|
+
) -> tuple[int, Path] | None:
|
|
1528
|
+
try:
|
|
1529
|
+
_contained_final_report_data_path(report, task_root)
|
|
1530
|
+
candidate = load_analysis_report_candidate(
|
|
1531
|
+
Path(state.project_root), report
|
|
1532
|
+
)
|
|
1533
|
+
except AnalysisInputError:
|
|
1534
|
+
return None
|
|
1535
|
+
if (
|
|
1536
|
+
candidate.task_key != expected_task_key
|
|
1537
|
+
or candidate.task_type != state.task_type
|
|
1538
|
+
or candidate.review_status != "revision-requested"
|
|
1539
|
+
):
|
|
1540
|
+
return None
|
|
1541
|
+
return int(candidate.run_seq), report
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
def _latest_revision_requested_analysis_report(
|
|
1545
|
+
state: WizardState,
|
|
1546
|
+
) -> Path | None:
|
|
1547
|
+
if state.task_type not in ANALYSIS_TASK_TYPES:
|
|
1548
|
+
return None
|
|
1549
|
+
reports = task_runs_dir(
|
|
1550
|
+
Path(state.project_root), state.task_group, state.task_id
|
|
1551
|
+
) / state.task_type / "reports"
|
|
1552
|
+
try:
|
|
1553
|
+
task_root = _resolved_within(
|
|
1554
|
+
reports.parents[2],
|
|
1555
|
+
Path(state.project_root).resolve(),
|
|
1556
|
+
"analysis task root",
|
|
1557
|
+
)
|
|
1558
|
+
except AnalysisInputError:
|
|
1559
|
+
return None
|
|
1560
|
+
expected_task_key = f"{state.project_id}:{state.task_group}:{state.task_id}"
|
|
1561
|
+
candidates: list[tuple[int, Path]] = []
|
|
1562
|
+
for report in reports.glob("final-report-*.md"):
|
|
1563
|
+
candidate = _analysis_revision_candidate(
|
|
1564
|
+
state,
|
|
1565
|
+
report,
|
|
1566
|
+
task_root,
|
|
1567
|
+
expected_task_key,
|
|
1568
|
+
)
|
|
1569
|
+
if candidate is not None:
|
|
1570
|
+
candidates.append(candidate)
|
|
1571
|
+
if not candidates:
|
|
1572
|
+
return None
|
|
1573
|
+
return max(candidates, key=lambda candidate: candidate[0])[1]
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def _latest_revision_requested_analysis_type(state: WizardState) -> str:
|
|
1577
|
+
report = _latest_revision_requested_analysis_report(state)
|
|
1578
|
+
return state.task_type if report is not None else ""
|
|
1579
|
+
|
|
1580
|
+
|
|
1448
1581
|
def _build_task_type(state: WizardState) -> Prompt:
|
|
1449
1582
|
t = _p(state.workspace_root, "task_type")
|
|
1450
1583
|
recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
|
|
@@ -1464,7 +1597,9 @@ def _build_task_type(state: WizardState) -> Prompt:
|
|
|
1464
1597
|
description_by_type[task_type]))
|
|
1465
1598
|
|
|
1466
1599
|
workflow = _existing_task_workflow(state)
|
|
1467
|
-
|
|
1600
|
+
revision_requested = _latest_revision_requested_analysis_type(state)
|
|
1601
|
+
recommended = (revision_requested or state.task_type
|
|
1602
|
+
or workflow.get("nextRecommendedPhase") or "")
|
|
1468
1603
|
if not recommended and not workflow:
|
|
1469
1604
|
recommended = TASK_TYPE_VALUES[0]
|
|
1470
1605
|
add(recommended, recommended_suffix)
|
|
@@ -1677,6 +1812,225 @@ def _submit_brief_carry(state: WizardState, value: str) -> Optional[str]:
|
|
|
1677
1812
|
)
|
|
1678
1813
|
|
|
1679
1814
|
|
|
1815
|
+
def _analysis_current_commit(state: WizardState) -> str:
|
|
1816
|
+
try:
|
|
1817
|
+
return subprocess.check_output(
|
|
1818
|
+
["git", "-C", state.project_root, "rev-parse", "HEAD"],
|
|
1819
|
+
text=True,
|
|
1820
|
+
stderr=subprocess.DEVNULL,
|
|
1821
|
+
).strip()
|
|
1822
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
1823
|
+
raise WizardError(
|
|
1824
|
+
f"cannot resolve current project commit for analysis evidence: {state.project_root}"
|
|
1825
|
+
) from exc
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
def _analysis_evidence_paths(state: WizardState) -> list[Path]:
|
|
1829
|
+
return [
|
|
1830
|
+
Path(path)
|
|
1831
|
+
for path in (state.feature_evidence_path, state.project_evidence_path)
|
|
1832
|
+
if path
|
|
1833
|
+
]
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
def _resolve_analysis_evidence(state: WizardState):
|
|
1837
|
+
try:
|
|
1838
|
+
return resolve_evidence_inputs(
|
|
1839
|
+
Path(state.project_root),
|
|
1840
|
+
state.task_type,
|
|
1841
|
+
_analysis_evidence_paths(state),
|
|
1842
|
+
_analysis_current_commit(state),
|
|
1843
|
+
)
|
|
1844
|
+
except AnalysisInputError as exc:
|
|
1845
|
+
raise WizardError(str(exc)) from exc
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
def _accept_analysis_evidence(
|
|
1849
|
+
state: WizardState, raw_path: str, field_name: str
|
|
1850
|
+
) -> AnalysisReportCandidate:
|
|
1851
|
+
report_path = _resolve_path(raw_path, Path(state.project_root))
|
|
1852
|
+
try:
|
|
1853
|
+
candidate = load_analysis_report_candidate(
|
|
1854
|
+
Path(state.project_root), report_path
|
|
1855
|
+
)
|
|
1856
|
+
except AnalysisInputError as exc:
|
|
1857
|
+
raise WizardError(str(exc)) from exc
|
|
1858
|
+
previous = getattr(state, field_name)
|
|
1859
|
+
setattr(state, field_name, str(candidate.report_path))
|
|
1860
|
+
try:
|
|
1861
|
+
_resolve_analysis_evidence(state)
|
|
1862
|
+
except WizardError:
|
|
1863
|
+
setattr(state, field_name, previous)
|
|
1864
|
+
raise
|
|
1865
|
+
return candidate
|
|
1866
|
+
|
|
1867
|
+
|
|
1868
|
+
def _evidence_option_label(candidate: AnalysisReportCandidate) -> str:
|
|
1869
|
+
return f"{candidate.task_key} · {candidate.task_type} · run {candidate.run_seq}"
|
|
1870
|
+
|
|
1871
|
+
|
|
1872
|
+
def _build_analysis_evidence_pick(
|
|
1873
|
+
state: WizardState, *, step: str, relation: str
|
|
1874
|
+
) -> Prompt:
|
|
1875
|
+
t = _p(state.workspace_root, step)
|
|
1876
|
+
candidates = list_evidence_candidates(
|
|
1877
|
+
Path(state.project_root), state.task_type, relation
|
|
1878
|
+
)[:2]
|
|
1879
|
+
options = [
|
|
1880
|
+
_opt(str(candidate.report_path), _evidence_option_label(candidate))
|
|
1881
|
+
for candidate in candidates
|
|
1882
|
+
]
|
|
1883
|
+
options.extend([
|
|
1884
|
+
_opt(PICK_SKIP, t["options"][PICK_SKIP]),
|
|
1885
|
+
_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]),
|
|
1886
|
+
])
|
|
1887
|
+
return Prompt(step=step, kind="pick", label=t["label"], options=options,
|
|
1888
|
+
echo_template=t["echo_template"])
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
def _submit_analysis_evidence_pick(
|
|
1892
|
+
state: WizardState,
|
|
1893
|
+
value: str,
|
|
1894
|
+
*,
|
|
1895
|
+
step: str,
|
|
1896
|
+
field_name: str,
|
|
1897
|
+
pending_name: str,
|
|
1898
|
+
target_after_skip: bool = False,
|
|
1899
|
+
) -> Optional[str]:
|
|
1900
|
+
t = _p(state.workspace_root, step)
|
|
1901
|
+
if value == PICK_SKIP:
|
|
1902
|
+
setattr(state, field_name, "")
|
|
1903
|
+
setattr(state, pending_name, False)
|
|
1904
|
+
if target_after_skip:
|
|
1905
|
+
state.analysis_target_pending_text = True
|
|
1906
|
+
return t["echo_variants"]["skip"]
|
|
1907
|
+
if value == PICK_TYPE_CUSTOM:
|
|
1908
|
+
setattr(state, pending_name, True)
|
|
1909
|
+
return None
|
|
1910
|
+
candidate = _accept_analysis_evidence(state, value, field_name)
|
|
1911
|
+
setattr(state, pending_name, False)
|
|
1912
|
+
return t["echo_variants"]["selected"].format(path=candidate.report_path)
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
def _build_feature_evidence_pick(state: WizardState) -> Prompt:
|
|
1916
|
+
return _build_analysis_evidence_pick(
|
|
1917
|
+
state, step=S_FEATURE_EVIDENCE_PICK, relation="feature-baseline"
|
|
1918
|
+
)
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
def _submit_feature_evidence_pick(state: WizardState, value: str) -> Optional[str]:
|
|
1922
|
+
return _submit_analysis_evidence_pick(
|
|
1923
|
+
state, value, step=S_FEATURE_EVIDENCE_PICK,
|
|
1924
|
+
field_name="feature_evidence_path",
|
|
1925
|
+
pending_name="feature_evidence_pending_text",
|
|
1926
|
+
)
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
def _build_feature_evidence(state: WizardState) -> Prompt:
|
|
1930
|
+
t = _p(state.workspace_root, S_FEATURE_EVIDENCE)
|
|
1931
|
+
return Prompt(step=S_FEATURE_EVIDENCE, kind="text", label=t["label"],
|
|
1932
|
+
echo_template=t["echo_template"])
|
|
1933
|
+
|
|
1934
|
+
|
|
1935
|
+
def _submit_feature_evidence(state: WizardState, value: str) -> Optional[str]:
|
|
1936
|
+
candidate = _accept_analysis_evidence(state, value, "feature_evidence_path")
|
|
1937
|
+
state.feature_evidence_pending_text = False
|
|
1938
|
+
return f"feature-evidence: {candidate.report_path}"
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
def _build_project_evidence_pick(state: WizardState) -> Prompt:
|
|
1942
|
+
return _build_analysis_evidence_pick(
|
|
1943
|
+
state, step=S_PROJECT_EVIDENCE_PICK, relation="project-context"
|
|
1944
|
+
)
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
def _submit_project_evidence_pick(state: WizardState, value: str) -> Optional[str]:
|
|
1948
|
+
return _submit_analysis_evidence_pick(
|
|
1949
|
+
state, value, step=S_PROJECT_EVIDENCE_PICK,
|
|
1950
|
+
field_name="project_evidence_path",
|
|
1951
|
+
pending_name="project_evidence_pending_text",
|
|
1952
|
+
target_after_skip=state.task_type == "feature-analysis",
|
|
1953
|
+
)
|
|
1954
|
+
|
|
1955
|
+
|
|
1956
|
+
def _build_project_evidence(state: WizardState) -> Prompt:
|
|
1957
|
+
t = _p(state.workspace_root, S_PROJECT_EVIDENCE)
|
|
1958
|
+
return Prompt(step=S_PROJECT_EVIDENCE, kind="text", label=t["label"],
|
|
1959
|
+
echo_template=t["echo_template"])
|
|
1960
|
+
|
|
1961
|
+
|
|
1962
|
+
def _submit_project_evidence(state: WizardState, value: str) -> Optional[str]:
|
|
1963
|
+
candidate = _accept_analysis_evidence(state, value, "project_evidence_path")
|
|
1964
|
+
state.project_evidence_pending_text = False
|
|
1965
|
+
return f"project-evidence: {candidate.report_path}"
|
|
1966
|
+
|
|
1967
|
+
|
|
1968
|
+
def _feature_description(feature: dict[object, object]) -> str:
|
|
1969
|
+
name = str(feature.get("name") or "")
|
|
1970
|
+
summary = str(feature.get("summary") or "")
|
|
1971
|
+
entry_point = str(
|
|
1972
|
+
feature.get("entryPoint") or feature.get("entrypoint") or ""
|
|
1973
|
+
)
|
|
1974
|
+
return " · ".join(value for value in (name, summary, entry_point) if value)
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
def _build_analysis_target_pick(state: WizardState) -> Prompt:
|
|
1978
|
+
t = _p(state.workspace_root, S_ANALYSIS_TARGET_PICK)
|
|
1979
|
+
try:
|
|
1980
|
+
candidate = load_analysis_report_candidate(
|
|
1981
|
+
Path(state.project_root), Path(state.project_evidence_path)
|
|
1982
|
+
)
|
|
1983
|
+
except AnalysisInputError as exc:
|
|
1984
|
+
raise WizardError(str(exc)) from exc
|
|
1985
|
+
options = [
|
|
1986
|
+
_opt(str(feature["id"]), str(feature["id"]), _feature_description(feature))
|
|
1987
|
+
for feature in candidate.feature_index
|
|
1988
|
+
if isinstance(feature, dict) and isinstance(feature.get("id"), str)
|
|
1989
|
+
][:3]
|
|
1990
|
+
options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
|
|
1991
|
+
return Prompt(step=S_ANALYSIS_TARGET_PICK, kind="pick", label=t["label"],
|
|
1992
|
+
options=options, echo_template=t["echo_template"])
|
|
1993
|
+
|
|
1994
|
+
|
|
1995
|
+
def _accept_analysis_target(state: WizardState, value: str) -> str:
|
|
1996
|
+
candidates: dict[Path, AnalysisReportCandidate] = {}
|
|
1997
|
+
for path in _analysis_evidence_paths(state):
|
|
1998
|
+
try:
|
|
1999
|
+
candidate = load_analysis_report_candidate(Path(state.project_root), path)
|
|
2000
|
+
except AnalysisInputError as exc:
|
|
2001
|
+
raise WizardError(str(exc)) from exc
|
|
2002
|
+
candidates[candidate.report_path] = candidate
|
|
2003
|
+
try:
|
|
2004
|
+
target = resolve_analysis_target(
|
|
2005
|
+
value, _resolve_analysis_evidence(state), candidates
|
|
2006
|
+
)
|
|
2007
|
+
except AnalysisInputError as exc:
|
|
2008
|
+
raise WizardError(str(exc)) from exc
|
|
2009
|
+
requested_value = target["requestedValue"]
|
|
2010
|
+
if not isinstance(requested_value, str):
|
|
2011
|
+
raise WizardError("analysis target resolver returned an invalid requestedValue")
|
|
2012
|
+
state.analysis_target = requested_value
|
|
2013
|
+
state.analysis_target_pending_text = False
|
|
2014
|
+
return requested_value
|
|
2015
|
+
|
|
2016
|
+
|
|
2017
|
+
def _submit_analysis_target_pick(state: WizardState, value: str) -> Optional[str]:
|
|
2018
|
+
if value == PICK_TYPE_CUSTOM:
|
|
2019
|
+
state.analysis_target_pending_text = True
|
|
2020
|
+
return None
|
|
2021
|
+
return f"analysis-target: {_accept_analysis_target(state, value)}"
|
|
2022
|
+
|
|
2023
|
+
|
|
2024
|
+
def _build_analysis_target(state: WizardState) -> Prompt:
|
|
2025
|
+
t = _p(state.workspace_root, S_ANALYSIS_TARGET)
|
|
2026
|
+
return Prompt(step=S_ANALYSIS_TARGET, kind="text", label=t["label"],
|
|
2027
|
+
echo_template=t["echo_template"])
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
def _submit_analysis_target(state: WizardState, value: str) -> Optional[str]:
|
|
2031
|
+
return f"analysis-target: {_accept_analysis_target(state, value)}"
|
|
2032
|
+
|
|
2033
|
+
|
|
1680
2034
|
def _build_base_ref_pick(state: WizardState) -> Prompt:
|
|
1681
2035
|
t = _p(state.workspace_root, "base_ref_pick")
|
|
1682
2036
|
recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
|
|
@@ -2383,24 +2737,34 @@ def _suggest_latest_final_report(state: WizardState) -> str:
|
|
|
2383
2737
|
runs_base = task_runs_dir(state.project_root, state.task_group, state.task_id)
|
|
2384
2738
|
if not runs_base.is_dir():
|
|
2385
2739
|
return ""
|
|
2740
|
+
try:
|
|
2741
|
+
task_root = _resolved_within(
|
|
2742
|
+
runs_base.parent,
|
|
2743
|
+
Path(state.project_root).resolve(),
|
|
2744
|
+
"task root",
|
|
2745
|
+
)
|
|
2746
|
+
except AnalysisInputError:
|
|
2747
|
+
return ""
|
|
2386
2748
|
|
|
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:
|
|
2749
|
+
# A revision request outranks ordinary recency. Carry the newest requested
|
|
2750
|
+
# revision for the selected analysis type into that exact rerun.
|
|
2751
|
+
revision = _latest_revision_requested_analysis_report(state)
|
|
2752
|
+
best = revision
|
|
2753
|
+
if best is None and state.task_type:
|
|
2400
2754
|
seg = slugify_task_segment(state.task_type)
|
|
2401
|
-
best =
|
|
2755
|
+
best = _newest_contained_final_report(
|
|
2756
|
+
runs_base,
|
|
2757
|
+
task_root,
|
|
2758
|
+
f"{seg}/reports/final-report-*.md",
|
|
2759
|
+
Path(state.project_root),
|
|
2760
|
+
)
|
|
2402
2761
|
if best is None:
|
|
2403
|
-
best =
|
|
2762
|
+
best = _newest_contained_final_report(
|
|
2763
|
+
runs_base,
|
|
2764
|
+
task_root,
|
|
2765
|
+
"*/reports/final-report-*.md",
|
|
2766
|
+
Path(state.project_root),
|
|
2767
|
+
)
|
|
2404
2768
|
if best is None:
|
|
2405
2769
|
return ""
|
|
2406
2770
|
# The approved plan is already wired via --approved-plan. On the first run
|
|
@@ -2735,10 +3099,15 @@ def _submit_executor(state: WizardState, value: str) -> Optional[str]:
|
|
|
2735
3099
|
# §1 clarification 을 갖는 비-implementation phase 로 한정한다(implementation 은
|
|
2736
3100
|
# approved-plan/stage 가 매 run 고유라 재사용 대상이 아니다).
|
|
2737
3101
|
_RESUME_REUSE_PHASES = (
|
|
2738
|
-
"requirements-discovery", "error-analysis", "implementation-planning"
|
|
3102
|
+
"requirements-discovery", "error-analysis", "implementation-planning",
|
|
3103
|
+
*ANALYSIS_TASK_TYPES,
|
|
3104
|
+
)
|
|
2739
3105
|
|
|
2740
3106
|
# YES 선택 시 prefill 로 충족되어 다시 묻지 않는 설정 단계들.
|
|
2741
3107
|
_REUSE_FILLED_STEPS = (
|
|
3108
|
+
S_FEATURE_EVIDENCE_PICK, S_FEATURE_EVIDENCE,
|
|
3109
|
+
S_PROJECT_EVIDENCE_PICK, S_PROJECT_EVIDENCE,
|
|
3110
|
+
S_ANALYSIS_TARGET_PICK, S_ANALYSIS_TARGET,
|
|
2742
3111
|
S_DEFAULTS_OR_CUSTOM, S_WORKERS_OVERRIDE, S_LEAD_MODEL, S_EXECUTOR_MODEL,
|
|
2743
3112
|
S_CLAUDE_MODEL, S_CODEX_MODEL, S_ANTIGRAVITY_MODEL, S_REPORT_WRITER_MODEL,
|
|
2744
3113
|
S_DIRECTIVE_PICK, S_DIRECTIVE, S_RELATED_TASKS_PICK, S_RELATED_TASKS,
|
|
@@ -2750,6 +3119,49 @@ def _has_prior_run_inputs(state: WizardState) -> bool:
|
|
|
2750
3119
|
return bool(_latest_run_inputs(state, phase_segment=seg))
|
|
2751
3120
|
|
|
2752
3121
|
|
|
3122
|
+
def _analysis_inputs_after_reuse(state: WizardState) -> bool:
|
|
3123
|
+
return (
|
|
3124
|
+
state.reuse_previous is not None
|
|
3125
|
+
or state.task_type not in _RESUME_REUSE_PHASES
|
|
3126
|
+
or not _has_prior_run_inputs(state)
|
|
3127
|
+
)
|
|
3128
|
+
|
|
3129
|
+
|
|
3130
|
+
def _restore_analysis_evidence_inputs(
|
|
3131
|
+
state: WizardState, evidence_raw: object
|
|
3132
|
+
) -> tuple[tuple[Any, ...], dict[Path, AnalysisReportCandidate]]:
|
|
3133
|
+
if state.task_type not in ANALYSIS_TASK_TYPES or not isinstance(evidence_raw, str):
|
|
3134
|
+
return (), {}
|
|
3135
|
+
report_paths = [
|
|
3136
|
+
_resolve_path(item.strip(), Path(state.project_root))
|
|
3137
|
+
for item in evidence_raw.split(",")
|
|
3138
|
+
if item.strip()
|
|
3139
|
+
]
|
|
3140
|
+
try:
|
|
3141
|
+
candidates = [
|
|
3142
|
+
load_analysis_report_candidate(Path(state.project_root), path)
|
|
3143
|
+
for path in report_paths
|
|
3144
|
+
]
|
|
3145
|
+
evidence_inputs = resolve_evidence_inputs(
|
|
3146
|
+
Path(state.project_root),
|
|
3147
|
+
state.task_type,
|
|
3148
|
+
[candidate.report_path for candidate in candidates],
|
|
3149
|
+
_analysis_current_commit(state),
|
|
3150
|
+
)
|
|
3151
|
+
except AnalysisInputError as exc:
|
|
3152
|
+
raise WizardError(str(exc)) from exc
|
|
3153
|
+
state.feature_evidence_path = ""
|
|
3154
|
+
state.project_evidence_path = ""
|
|
3155
|
+
for evidence in evidence_inputs:
|
|
3156
|
+
if evidence.relation == "feature-baseline":
|
|
3157
|
+
state.feature_evidence_path = str(evidence.report_path)
|
|
3158
|
+
elif evidence.relation == "project-context":
|
|
3159
|
+
state.project_evidence_path = str(evidence.report_path)
|
|
3160
|
+
return evidence_inputs, {
|
|
3161
|
+
candidate.report_path: candidate for candidate in candidates
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
|
|
2753
3165
|
def _safe_model(provider: str, raw: object) -> str:
|
|
2754
3166
|
"""run-inputs 의 모델 display 값을 wizard 필드로 환원. 알 수 없는 값은
|
|
2755
3167
|
빈 문자열(phase 기본값)로 안전하게 떨어뜨린다."""
|
|
@@ -2795,6 +3207,25 @@ def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
|
|
|
2795
3207
|
state.directive = directive if isinstance(directive, str) else ""
|
|
2796
3208
|
related = inputs.get("relatedTasks")
|
|
2797
3209
|
state.related_tasks_raw = related if isinstance(related, str) else ""
|
|
3210
|
+
evidence_inputs, candidates = _restore_analysis_evidence_inputs(
|
|
3211
|
+
state, inputs.get("evidenceInputs")
|
|
3212
|
+
)
|
|
3213
|
+
target = inputs.get("analysisTarget")
|
|
3214
|
+
if state.task_type == "feature-analysis":
|
|
3215
|
+
try:
|
|
3216
|
+
resolved_target = resolve_analysis_target(
|
|
3217
|
+
target if isinstance(target, str) else "",
|
|
3218
|
+
evidence_inputs,
|
|
3219
|
+
candidates,
|
|
3220
|
+
)
|
|
3221
|
+
except AnalysisInputError as exc:
|
|
3222
|
+
raise WizardError(str(exc)) from exc
|
|
3223
|
+
requested_value = resolved_target.get("requestedValue")
|
|
3224
|
+
if not isinstance(requested_value, str):
|
|
3225
|
+
raise WizardError("analysis target resolver returned an invalid requestedValue")
|
|
3226
|
+
state.analysis_target = requested_value
|
|
3227
|
+
else:
|
|
3228
|
+
state.analysis_target = target if isinstance(target, str) else ""
|
|
2798
3229
|
# clarification 은 직전 run 의 입력이 아니라 "직전 run 의 산출물(가장 최근
|
|
2799
3230
|
# final-report)" 을 재실행 입력으로 자동 선택한다 — resume-clarification 의 본질.
|
|
2800
3231
|
state.clarification_response_path = _suggest_latest_final_report(state)
|
|
@@ -3304,6 +3735,63 @@ STEPS: list[Step] = [
|
|
|
3304
3735
|
and s.task_id_pending_text),
|
|
3305
3736
|
build=_build_task_id_text, submit=_submit_task_id_text,
|
|
3306
3737
|
owns=("task_id", "task_id_pending_text")),
|
|
3738
|
+
Step(S_REUSE_PREVIOUS,
|
|
3739
|
+
applies=lambda s: (_identity_ready(s)
|
|
3740
|
+
and s.use_defaults is None
|
|
3741
|
+
and s.reuse_previous is None
|
|
3742
|
+
and s.task_type in _RESUME_REUSE_PHASES
|
|
3743
|
+
and _has_prior_run_inputs(s)),
|
|
3744
|
+
build=_build_reuse_previous, submit=_submit_reuse_previous,
|
|
3745
|
+
owns=("reuse_previous",)),
|
|
3746
|
+
Step(S_FEATURE_EVIDENCE_PICK,
|
|
3747
|
+
applies=lambda s: (s.task_type == "change-impact-analysis"
|
|
3748
|
+
and not s.feature_evidence_path
|
|
3749
|
+
and not s.feature_evidence_pending_text
|
|
3750
|
+
and S_FEATURE_EVIDENCE_PICK not in s.answered
|
|
3751
|
+
and _brief_resolved(s)
|
|
3752
|
+
and _analysis_inputs_after_reuse(s)),
|
|
3753
|
+
build=_build_feature_evidence_pick, submit=_submit_feature_evidence_pick,
|
|
3754
|
+
owns=("feature_evidence_path", "feature_evidence_pending_text")),
|
|
3755
|
+
Step(S_FEATURE_EVIDENCE,
|
|
3756
|
+
applies=lambda s: (s.task_type == "change-impact-analysis"
|
|
3757
|
+
and s.feature_evidence_pending_text
|
|
3758
|
+
and S_FEATURE_EVIDENCE not in s.answered),
|
|
3759
|
+
build=_build_feature_evidence, submit=_submit_feature_evidence,
|
|
3760
|
+
owns=("feature_evidence_path", "feature_evidence_pending_text")),
|
|
3761
|
+
Step(S_PROJECT_EVIDENCE_PICK,
|
|
3762
|
+
applies=lambda s: (s.task_type in ("feature-analysis", "change-impact-analysis")
|
|
3763
|
+
and not s.project_evidence_path
|
|
3764
|
+
and not s.project_evidence_pending_text
|
|
3765
|
+
and S_PROJECT_EVIDENCE_PICK not in s.answered
|
|
3766
|
+
and _brief_resolved(s)
|
|
3767
|
+
and _analysis_inputs_after_reuse(s)
|
|
3768
|
+
and (s.task_type != "change-impact-analysis"
|
|
3769
|
+
or S_FEATURE_EVIDENCE_PICK in s.answered)),
|
|
3770
|
+
build=_build_project_evidence_pick, submit=_submit_project_evidence_pick,
|
|
3771
|
+
owns=("project_evidence_path", "project_evidence_pending_text",
|
|
3772
|
+
"analysis_target", "analysis_target_pending_text")),
|
|
3773
|
+
Step(S_PROJECT_EVIDENCE,
|
|
3774
|
+
applies=lambda s: (s.task_type in ("feature-analysis", "change-impact-analysis")
|
|
3775
|
+
and s.project_evidence_pending_text
|
|
3776
|
+
and S_PROJECT_EVIDENCE not in s.answered),
|
|
3777
|
+
build=_build_project_evidence, submit=_submit_project_evidence,
|
|
3778
|
+
owns=("project_evidence_path", "project_evidence_pending_text",
|
|
3779
|
+
"analysis_target", "analysis_target_pending_text")),
|
|
3780
|
+
Step(S_ANALYSIS_TARGET_PICK,
|
|
3781
|
+
applies=lambda s: (s.task_type == "feature-analysis"
|
|
3782
|
+
and bool(s.project_evidence_path)
|
|
3783
|
+
and not s.analysis_target
|
|
3784
|
+
and not s.analysis_target_pending_text
|
|
3785
|
+
and S_ANALYSIS_TARGET_PICK not in s.answered),
|
|
3786
|
+
build=_build_analysis_target_pick, submit=_submit_analysis_target_pick,
|
|
3787
|
+
owns=("analysis_target", "analysis_target_pending_text")),
|
|
3788
|
+
Step(S_ANALYSIS_TARGET,
|
|
3789
|
+
applies=lambda s: (s.task_type == "feature-analysis"
|
|
3790
|
+
and not s.analysis_target
|
|
3791
|
+
and s.analysis_target_pending_text
|
|
3792
|
+
and S_ANALYSIS_TARGET not in s.answered),
|
|
3793
|
+
build=_build_analysis_target, submit=_submit_analysis_target,
|
|
3794
|
+
owns=("analysis_target", "analysis_target_pending_text")),
|
|
3307
3795
|
Step(S_BASE_REF_PICK,
|
|
3308
3796
|
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
3309
3797
|
and _base_ref_required(s)
|
|
@@ -3395,18 +3883,6 @@ STEPS: list[Step] = [
|
|
|
3395
3883
|
applies=lambda s: (s.critic_pending_text and S_CRITIC_TEXT not in s.answered),
|
|
3396
3884
|
build=_build_critic_text, submit=_submit_critic_text,
|
|
3397
3885
|
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
3886
|
Step(S_DEFAULTS_OR_CUSTOM,
|
|
3411
3887
|
applies=lambda s: (_identity_ready(s)
|
|
3412
3888
|
and s.use_defaults is None),
|
|
@@ -3609,6 +4085,14 @@ def _reset_from(state: WizardState, target_step: str) -> None:
|
|
|
3609
4085
|
for fname in step.owns:
|
|
3610
4086
|
_reset_field(state, fname)
|
|
3611
4087
|
state.answered = [a for a in state.answered if a not in cleared_ids]
|
|
4088
|
+
direct_input_pending = {
|
|
4089
|
+
S_FEATURE_EVIDENCE: "feature_evidence_pending_text",
|
|
4090
|
+
S_PROJECT_EVIDENCE: "project_evidence_pending_text",
|
|
4091
|
+
S_ANALYSIS_TARGET: "analysis_target_pending_text",
|
|
4092
|
+
}
|
|
4093
|
+
pending_field = direct_input_pending.get(target_step)
|
|
4094
|
+
if pending_field is not None:
|
|
4095
|
+
setattr(state, pending_field, True)
|
|
3612
4096
|
|
|
3613
4097
|
|
|
3614
4098
|
_FIELD_DEFAULTS: dict[str, Any] = {
|
|
@@ -3619,6 +4103,9 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
3619
4103
|
"profile_workers": [], "profile_optional_workers": [],
|
|
3620
4104
|
"keep_existing_brief": None,
|
|
3621
4105
|
"brief_path": "", "brief_path_pending_text": False,
|
|
4106
|
+
"project_evidence_path": "", "project_evidence_pending_text": False,
|
|
4107
|
+
"feature_evidence_path": "", "feature_evidence_pending_text": False,
|
|
4108
|
+
"analysis_target": "", "analysis_target_pending_text": False,
|
|
3622
4109
|
"reuse_worktree": None, "base_ref": "",
|
|
3623
4110
|
"base_ref_pending_text": False, "approved_plan_path": "",
|
|
3624
4111
|
"approved_plan_pending_text": False, "approve_plan_candidate": "",
|
|
@@ -3876,6 +4363,11 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
3876
4363
|
if state.task_type == "release-handoff"
|
|
3877
4364
|
else ""
|
|
3878
4365
|
)
|
|
4366
|
+
evidence_paths = [
|
|
4367
|
+
path
|
|
4368
|
+
for path in (state.feature_evidence_path, state.project_evidence_path)
|
|
4369
|
+
if path
|
|
4370
|
+
]
|
|
3879
4371
|
return {
|
|
3880
4372
|
"project-root": state.project_root,
|
|
3881
4373
|
"project-id": state.project_id,
|
|
@@ -3883,6 +4375,8 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
3883
4375
|
"task-id": state.task_id,
|
|
3884
4376
|
"task-type": state.task_type,
|
|
3885
4377
|
"task-brief": state.brief_path,
|
|
4378
|
+
"analysis-target": state.analysis_target,
|
|
4379
|
+
"evidence-inputs": ",".join(evidence_paths),
|
|
3886
4380
|
"executor": state.executor,
|
|
3887
4381
|
"critic": state.critic,
|
|
3888
4382
|
"approved-plan": state.approved_plan_path,
|
|
@@ -3911,6 +4405,25 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
3911
4405
|
lines.append(f" task-type : {state.task_type}")
|
|
3912
4406
|
lines.append(f" task-key : {state.task_group}/{state.task_id}")
|
|
3913
4407
|
lines.append(f" brief : {state.brief_path or '(none)'}")
|
|
4408
|
+
if state.analysis_target:
|
|
4409
|
+
lines.append(f" analysis-target: {state.analysis_target}")
|
|
4410
|
+
if state.feature_evidence_path or state.project_evidence_path:
|
|
4411
|
+
evidence_status = {
|
|
4412
|
+
str(item.report_path): item.review_status
|
|
4413
|
+
for item in _resolve_analysis_evidence(state)
|
|
4414
|
+
}
|
|
4415
|
+
if state.feature_evidence_path:
|
|
4416
|
+
lines.append(
|
|
4417
|
+
" feature-evidence: "
|
|
4418
|
+
f"{state.feature_evidence_path} "
|
|
4419
|
+
f"({evidence_status.get(state.feature_evidence_path, 'unknown')})"
|
|
4420
|
+
)
|
|
4421
|
+
if state.project_evidence_path:
|
|
4422
|
+
lines.append(
|
|
4423
|
+
" project-evidence: "
|
|
4424
|
+
f"{state.project_evidence_path} "
|
|
4425
|
+
f"({evidence_status.get(state.project_evidence_path, 'unknown')})"
|
|
4426
|
+
)
|
|
3914
4427
|
if state.task_type == "final-verification":
|
|
3915
4428
|
lines.append(" base-ref : (selected stage worktree)")
|
|
3916
4429
|
elif state.task_type == "implementation" and state.reuse_worktree:
|