easy-coding-harness 0.10.0-beta.0 → 0.10.0-beta.1
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/CHANGELOG.md +15 -0
- package/README.md +7 -3
- package/dist/cli.js +102 -13
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +6 -0
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +9 -2
- package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +30 -0
- package/templates/common/skills/ec-config/SKILL.md +54 -0
- package/templates/common/skills/ec-implementing/SKILL.md +11 -0
- package/templates/common/skills/ec-memory/SKILL.md +4 -0
- package/templates/common/skills/ec-reviewing/SKILL.md +6 -0
- package/templates/common/skills/ec-task-management/SKILL.md +6 -31
- package/templates/common/skills/ec-verification/SKILL.md +45 -1
- package/templates/common/skills/ec-workflow/SKILL.md +7 -1
- package/templates/main-constraint/AGENTS.md.tpl +10 -3
- package/templates/main-constraint/CLAUDE.md.tpl +10 -3
- package/templates/runtime/tools/easy_coding_java_coverage.py +317 -0
- package/templates/shared-hooks/easy_coding_state.py +602 -13
- package/templates/shared-hooks/inject-subagent-context.py +5 -0
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
7
|
import secrets
|
|
8
|
+
import shlex
|
|
8
9
|
import subprocess
|
|
9
10
|
import time
|
|
10
11
|
import uuid
|
|
@@ -24,7 +25,8 @@ from easy_dev_spec import (
|
|
|
24
25
|
TERMINAL_STATUSES = {"COMPLETE", "CLOSED"}
|
|
25
26
|
HELP_SUFFIX = (
|
|
26
27
|
"Use `ec-workflow` to start or resume a task, "
|
|
27
|
-
"`ec-brainstorming` to brainstorm,
|
|
28
|
+
"`ec-brainstorming` to brainstorm, `ec-task-management` to manage tasks, "
|
|
29
|
+
"or `ec-config` to inspect or change modes"
|
|
28
30
|
)
|
|
29
31
|
READY_LINE = f"Ready · {HELP_SUFFIX}"
|
|
30
32
|
WAITING_INIT_LINE = "Waiting init · Use `ec-init` to initialize"
|
|
@@ -78,6 +80,8 @@ STRICT_WORKFLOW_RISK_PATTERN = re.compile(
|
|
|
78
80
|
)
|
|
79
81
|
DEFAULT_APPROVAL_MODE = "guard"
|
|
80
82
|
DEFAULT_WORKFLOW_MODE = "adaptive"
|
|
83
|
+
DEFAULT_TDD_ENABLED = False
|
|
84
|
+
DEFAULT_TDD_COVERAGE_THRESHOLD = 90
|
|
81
85
|
CRITICAL_CONFIRM_TRANSITIONS = {
|
|
82
86
|
("ANALYSIS", "IMPLEMENT"),
|
|
83
87
|
("VERIFICATION", "MEMORY"),
|
|
@@ -310,16 +314,45 @@ def read_memory_config(root: Path) -> dict[str, int]:
|
|
|
310
314
|
return config
|
|
311
315
|
|
|
312
316
|
|
|
313
|
-
def
|
|
317
|
+
def parse_tdd_threshold(value: object, source: str) -> int:
|
|
318
|
+
if isinstance(value, bool):
|
|
319
|
+
raise StateError(f"Invalid {source}: expected an integer from 1 to 100.")
|
|
320
|
+
try:
|
|
321
|
+
threshold = int(str(value))
|
|
322
|
+
except (TypeError, ValueError) as error:
|
|
323
|
+
raise StateError(f"Invalid {source}: expected an integer from 1 to 100.") from error
|
|
324
|
+
if threshold < 1 or threshold > 100:
|
|
325
|
+
raise StateError(f"Invalid {source}: expected an integer from 1 to 100.")
|
|
326
|
+
return threshold
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def parse_yaml_bool(value: str | None, source: str) -> bool:
|
|
330
|
+
if value is None:
|
|
331
|
+
return DEFAULT_TDD_ENABLED
|
|
332
|
+
normalized = value.lower()
|
|
333
|
+
if normalized in {"true", "yes", "on"}:
|
|
334
|
+
return True
|
|
335
|
+
if normalized in {"false", "no", "off"}:
|
|
336
|
+
return False
|
|
337
|
+
raise StateError(f"Invalid {source}: expected true or false.")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def read_project_behavior(root: Path) -> tuple[str, str, bool, int]:
|
|
314
341
|
path = root / ".easy-coding" / "config.yaml"
|
|
315
342
|
try:
|
|
316
343
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
317
344
|
except OSError:
|
|
318
|
-
return
|
|
345
|
+
return (
|
|
346
|
+
DEFAULT_APPROVAL_MODE,
|
|
347
|
+
DEFAULT_WORKFLOW_MODE,
|
|
348
|
+
DEFAULT_TDD_ENABLED,
|
|
349
|
+
DEFAULT_TDD_COVERAGE_THRESHOLD,
|
|
350
|
+
)
|
|
319
351
|
|
|
320
352
|
in_behavior = False
|
|
321
353
|
behavior_indent = 0
|
|
322
354
|
behavior: dict[str, str] = {}
|
|
355
|
+
schema_version = 0
|
|
323
356
|
for raw_line in lines:
|
|
324
357
|
without_comment = raw_line.split("#", 1)[0].rstrip()
|
|
325
358
|
stripped = without_comment.strip()
|
|
@@ -332,6 +365,12 @@ def read_project_behavior(root: Path) -> tuple[str, str]:
|
|
|
332
365
|
continue
|
|
333
366
|
if in_behavior and indent <= behavior_indent:
|
|
334
367
|
in_behavior = False
|
|
368
|
+
if not in_behavior and indent == 0 and stripped.startswith("version:"):
|
|
369
|
+
try:
|
|
370
|
+
schema_version = int(stripped.split(":", 1)[1].strip().strip("'\""))
|
|
371
|
+
except ValueError:
|
|
372
|
+
schema_version = 0
|
|
373
|
+
continue
|
|
335
374
|
if not in_behavior or ":" not in stripped:
|
|
336
375
|
continue
|
|
337
376
|
key, value = stripped.split(":", 1)
|
|
@@ -359,16 +398,27 @@ def read_project_behavior(root: Path) -> tuple[str, str]:
|
|
|
359
398
|
"Invalid behavior.workflow_mode in .easy-coding/config.yaml: "
|
|
360
399
|
"expected adaptive, fast, standard, or strict."
|
|
361
400
|
)
|
|
362
|
-
|
|
401
|
+
if schema_version >= 4:
|
|
402
|
+
tdd_enabled = parse_yaml_bool(behavior.get("tdd_enabled"), "behavior.tdd_enabled")
|
|
403
|
+
tdd_threshold = parse_tdd_threshold(
|
|
404
|
+
behavior.get("tdd_coverage_threshold", DEFAULT_TDD_COVERAGE_THRESHOLD),
|
|
405
|
+
"behavior.tdd_coverage_threshold",
|
|
406
|
+
)
|
|
407
|
+
else:
|
|
408
|
+
tdd_enabled = DEFAULT_TDD_ENABLED
|
|
409
|
+
tdd_threshold = DEFAULT_TDD_COVERAGE_THRESHOLD
|
|
410
|
+
return approval_mode, workflow_mode, tdd_enabled, tdd_threshold
|
|
363
411
|
|
|
364
412
|
|
|
365
413
|
def resolve_behavior(
|
|
366
414
|
root: Path, session: dict
|
|
367
|
-
) -> tuple[str, str | None, str, str, str | None, str]:
|
|
368
|
-
project_approval, project_workflow = read_project_behavior(root)
|
|
415
|
+
) -> tuple[str, str | None, str, str, str | None, str, bool, bool | None, bool, int, int | None, int]:
|
|
416
|
+
project_approval, project_workflow, project_tdd, project_threshold = read_project_behavior(root)
|
|
369
417
|
legacy = session.get("confirm_mode")
|
|
370
418
|
session_approval = session.get("approval_mode")
|
|
371
419
|
session_workflow = session.get("workflow_mode")
|
|
420
|
+
session_tdd = session.get("tdd_enabled")
|
|
421
|
+
session_threshold = session.get("tdd_coverage_threshold")
|
|
372
422
|
if session_approval is None:
|
|
373
423
|
if legacy == "lite":
|
|
374
424
|
session_approval = "guard"
|
|
@@ -387,6 +437,12 @@ def resolve_behavior(
|
|
|
387
437
|
raise StateError(
|
|
388
438
|
"Invalid session workflow_mode: expected adaptive, fast, standard, or strict."
|
|
389
439
|
)
|
|
440
|
+
if session_tdd is not None and not isinstance(session_tdd, bool):
|
|
441
|
+
raise StateError("Invalid session tdd_enabled: expected true or false.")
|
|
442
|
+
if session_threshold is not None:
|
|
443
|
+
session_threshold = parse_tdd_threshold(
|
|
444
|
+
session_threshold, "session tdd_coverage_threshold"
|
|
445
|
+
)
|
|
390
446
|
return (
|
|
391
447
|
project_approval,
|
|
392
448
|
str(session_approval) if session_approval else None,
|
|
@@ -394,6 +450,12 @@ def resolve_behavior(
|
|
|
394
450
|
project_workflow,
|
|
395
451
|
str(session_workflow) if session_workflow else None,
|
|
396
452
|
str(session_workflow or project_workflow),
|
|
453
|
+
project_tdd,
|
|
454
|
+
session_tdd,
|
|
455
|
+
session_tdd if session_tdd is not None else project_tdd,
|
|
456
|
+
project_threshold,
|
|
457
|
+
session_threshold,
|
|
458
|
+
session_threshold if session_threshold is not None else project_threshold,
|
|
397
459
|
)
|
|
398
460
|
|
|
399
461
|
|
|
@@ -1624,6 +1686,77 @@ def task_repository_roots(root: Path, task: dict | None, plan: dict) -> list[Pat
|
|
|
1624
1686
|
]
|
|
1625
1687
|
|
|
1626
1688
|
|
|
1689
|
+
def tdd_repositories(root: Path, task: dict, plan: dict) -> dict[str, Path]:
|
|
1690
|
+
if isinstance(task.get("spec_source"), dict):
|
|
1691
|
+
repo_paths = task.get("repo_paths")
|
|
1692
|
+
if not isinstance(repo_paths, dict):
|
|
1693
|
+
raise StateError("TDD Canonical task is missing repository bindings.")
|
|
1694
|
+
repositories: dict[str, Path] = {}
|
|
1695
|
+
for unit in plan.get("units", []):
|
|
1696
|
+
if not isinstance(unit, dict) or not is_non_empty_string(unit.get("repo_id")):
|
|
1697
|
+
raise StateError("TDD Canonical unit is missing repo_id.")
|
|
1698
|
+
repo_id = str(unit["repo_id"])
|
|
1699
|
+
raw_path = repo_paths.get(repo_id)
|
|
1700
|
+
if not is_non_empty_string(raw_path):
|
|
1701
|
+
raise StateError(f"TDD repository path is missing: {repo_id}")
|
|
1702
|
+
candidate = Path(str(raw_path))
|
|
1703
|
+
resolved = (candidate if candidate.is_absolute() else root / candidate).resolve()
|
|
1704
|
+
repository = git_repository_root(resolved)
|
|
1705
|
+
if repository is None or repository.resolve() != resolved:
|
|
1706
|
+
raise StateError(f"TDD repository binding is not a Git root: {repo_id}")
|
|
1707
|
+
repositories[repo_id] = repository
|
|
1708
|
+
return repositories
|
|
1709
|
+
|
|
1710
|
+
repositories = task_repository_roots(root, task, plan)
|
|
1711
|
+
if len(repositories) != 1:
|
|
1712
|
+
raise StateError(
|
|
1713
|
+
"Non-Canonical TDD requires exactly one Git repository; use a Canonical Spec for multi-repository work."
|
|
1714
|
+
)
|
|
1715
|
+
return {"project": repositories[0]}
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
def git_head_sha(repository: Path) -> str:
|
|
1719
|
+
result = run_git(repository, "rev-parse", "--verify", "HEAD")
|
|
1720
|
+
sha = result.stdout.decode("ascii", errors="ignore").strip() if result else ""
|
|
1721
|
+
if (
|
|
1722
|
+
result is None
|
|
1723
|
+
or result.returncode != 0
|
|
1724
|
+
or re.fullmatch(r"[0-9a-f]{40}|[0-9a-f]{64}", sha) is None
|
|
1725
|
+
):
|
|
1726
|
+
raise StateError(f"Cannot freeze TDD Git baseline for {repository.name}.")
|
|
1727
|
+
return sha
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def tdd_baseline_marker_reasons(
|
|
1731
|
+
dev_spec_content: str, strategy_content: str, baselines: dict[str, str]
|
|
1732
|
+
) -> list[str]:
|
|
1733
|
+
reasons: list[str] = []
|
|
1734
|
+
canonical = set(baselines) != {"project"}
|
|
1735
|
+
for repo_id, baseline in sorted(baselines.items()):
|
|
1736
|
+
for artifact_name, content in (
|
|
1737
|
+
("dev-spec.md", dev_spec_content),
|
|
1738
|
+
("test-strategy.md", strategy_content),
|
|
1739
|
+
):
|
|
1740
|
+
if baseline not in content:
|
|
1741
|
+
reasons.append(
|
|
1742
|
+
f"{artifact_name} must record the immutable TDD baseline SHA for {repo_id}: {baseline}"
|
|
1743
|
+
)
|
|
1744
|
+
if canonical and repo_id not in content:
|
|
1745
|
+
reasons.append(
|
|
1746
|
+
f"{artifact_name} must map the TDD baseline to repository {repo_id}"
|
|
1747
|
+
)
|
|
1748
|
+
return reasons
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
def contains_tdd_threshold(content: str, threshold: int) -> bool:
|
|
1752
|
+
return re.search(
|
|
1753
|
+
rf"(?<!\d){threshold}\s*%|--threshold(?:\s+|=){threshold}(?!\d)|"
|
|
1754
|
+
rf"tdd_coverage_threshold\s*[:=]\s*{threshold}(?!\d)",
|
|
1755
|
+
content,
|
|
1756
|
+
re.IGNORECASE,
|
|
1757
|
+
) is not None
|
|
1758
|
+
|
|
1759
|
+
|
|
1627
1760
|
def repository_scope_pathspecs(repository: Path, scopes: list[Path]) -> list[str]:
|
|
1628
1761
|
return [
|
|
1629
1762
|
f":(literal){scope.relative_to(repository).as_posix()}"
|
|
@@ -1837,6 +1970,18 @@ def implementation_fingerprint(root: Path, task_id: str) -> str:
|
|
|
1837
1970
|
digest.update(b"workflow-mode\0")
|
|
1838
1971
|
digest.update(workflow_mode.encode("utf-8"))
|
|
1839
1972
|
digest.update(b"\0")
|
|
1973
|
+
if task and task.get("tdd_enabled") is True:
|
|
1974
|
+
digest.update(b"tdd\0enabled\0")
|
|
1975
|
+
digest.update(str(task.get("tdd_coverage_threshold") or "").encode("utf-8"))
|
|
1976
|
+
digest.update(b"\0")
|
|
1977
|
+
digest.update(
|
|
1978
|
+
json.dumps(
|
|
1979
|
+
task.get("tdd_baselines") or {},
|
|
1980
|
+
sort_keys=True,
|
|
1981
|
+
separators=(",", ":"),
|
|
1982
|
+
).encode("utf-8")
|
|
1983
|
+
)
|
|
1984
|
+
digest.update(b"\0")
|
|
1840
1985
|
digest.update(b"execution-plan\0")
|
|
1841
1986
|
digest.update(
|
|
1842
1987
|
json.dumps(
|
|
@@ -1901,23 +2046,93 @@ def implementation_fingerprint(root: Path, task_id: str) -> str:
|
|
|
1901
2046
|
return digest.hexdigest()
|
|
1902
2047
|
|
|
1903
2048
|
|
|
1904
|
-
def
|
|
2049
|
+
def config_without_frozen_tdd_settings(payload: bytes) -> bytes:
|
|
2050
|
+
"""任务冻结 TDD 契约后,从证据指纹中排除仅影响未来任务的实时 TDD 配置。"""
|
|
2051
|
+
try:
|
|
2052
|
+
lines = payload.decode("utf-8").splitlines(keepends=True)
|
|
2053
|
+
except UnicodeDecodeError:
|
|
2054
|
+
return payload
|
|
2055
|
+
filtered: list[str] = []
|
|
2056
|
+
in_behavior = False
|
|
2057
|
+
behavior_indent = 0
|
|
2058
|
+
behavior_key_indent: int | None = None
|
|
2059
|
+
for line in lines:
|
|
2060
|
+
clean = line.split("#", 1)[0].rstrip()
|
|
2061
|
+
stripped = clean.strip()
|
|
2062
|
+
indent = len(clean) - len(clean.lstrip(" "))
|
|
2063
|
+
if stripped == "behavior:":
|
|
2064
|
+
in_behavior = True
|
|
2065
|
+
behavior_indent = indent
|
|
2066
|
+
behavior_key_indent = None
|
|
2067
|
+
filtered.append(line)
|
|
2068
|
+
continue
|
|
2069
|
+
if in_behavior and stripped and indent <= behavior_indent:
|
|
2070
|
+
in_behavior = False
|
|
2071
|
+
if in_behavior and stripped:
|
|
2072
|
+
if behavior_key_indent is None:
|
|
2073
|
+
behavior_key_indent = indent
|
|
2074
|
+
key = stripped.split(":", 1)[0]
|
|
2075
|
+
if (
|
|
2076
|
+
indent == behavior_key_indent
|
|
2077
|
+
and key in {"tdd_enabled", "tdd_coverage_threshold"}
|
|
2078
|
+
):
|
|
2079
|
+
continue
|
|
2080
|
+
filtered.append(line)
|
|
2081
|
+
return "".join(filtered).encode("utf-8")
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
def behavior_config_fingerprint(root: Path, task: dict | None = None) -> str:
|
|
1905
2085
|
path = root / ".easy-coding" / "config.yaml"
|
|
1906
2086
|
digest = hashlib.sha256()
|
|
1907
2087
|
try:
|
|
1908
|
-
|
|
2088
|
+
payload = path.read_bytes()
|
|
2089
|
+
if task and isinstance(task.get("tdd_enabled"), bool):
|
|
2090
|
+
payload = config_without_frozen_tdd_settings(payload)
|
|
2091
|
+
digest.update(payload)
|
|
1909
2092
|
except OSError:
|
|
1910
2093
|
digest.update(b"<missing-config>")
|
|
1911
2094
|
return digest.hexdigest()
|
|
1912
2095
|
|
|
1913
2096
|
|
|
1914
2097
|
def evidence_fingerprints(root: Path, task_id: str) -> dict[str, str]:
|
|
2098
|
+
task = load_task(root, task_id)
|
|
1915
2099
|
return {
|
|
1916
2100
|
"implementation_fingerprint": implementation_fingerprint(root, task_id),
|
|
1917
|
-
"config_fingerprint": behavior_config_fingerprint(root),
|
|
2101
|
+
"config_fingerprint": behavior_config_fingerprint(root, task),
|
|
1918
2102
|
}
|
|
1919
2103
|
|
|
1920
2104
|
|
|
2105
|
+
def command_option_value(command: str, option: str) -> str | None:
|
|
2106
|
+
try:
|
|
2107
|
+
tokens = shlex.split(command)
|
|
2108
|
+
except ValueError:
|
|
2109
|
+
return None
|
|
2110
|
+
for index, token in enumerate(tokens):
|
|
2111
|
+
if token == option and index + 1 < len(tokens):
|
|
2112
|
+
return tokens[index + 1]
|
|
2113
|
+
prefix = f"{option}="
|
|
2114
|
+
if token.startswith(prefix):
|
|
2115
|
+
return token[len(prefix) :]
|
|
2116
|
+
return None
|
|
2117
|
+
|
|
2118
|
+
|
|
2119
|
+
def coverage_command_matches_frozen_contract(
|
|
2120
|
+
command: object, baseline: str, threshold: int
|
|
2121
|
+
) -> bool:
|
|
2122
|
+
if not is_non_empty_string(command):
|
|
2123
|
+
return False
|
|
2124
|
+
try:
|
|
2125
|
+
tokens = shlex.split(str(command))
|
|
2126
|
+
except ValueError:
|
|
2127
|
+
return False
|
|
2128
|
+
return (
|
|
2129
|
+
any(Path(token).name == "easy_coding_java_coverage.py" for token in tokens)
|
|
2130
|
+
and "check" in tokens
|
|
2131
|
+
and command_option_value(str(command), "--base") == baseline
|
|
2132
|
+
and command_option_value(str(command), "--threshold") == str(threshold)
|
|
2133
|
+
)
|
|
2134
|
+
|
|
2135
|
+
|
|
1921
2136
|
def validate_spec_implementation_results(root: Path, task_id: str, task: dict) -> None:
|
|
1922
2137
|
if not isinstance(task.get("spec_source"), dict):
|
|
1923
2138
|
return
|
|
@@ -2074,6 +2289,25 @@ def validate_review_readiness(root: Path, task_id: str, task: dict) -> None:
|
|
|
2074
2289
|
raise StateError(
|
|
2075
2290
|
"REVIEW cannot advance to VERIFICATION while a current review dimension is not passed or has error findings."
|
|
2076
2291
|
)
|
|
2292
|
+
if task.get("tdd_enabled") is True:
|
|
2293
|
+
if is_spec_task:
|
|
2294
|
+
missing_tdd_reviews = sorted(
|
|
2295
|
+
source_task_id
|
|
2296
|
+
for source_task_id, dimensions in reviewed_dimensions.items()
|
|
2297
|
+
if "tdd" not in {dimension.lower() for dimension in dimensions}
|
|
2298
|
+
)
|
|
2299
|
+
if missing_tdd_reviews:
|
|
2300
|
+
raise StateError(
|
|
2301
|
+
"TDD tasks require a passed TDD review dimension for every selected source task: "
|
|
2302
|
+
+ ", ".join(missing_tdd_reviews)
|
|
2303
|
+
)
|
|
2304
|
+
elif not any(
|
|
2305
|
+
str(record.get("dimension") or "").lower() == "tdd"
|
|
2306
|
+
for record in latest_by_dimension.values()
|
|
2307
|
+
):
|
|
2308
|
+
raise StateError(
|
|
2309
|
+
"TDD tasks require a passed TDD review dimension for test quality, boundaries, and mocking."
|
|
2310
|
+
)
|
|
2077
2311
|
if task.get("workflow_mode") == "strict":
|
|
2078
2312
|
if is_spec_task:
|
|
2079
2313
|
missing_strict_dimensions = sorted(
|
|
@@ -2111,6 +2345,8 @@ def validate_verification_readiness(root: Path, task_id: str, task: dict) -> Non
|
|
|
2111
2345
|
and is_non_empty_string(record.get("check"))
|
|
2112
2346
|
):
|
|
2113
2347
|
check = str(record["check"])
|
|
2348
|
+
if task.get("tdd_enabled") is True and record.get("check_type") == "coverage":
|
|
2349
|
+
check = f"{check}\0{record.get('coverage_scope') or ''}"
|
|
2114
2350
|
if is_spec_task:
|
|
2115
2351
|
check = f"{check}\0{record.get('source_task_id') or ''}"
|
|
2116
2352
|
previous = latest_by_check.get(check)
|
|
@@ -2129,7 +2365,7 @@ def validate_verification_readiness(root: Path, task_id: str, task: dict) -> Non
|
|
|
2129
2365
|
for record in latest_by_check.values():
|
|
2130
2366
|
check_type = str(record.get("check_type") or "")
|
|
2131
2367
|
if (
|
|
2132
|
-
check_type not in STRICT_VERIFICATION_CHECK_TYPES
|
|
2368
|
+
check_type not in STRICT_VERIFICATION_CHECK_TYPES | {"coverage"}
|
|
2133
2369
|
or not is_non_empty_string(record.get("timestamp"))
|
|
2134
2370
|
or (
|
|
2135
2371
|
record.get("applicable") is not False
|
|
@@ -2175,6 +2411,139 @@ def validate_verification_readiness(root: Path, task_id: str, task: dict) -> Non
|
|
|
2175
2411
|
raise StateError(
|
|
2176
2412
|
"VERIFICATION cannot advance to MEMORY while current verification evidence contains failures."
|
|
2177
2413
|
)
|
|
2414
|
+
if task.get("tdd_enabled") is not True and any(
|
|
2415
|
+
record.get("check_type") == "coverage" for record in latest_by_check.values()
|
|
2416
|
+
):
|
|
2417
|
+
raise StateError(
|
|
2418
|
+
"Coverage verification evidence is not allowed when the frozen TDD mode is off."
|
|
2419
|
+
)
|
|
2420
|
+
if task.get("tdd_enabled") is True:
|
|
2421
|
+
coverage_records = [
|
|
2422
|
+
record
|
|
2423
|
+
for record in latest_by_check.values()
|
|
2424
|
+
if record.get("check_type") == "coverage"
|
|
2425
|
+
]
|
|
2426
|
+
if not coverage_records:
|
|
2427
|
+
raise StateError(
|
|
2428
|
+
"TDD verification requires changed-production-line JaCoCo coverage evidence."
|
|
2429
|
+
)
|
|
2430
|
+
if is_spec_task:
|
|
2431
|
+
covered_source_tasks = {
|
|
2432
|
+
str(record.get("source_task_id") or "") for record in coverage_records
|
|
2433
|
+
}
|
|
2434
|
+
missing_coverage_tasks = sorted(
|
|
2435
|
+
set(task_repositories) - covered_source_tasks
|
|
2436
|
+
)
|
|
2437
|
+
if missing_coverage_tasks:
|
|
2438
|
+
raise StateError(
|
|
2439
|
+
"TDD Canonical verification requires separate coverage evidence for every selected source task: "
|
|
2440
|
+
+ ", ".join(missing_coverage_tasks)
|
|
2441
|
+
)
|
|
2442
|
+
coverage_scopes_by_owner: dict[str, set[str]] = {}
|
|
2443
|
+
for record in coverage_records:
|
|
2444
|
+
scope = str(record.get("coverage_scope") or "")
|
|
2445
|
+
if scope not in {"local", "gitlab"}:
|
|
2446
|
+
raise StateError(
|
|
2447
|
+
"TDD coverage evidence must identify coverage_scope as local or gitlab."
|
|
2448
|
+
)
|
|
2449
|
+
owner = (
|
|
2450
|
+
str(record.get("source_task_id") or "")
|
|
2451
|
+
if is_spec_task
|
|
2452
|
+
else "project"
|
|
2453
|
+
)
|
|
2454
|
+
coverage_scopes_by_owner.setdefault(owner, set()).add(scope)
|
|
2455
|
+
expected_threshold = task.get("tdd_coverage_threshold")
|
|
2456
|
+
expected_baselines = task.get("tdd_baselines")
|
|
2457
|
+
if (
|
|
2458
|
+
type(expected_threshold) is not int
|
|
2459
|
+
or expected_threshold < 1
|
|
2460
|
+
or expected_threshold > 100
|
|
2461
|
+
):
|
|
2462
|
+
raise StateError("TDD task is missing a valid frozen coverage threshold.")
|
|
2463
|
+
if not isinstance(expected_baselines, dict) or not expected_baselines:
|
|
2464
|
+
raise StateError("TDD task is missing frozen Git baselines.")
|
|
2465
|
+
for record in coverage_records:
|
|
2466
|
+
coverage = record.get("coverage")
|
|
2467
|
+
if not isinstance(coverage, dict):
|
|
2468
|
+
raise StateError("TDD coverage evidence must include the coverage result object.")
|
|
2469
|
+
if record.get("coverage_scope") == "gitlab":
|
|
2470
|
+
ci = record.get("ci")
|
|
2471
|
+
if (
|
|
2472
|
+
not isinstance(ci, dict)
|
|
2473
|
+
or ci.get("provider") != "gitlab"
|
|
2474
|
+
or ci.get("status") != "success"
|
|
2475
|
+
or not is_non_empty_string(ci.get("pipeline_url"))
|
|
2476
|
+
or not is_non_empty_string(ci.get("job_name"))
|
|
2477
|
+
):
|
|
2478
|
+
raise StateError(
|
|
2479
|
+
"GitLab coverage evidence requires a successful pipeline URL and job name."
|
|
2480
|
+
)
|
|
2481
|
+
total = coverage.get("total_lines")
|
|
2482
|
+
covered = coverage.get("covered_lines")
|
|
2483
|
+
percentage = coverage.get("percentage")
|
|
2484
|
+
threshold = coverage.get("threshold")
|
|
2485
|
+
baseline_key = str(record.get("repo_id") or "") if is_spec_task else "project"
|
|
2486
|
+
expected_baseline = expected_baselines.get(baseline_key)
|
|
2487
|
+
if (
|
|
2488
|
+
not is_non_empty_string(expected_baseline)
|
|
2489
|
+
or coverage.get("baseline_sha") != expected_baseline
|
|
2490
|
+
or re.fullmatch(
|
|
2491
|
+
r"[0-9a-f]{40}|[0-9a-f]{64}", str(coverage.get("baseline_sha") or "")
|
|
2492
|
+
)
|
|
2493
|
+
is None
|
|
2494
|
+
or not isinstance(total, int)
|
|
2495
|
+
or not isinstance(covered, int)
|
|
2496
|
+
or not isinstance(percentage, (int, float))
|
|
2497
|
+
or threshold != expected_threshold
|
|
2498
|
+
or not isinstance(coverage.get("report_paths"), list)
|
|
2499
|
+
or not coverage.get("report_paths")
|
|
2500
|
+
or not all(
|
|
2501
|
+
is_non_empty_string(path) for path in coverage.get("report_paths", [])
|
|
2502
|
+
)
|
|
2503
|
+
or not re.fullmatch(
|
|
2504
|
+
r"[0-9a-f]{64}", str(coverage.get("report_sha256") or "")
|
|
2505
|
+
)
|
|
2506
|
+
or covered < 0
|
|
2507
|
+
or total < 0
|
|
2508
|
+
or covered > total
|
|
2509
|
+
or percentage < 0
|
|
2510
|
+
or percentage > 100
|
|
2511
|
+
or not coverage_command_matches_frozen_contract(
|
|
2512
|
+
record.get("command"), str(expected_baseline), int(expected_threshold)
|
|
2513
|
+
)
|
|
2514
|
+
):
|
|
2515
|
+
raise StateError(
|
|
2516
|
+
"TDD coverage evidence must preserve the exact gate command, baseline, counts, percentage, frozen threshold, reports, and report fingerprint."
|
|
2517
|
+
)
|
|
2518
|
+
if total == 0:
|
|
2519
|
+
if record.get("applicable") is not False or record.get("passed") is not True:
|
|
2520
|
+
raise StateError(
|
|
2521
|
+
"Coverage with no modified executable production Java lines must be explicit N/A."
|
|
2522
|
+
)
|
|
2523
|
+
elif abs(percentage - round(covered * 100.0 / total, 2)) > 0.01:
|
|
2524
|
+
raise StateError(
|
|
2525
|
+
"TDD coverage evidence percentage does not match covered/total counts."
|
|
2526
|
+
)
|
|
2527
|
+
elif (
|
|
2528
|
+
record.get("applicable") is False
|
|
2529
|
+
or record.get("passed") is not True
|
|
2530
|
+
or percentage < threshold
|
|
2531
|
+
):
|
|
2532
|
+
raise StateError(
|
|
2533
|
+
f"TDD changed-line coverage must meet the frozen {threshold}% threshold."
|
|
2534
|
+
)
|
|
2535
|
+
expected_coverage_owners = set(task_repositories) if is_spec_task else {"project"}
|
|
2536
|
+
missing_scopes = [
|
|
2537
|
+
f"{owner}:{scope}"
|
|
2538
|
+
for owner in sorted(expected_coverage_owners)
|
|
2539
|
+
for scope in ("local", "gitlab")
|
|
2540
|
+
if scope not in coverage_scopes_by_owner.get(owner, set())
|
|
2541
|
+
]
|
|
2542
|
+
if missing_scopes:
|
|
2543
|
+
raise StateError(
|
|
2544
|
+
"TDD verification requires both local and successful GitLab coverage gates: "
|
|
2545
|
+
+ ", ".join(missing_scopes)
|
|
2546
|
+
)
|
|
2178
2547
|
if task.get("workflow_mode") == "strict":
|
|
2179
2548
|
if is_spec_task:
|
|
2180
2549
|
check_types_by_repository: dict[str, set[str]] = {
|
|
@@ -2445,7 +2814,9 @@ def validate_mandatory_dev_spec_sections(content: str) -> tuple[list[str], list[
|
|
|
2445
2814
|
return missing, empty
|
|
2446
2815
|
|
|
2447
2816
|
|
|
2448
|
-
def validate_analysis_readiness(
|
|
2817
|
+
def validate_analysis_readiness(
|
|
2818
|
+
root: Path, task_id: str, session: dict | None = None
|
|
2819
|
+
) -> None:
|
|
2449
2820
|
task_dir = task_json_path(root, task_id).parent
|
|
2450
2821
|
task = load_task(root, task_id)
|
|
2451
2822
|
task_type = str(task.get("type") or "").strip().lower() if task else ""
|
|
@@ -2454,6 +2825,9 @@ def validate_analysis_readiness(root: Path, task_id: str) -> None:
|
|
|
2454
2825
|
skeleton = root / ".easy-coding" / "templates" / "dev-spec-skeleton.md"
|
|
2455
2826
|
test_strategy = task_dir / "test-strategy.md"
|
|
2456
2827
|
reasons: list[str] = []
|
|
2828
|
+
behavior = resolve_behavior(root, session or default_session())
|
|
2829
|
+
tdd_enabled = behavior[8]
|
|
2830
|
+
tdd_threshold = behavior[11]
|
|
2457
2831
|
|
|
2458
2832
|
dev_spec_content = ""
|
|
2459
2833
|
if not dev_spec.exists():
|
|
@@ -2500,6 +2874,81 @@ def validate_analysis_readiness(root: Path, task_id: str) -> None:
|
|
|
2500
2874
|
plan_is_valid = has_valid_execution_plan(root, task_id)
|
|
2501
2875
|
if not plan_is_valid:
|
|
2502
2876
|
reasons.append("execution.jsonl has no valid plan record")
|
|
2877
|
+
if tdd_enabled and not is_read_only_task:
|
|
2878
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
2879
|
+
if re.search(
|
|
2880
|
+
r"^###\s+TDD Mode\s*$", dev_spec_content, re.MULTILINE | re.IGNORECASE
|
|
2881
|
+
) is None:
|
|
2882
|
+
reasons.append("dev-spec.md is missing the required TDD Mode section")
|
|
2883
|
+
planned_files = [
|
|
2884
|
+
str(file_name)
|
|
2885
|
+
for unit in plan.get("units", [])
|
|
2886
|
+
if isinstance(unit, dict)
|
|
2887
|
+
for file_name in unit.get("files", [])
|
|
2888
|
+
]
|
|
2889
|
+
if not any(file_name.endswith(".java") for file_name in planned_files):
|
|
2890
|
+
reasons.append("TDD is enabled but the confirmed implementation scope has no Java source")
|
|
2891
|
+
baselines: dict[str, str] = {}
|
|
2892
|
+
if task:
|
|
2893
|
+
try:
|
|
2894
|
+
repositories = tdd_repositories(root, task, plan)
|
|
2895
|
+
baselines = {
|
|
2896
|
+
repo_id: git_head_sha(repository)
|
|
2897
|
+
for repo_id, repository in repositories.items()
|
|
2898
|
+
}
|
|
2899
|
+
except StateError as error:
|
|
2900
|
+
reasons.append(str(error))
|
|
2901
|
+
try:
|
|
2902
|
+
strategy_content = test_strategy.read_text(encoding="utf-8")
|
|
2903
|
+
except OSError:
|
|
2904
|
+
strategy_content = ""
|
|
2905
|
+
required_tdd_markers = ["TDD", "JaCoCo", "baseline", "GitLab"]
|
|
2906
|
+
missing_tdd_markers = [
|
|
2907
|
+
marker for marker in required_tdd_markers if marker.lower() not in strategy_content.lower()
|
|
2908
|
+
]
|
|
2909
|
+
if missing_tdd_markers:
|
|
2910
|
+
reasons.append(
|
|
2911
|
+
"TDD test strategy is missing: " + ", ".join(missing_tdd_markers)
|
|
2912
|
+
)
|
|
2913
|
+
if not contains_tdd_threshold(strategy_content, tdd_threshold):
|
|
2914
|
+
reasons.append(
|
|
2915
|
+
f"TDD test strategy must state the frozen {tdd_threshold}% coverage threshold"
|
|
2916
|
+
)
|
|
2917
|
+
if not contains_tdd_threshold(dev_spec_content, tdd_threshold):
|
|
2918
|
+
reasons.append(
|
|
2919
|
+
f"TDD dev spec must state the frozen {tdd_threshold}% coverage threshold"
|
|
2920
|
+
)
|
|
2921
|
+
if baselines:
|
|
2922
|
+
reasons.extend(
|
|
2923
|
+
tdd_baseline_marker_reasons(
|
|
2924
|
+
dev_spec_content, strategy_content, baselines
|
|
2925
|
+
)
|
|
2926
|
+
)
|
|
2927
|
+
elif not is_read_only_task:
|
|
2928
|
+
if re.search(
|
|
2929
|
+
r"^###\s+TDD Mode\s*$", dev_spec_content, re.MULTILINE | re.IGNORECASE
|
|
2930
|
+
):
|
|
2931
|
+
reasons.append("dev-spec.md must omit the TDD Mode section when TDD is disabled")
|
|
2932
|
+
try:
|
|
2933
|
+
strategy_content = test_strategy.read_text(encoding="utf-8")
|
|
2934
|
+
except OSError:
|
|
2935
|
+
strategy_content = ""
|
|
2936
|
+
forbidden_tdd_markers = [
|
|
2937
|
+
marker
|
|
2938
|
+
for marker in (
|
|
2939
|
+
"easy_coding_java_coverage.py",
|
|
2940
|
+
"coverage_scope",
|
|
2941
|
+
"task.tdd_baselines",
|
|
2942
|
+
"RED -> GREEN -> REFACTOR",
|
|
2943
|
+
"RED/GREEN/REFACTOR",
|
|
2944
|
+
)
|
|
2945
|
+
if marker.lower() in strategy_content.lower()
|
|
2946
|
+
]
|
|
2947
|
+
if forbidden_tdd_markers:
|
|
2948
|
+
reasons.append(
|
|
2949
|
+
"test-strategy.md contains TDD-only planning while TDD is disabled: "
|
|
2950
|
+
+ ", ".join(forbidden_tdd_markers)
|
|
2951
|
+
)
|
|
2503
2952
|
if task and isinstance(task.get("spec_source"), dict):
|
|
2504
2953
|
try:
|
|
2505
2954
|
inspection, selection = inspect_task_spec(root, task)
|
|
@@ -2785,6 +3234,12 @@ def snapshot_state(
|
|
|
2785
3234
|
project_workflow_mode,
|
|
2786
3235
|
session_workflow_mode,
|
|
2787
3236
|
configured_workflow_mode,
|
|
3237
|
+
project_tdd_enabled,
|
|
3238
|
+
session_tdd_enabled,
|
|
3239
|
+
effective_tdd_enabled,
|
|
3240
|
+
project_tdd_coverage_threshold,
|
|
3241
|
+
session_tdd_coverage_threshold,
|
|
3242
|
+
effective_tdd_coverage_threshold,
|
|
2788
3243
|
) = resolve_behavior(root, resolved_session)
|
|
2789
3244
|
concrete_workflow_mode = None
|
|
2790
3245
|
if task:
|
|
@@ -2792,6 +3247,19 @@ def snapshot_state(
|
|
|
2792
3247
|
proposal = task.get("workflow_mode_proposal")
|
|
2793
3248
|
if concrete_workflow_mode is None and isinstance(proposal, dict):
|
|
2794
3249
|
concrete_workflow_mode = proposal.get("selected_mode")
|
|
3250
|
+
task_tdd_enabled = task.get("tdd_enabled") if task else None
|
|
3251
|
+
task_tdd_coverage_threshold = task.get("tdd_coverage_threshold") if task else None
|
|
3252
|
+
frozen_tdd = bool(
|
|
3253
|
+
task
|
|
3254
|
+
and status not in {"ANALYSIS", "INIT"}
|
|
3255
|
+
and isinstance(task_tdd_enabled, bool)
|
|
3256
|
+
)
|
|
3257
|
+
displayed_tdd_enabled = task_tdd_enabled if frozen_tdd else effective_tdd_enabled
|
|
3258
|
+
displayed_tdd_threshold = (
|
|
3259
|
+
task_tdd_coverage_threshold
|
|
3260
|
+
if frozen_tdd and isinstance(task_tdd_coverage_threshold, int)
|
|
3261
|
+
else effective_tdd_coverage_threshold
|
|
3262
|
+
)
|
|
2795
3263
|
|
|
2796
3264
|
return {
|
|
2797
3265
|
"session_file": display_path(root, session_path),
|
|
@@ -2812,6 +3280,17 @@ def snapshot_state(
|
|
|
2812
3280
|
"session_workflow_mode": session_workflow_mode,
|
|
2813
3281
|
"configured_workflow_mode": configured_workflow_mode,
|
|
2814
3282
|
"concrete_workflow_mode": concrete_workflow_mode,
|
|
3283
|
+
"project_tdd_enabled": project_tdd_enabled,
|
|
3284
|
+
"session_tdd_enabled": session_tdd_enabled,
|
|
3285
|
+
"effective_tdd_enabled": effective_tdd_enabled,
|
|
3286
|
+
"project_tdd_coverage_threshold": project_tdd_coverage_threshold,
|
|
3287
|
+
"session_tdd_coverage_threshold": session_tdd_coverage_threshold,
|
|
3288
|
+
"effective_tdd_coverage_threshold": effective_tdd_coverage_threshold,
|
|
3289
|
+
"task_tdd_enabled": task_tdd_enabled,
|
|
3290
|
+
"task_tdd_coverage_threshold": task_tdd_coverage_threshold,
|
|
3291
|
+
"task_tdd_baselines": task.get("tdd_baselines") if task else None,
|
|
3292
|
+
"displayed_tdd_enabled": displayed_tdd_enabled,
|
|
3293
|
+
"displayed_tdd_coverage_threshold": displayed_tdd_threshold,
|
|
2815
3294
|
"spec_summary": spec_task_summary(task),
|
|
2816
3295
|
# Compatibility output aliases for pre-0.9 clients.
|
|
2817
3296
|
"project_confirm_mode": project_approval_mode,
|
|
@@ -2831,6 +3310,8 @@ def build_status_line(
|
|
|
2831
3310
|
approval = str(state["effective_approval_mode"]).capitalize()
|
|
2832
3311
|
workflow = str(state["concrete_workflow_mode"] or state["configured_workflow_mode"]).capitalize()
|
|
2833
3312
|
status_brand = f"> **Easy Coding** · **Approval: {approval}** · **Workflow: {workflow}**"
|
|
3313
|
+
if state["displayed_tdd_enabled"] is True:
|
|
3314
|
+
status_brand += " · **TDD**"
|
|
2834
3315
|
task_id = state["current_task"]
|
|
2835
3316
|
if task_id:
|
|
2836
3317
|
status = str(state["status"])
|
|
@@ -2874,6 +3355,11 @@ def build_machine_breadcrumbs(
|
|
|
2874
3355
|
]
|
|
2875
3356
|
if state.get("concrete_workflow_mode"):
|
|
2876
3357
|
lines.append(f"[easy-coding:workflow-mode:{state['concrete_workflow_mode']}]")
|
|
3358
|
+
if state.get("displayed_tdd_enabled") is True:
|
|
3359
|
+
lines.append("[easy-coding:tdd:enabled]")
|
|
3360
|
+
lines.append(
|
|
3361
|
+
f"[easy-coding:tdd-coverage-threshold:{state['displayed_tdd_coverage_threshold']}]"
|
|
3362
|
+
)
|
|
2877
3363
|
|
|
2878
3364
|
if task_id:
|
|
2879
3365
|
lines.append(f"[current-task:{task_id}]")
|
|
@@ -3181,6 +3667,43 @@ def clear_session_workflow_mode(
|
|
|
3181
3667
|
return snapshot
|
|
3182
3668
|
|
|
3183
3669
|
|
|
3670
|
+
def set_session_tdd(
|
|
3671
|
+
root: Path,
|
|
3672
|
+
enabled: bool,
|
|
3673
|
+
agent: str,
|
|
3674
|
+
threshold: int | None = None,
|
|
3675
|
+
session_file: str | Path | None = None,
|
|
3676
|
+
) -> dict:
|
|
3677
|
+
session = ensure_session(root, session_file)
|
|
3678
|
+
materialize_legacy_session_behavior(session)
|
|
3679
|
+
session["tdd_enabled"] = enabled
|
|
3680
|
+
if threshold is not None:
|
|
3681
|
+
session["tdd_coverage_threshold"] = parse_tdd_threshold(
|
|
3682
|
+
threshold, "session tdd_coverage_threshold"
|
|
3683
|
+
)
|
|
3684
|
+
session["last_agent"] = agent
|
|
3685
|
+
write_session(root, session, session_file)
|
|
3686
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
3687
|
+
snapshot["action"] = "set-tdd"
|
|
3688
|
+
return snapshot
|
|
3689
|
+
|
|
3690
|
+
|
|
3691
|
+
def clear_session_tdd(
|
|
3692
|
+
root: Path,
|
|
3693
|
+
agent: str,
|
|
3694
|
+
session_file: str | Path | None = None,
|
|
3695
|
+
) -> dict:
|
|
3696
|
+
session = ensure_session(root, session_file)
|
|
3697
|
+
materialize_legacy_session_behavior(session)
|
|
3698
|
+
session.pop("tdd_enabled", None)
|
|
3699
|
+
session.pop("tdd_coverage_threshold", None)
|
|
3700
|
+
session["last_agent"] = agent
|
|
3701
|
+
write_session(root, session, session_file)
|
|
3702
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
3703
|
+
snapshot["action"] = "clear-tdd"
|
|
3704
|
+
return snapshot
|
|
3705
|
+
|
|
3706
|
+
|
|
3184
3707
|
def set_harness_disabled(
|
|
3185
3708
|
root: Path,
|
|
3186
3709
|
disabled: bool,
|
|
@@ -3611,6 +4134,39 @@ def freeze_workflow_mode(
|
|
|
3611
4134
|
task["workflow_mode_confirmed_by"] = agent
|
|
3612
4135
|
|
|
3613
4136
|
|
|
4137
|
+
def freeze_tdd_mode(
|
|
4138
|
+
root: Path, session: dict, task_id: str, task: dict, agent: str
|
|
4139
|
+
) -> None:
|
|
4140
|
+
behavior = resolve_behavior(root, session)
|
|
4141
|
+
task_type = str(task.get("type") or "").strip().lower()
|
|
4142
|
+
task["tdd_enabled"] = behavior[8] if task_type not in NO_CODE_TASK_TYPES else False
|
|
4143
|
+
task["tdd_coverage_threshold"] = behavior[11]
|
|
4144
|
+
if task["tdd_enabled"] is True:
|
|
4145
|
+
plan = latest_execution_plan(root, task_id)
|
|
4146
|
+
if plan is None:
|
|
4147
|
+
raise StateError("Cannot freeze TDD baseline without a valid execution plan.")
|
|
4148
|
+
baselines = {
|
|
4149
|
+
key: git_head_sha(repository)
|
|
4150
|
+
for key, repository in tdd_repositories(root, task, plan).items()
|
|
4151
|
+
}
|
|
4152
|
+
task_dir = task_json_path(root, task_id).parent
|
|
4153
|
+
try:
|
|
4154
|
+
dev_spec_content = (task_dir / "dev-spec.md").read_text(encoding="utf-8")
|
|
4155
|
+
strategy_content = (task_dir / "test-strategy.md").read_text(encoding="utf-8")
|
|
4156
|
+
except OSError as error:
|
|
4157
|
+
raise StateError("Cannot freeze TDD without readable analysis artifacts.") from error
|
|
4158
|
+
marker_reasons = tdd_baseline_marker_reasons(
|
|
4159
|
+
dev_spec_content, strategy_content, baselines
|
|
4160
|
+
)
|
|
4161
|
+
if marker_reasons:
|
|
4162
|
+
raise StateError("; ".join(marker_reasons))
|
|
4163
|
+
task["tdd_baselines"] = baselines
|
|
4164
|
+
else:
|
|
4165
|
+
task.pop("tdd_baselines", None)
|
|
4166
|
+
task["tdd_confirmed_at"] = now_iso()
|
|
4167
|
+
task["tdd_confirmed_by"] = agent
|
|
4168
|
+
|
|
4169
|
+
|
|
3614
4170
|
def raise_workflow_mode(
|
|
3615
4171
|
root: Path,
|
|
3616
4172
|
mode: str,
|
|
@@ -3676,7 +4232,7 @@ def request_transition(
|
|
|
3676
4232
|
"use auto-transition instead."
|
|
3677
4233
|
)
|
|
3678
4234
|
if previous == "ANALYSIS" and stage == "IMPLEMENT":
|
|
3679
|
-
validate_analysis_readiness(root, resolved_task_id)
|
|
4235
|
+
validate_analysis_readiness(root, resolved_task_id, session)
|
|
3680
4236
|
if task.get("workflow_mode_legacy") is not True:
|
|
3681
4237
|
validate_workflow_mode_proposal(
|
|
3682
4238
|
root,
|
|
@@ -3729,9 +4285,10 @@ def apply_transition(
|
|
|
3729
4285
|
if violation:
|
|
3730
4286
|
raise StateError(violation)
|
|
3731
4287
|
if previous == "ANALYSIS" and stage == "IMPLEMENT":
|
|
3732
|
-
validate_analysis_readiness(root, resolved_task_id)
|
|
4288
|
+
validate_analysis_readiness(root, resolved_task_id, session)
|
|
3733
4289
|
if task.get("workflow_mode_legacy") is not True:
|
|
3734
4290
|
freeze_workflow_mode(root, session, resolved_task_id, task, agent)
|
|
4291
|
+
freeze_tdd_mode(root, session, resolved_task_id, task, agent)
|
|
3735
4292
|
if previous == "REVIEW" and stage == "VERIFICATION":
|
|
3736
4293
|
validate_review_readiness(root, resolved_task_id, task)
|
|
3737
4294
|
if previous == "VERIFICATION" and stage == "MEMORY":
|
|
@@ -4156,6 +4713,14 @@ def main() -> int:
|
|
|
4156
4713
|
clear_workflow_mode_parser = subcommands.add_parser("clear-workflow-mode", parents=[common])
|
|
4157
4714
|
clear_workflow_mode_parser.add_argument("--agent", required=True)
|
|
4158
4715
|
|
|
4716
|
+
set_tdd_parser = subcommands.add_parser("set-tdd", parents=[common])
|
|
4717
|
+
set_tdd_parser.add_argument("--enabled", required=True, choices=["true", "false"])
|
|
4718
|
+
set_tdd_parser.add_argument("--threshold", type=int)
|
|
4719
|
+
set_tdd_parser.add_argument("--agent", required=True)
|
|
4720
|
+
|
|
4721
|
+
clear_tdd_parser = subcommands.add_parser("clear-tdd", parents=[common])
|
|
4722
|
+
clear_tdd_parser.add_argument("--agent", required=True)
|
|
4723
|
+
|
|
4159
4724
|
# Compatibility aliases for pre-0.9 callers.
|
|
4160
4725
|
set_confirm_mode_parser = subcommands.add_parser("set-confirm-mode", parents=[common])
|
|
4161
4726
|
set_confirm_mode_parser.add_argument(
|
|
@@ -4441,6 +5006,30 @@ def main() -> int:
|
|
|
4441
5006
|
session_file,
|
|
4442
5007
|
)
|
|
4443
5008
|
)
|
|
5009
|
+
elif command == "set-tdd":
|
|
5010
|
+
emit(
|
|
5011
|
+
attach_status_context(
|
|
5012
|
+
root,
|
|
5013
|
+
set_session_tdd(
|
|
5014
|
+
root,
|
|
5015
|
+
args.enabled == "true",
|
|
5016
|
+
agent,
|
|
5017
|
+
args.threshold,
|
|
5018
|
+
session_file,
|
|
5019
|
+
),
|
|
5020
|
+
agent,
|
|
5021
|
+
session_file,
|
|
5022
|
+
)
|
|
5023
|
+
)
|
|
5024
|
+
elif command == "clear-tdd":
|
|
5025
|
+
emit(
|
|
5026
|
+
attach_status_context(
|
|
5027
|
+
root,
|
|
5028
|
+
clear_session_tdd(root, agent, session_file),
|
|
5029
|
+
agent,
|
|
5030
|
+
session_file,
|
|
5031
|
+
)
|
|
5032
|
+
)
|
|
4444
5033
|
elif command == "propose-workflow-mode":
|
|
4445
5034
|
emit(
|
|
4446
5035
|
attach_status_context(
|