easy-coding-harness 1.0.1 → 1.1.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 +19 -0
- package/README.md +17 -15
- package/dist/cli.js +209 -195
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/agents/ec-implementer.md +4 -0
- package/templates/claude/agents/ec-reviewer.md +6 -0
- package/templates/codex/agents/ec-implementer.toml +4 -0
- package/templates/codex/agents/ec-reviewer.toml +6 -0
- package/templates/common/bundled-skills/ec-init/SKILL.md +2 -2
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +6 -6
- package/templates/common/skills/ec-analysis/SKILL.md +56 -104
- package/templates/common/skills/ec-config/SKILL.md +37 -47
- package/templates/common/skills/ec-implementing/SKILL.md +26 -10
- package/templates/common/skills/ec-lite/SKILL.md +1 -1
- package/templates/common/skills/ec-memory/SKILL.md +3 -4
- package/templates/common/skills/ec-quality/SKILL.md +46 -25
- package/templates/common/skills/ec-task-management/SKILL.md +2 -2
- package/templates/common/skills/ec-tdd-init/SKILL.md +7 -7
- package/templates/common/skills/ec-workflow/SKILL.md +24 -22
- package/templates/main-constraint/AGENTS.md.tpl +23 -16
- package/templates/main-constraint/CLAUDE.md.tpl +23 -16
- package/templates/qoder/agents/ec-implementer.md +4 -0
- package/templates/qoder/agents/ec-reviewer.md +6 -0
- package/templates/runtime/tools/easy_coding_java_coverage.py +5 -5
- package/templates/shared-hooks/easy_coding_inputs.py +305 -0
- package/templates/shared-hooks/easy_coding_state.py +495 -461
- package/templates/shared-hooks/inject-subagent-context.py +3 -3
|
@@ -16,6 +16,10 @@ from datetime import datetime, timezone
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
import sys
|
|
18
18
|
|
|
19
|
+
from easy_coding_inputs import (
|
|
20
|
+
evidence_operation, memo, digest, input_spec, capture, changed_inputs, command_covers,
|
|
21
|
+
)
|
|
22
|
+
|
|
19
23
|
from easy_dev_spec import (
|
|
20
24
|
EasyDevSpecError,
|
|
21
25
|
inspect_manifest,
|
|
@@ -121,8 +125,8 @@ WIDE_WORKFLOW_CONTRACT_PATTERN = re.compile(
|
|
|
121
125
|
)
|
|
122
126
|
DEFAULT_APPROVAL_MODE = "guard"
|
|
123
127
|
DEFAULT_WORKFLOW_MODE = "adaptive"
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
DEFAULT_UNIT_TEST_MODE = "none"
|
|
129
|
+
DEFAULT_UT_COVERAGE_THRESHOLD = 90
|
|
126
130
|
TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1"
|
|
127
131
|
TDD_READINESS_SCOPE = "changed-production-lines"
|
|
128
132
|
TDD_READINESS_PATH = Path(".easy-coding/tdd/readiness.json")
|
|
@@ -454,7 +458,7 @@ def read_memory_config(root: Path) -> dict[str, int]:
|
|
|
454
458
|
return config
|
|
455
459
|
|
|
456
460
|
|
|
457
|
-
def
|
|
461
|
+
def parse_ut_threshold(value: object, source: str) -> int:
|
|
458
462
|
if isinstance(value, bool):
|
|
459
463
|
raise StateError(f"Invalid {source}: expected an integer from 1 to 100.")
|
|
460
464
|
try:
|
|
@@ -466,18 +470,13 @@ def parse_tdd_threshold(value: object, source: str) -> int:
|
|
|
466
470
|
return threshold
|
|
467
471
|
|
|
468
472
|
|
|
469
|
-
def
|
|
470
|
-
if value
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if normalized in {"true", "yes", "on"}:
|
|
474
|
-
return True
|
|
475
|
-
if normalized in {"false", "no", "off"}:
|
|
476
|
-
return False
|
|
477
|
-
raise StateError(f"Invalid {source}: expected true or false.")
|
|
473
|
+
def parse_unit_test_mode(value: object, source: str) -> str:
|
|
474
|
+
if not isinstance(value, str) or value not in {"none", "ut", "tdd"}:
|
|
475
|
+
raise StateError(f"Invalid {source}: expected none, ut, or tdd.")
|
|
476
|
+
return str(value)
|
|
478
477
|
|
|
479
478
|
|
|
480
|
-
def read_project_behavior(root: Path) -> tuple[str, str,
|
|
479
|
+
def read_project_behavior(root: Path) -> tuple[str, str, str, int]:
|
|
481
480
|
path = root / ".easy-coding" / "config.yaml"
|
|
482
481
|
try:
|
|
483
482
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
@@ -485,8 +484,8 @@ def read_project_behavior(root: Path) -> tuple[str, str, bool, int]:
|
|
|
485
484
|
return (
|
|
486
485
|
DEFAULT_APPROVAL_MODE,
|
|
487
486
|
DEFAULT_WORKFLOW_MODE,
|
|
488
|
-
|
|
489
|
-
|
|
487
|
+
DEFAULT_UNIT_TEST_MODE,
|
|
488
|
+
DEFAULT_UT_COVERAGE_THRESHOLD,
|
|
490
489
|
)
|
|
491
490
|
|
|
492
491
|
in_behavior = False
|
|
@@ -538,16 +537,20 @@ def read_project_behavior(root: Path) -> tuple[str, str, bool, int]:
|
|
|
538
537
|
"Invalid behavior.workflow_mode in .easy-coding/config.yaml: "
|
|
539
538
|
"expected adaptive, fast, standard, or strict."
|
|
540
539
|
)
|
|
541
|
-
if schema_version >=
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
540
|
+
if schema_version >= 6:
|
|
541
|
+
unit_test_mode = parse_unit_test_mode(
|
|
542
|
+
behavior.get("unit_test_mode", DEFAULT_UNIT_TEST_MODE), "behavior.unit_test_mode"
|
|
543
|
+
)
|
|
544
|
+
threshold = parse_ut_threshold(
|
|
545
|
+
behavior.get("ut_coverage_threshold", DEFAULT_UT_COVERAGE_THRESHOLD),
|
|
546
|
+
"behavior.ut_coverage_threshold",
|
|
546
547
|
)
|
|
547
548
|
else:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
549
|
+
if schema_version >= 4:
|
|
550
|
+
raise StateError("Run easy-coding upgrade to migrate unit-test settings to schema 6.")
|
|
551
|
+
unit_test_mode = DEFAULT_UNIT_TEST_MODE
|
|
552
|
+
threshold = DEFAULT_UT_COVERAGE_THRESHOLD
|
|
553
|
+
return approval_mode, workflow_mode, unit_test_mode, threshold
|
|
551
554
|
|
|
552
555
|
|
|
553
556
|
def safe_tdd_report_pattern(value: object) -> bool:
|
|
@@ -719,13 +722,13 @@ def require_tdd_readiness(root: Path) -> None:
|
|
|
719
722
|
|
|
720
723
|
def resolve_behavior(
|
|
721
724
|
root: Path, session: dict
|
|
722
|
-
) -> tuple[str, str | None, str, str, str | None, str,
|
|
723
|
-
project_approval, project_workflow,
|
|
725
|
+
) -> tuple[str, str | None, str, str, str | None, str, str, str | None, str, int, int | None, int]:
|
|
726
|
+
project_approval, project_workflow, project_unit_test, project_threshold = read_project_behavior(root)
|
|
724
727
|
legacy = session.get("confirm_mode")
|
|
725
728
|
session_approval = session.get("approval_mode")
|
|
726
729
|
session_workflow = session.get("workflow_mode")
|
|
727
|
-
|
|
728
|
-
session_threshold = session.get("
|
|
730
|
+
session_unit_test = session.get("unit_test_mode")
|
|
731
|
+
session_threshold = session.get("ut_coverage_threshold")
|
|
729
732
|
if session_approval is None:
|
|
730
733
|
if legacy == "lite":
|
|
731
734
|
session_approval = "guard"
|
|
@@ -744,11 +747,11 @@ def resolve_behavior(
|
|
|
744
747
|
raise StateError(
|
|
745
748
|
"Invalid session workflow_mode: expected adaptive, fast, standard, or strict."
|
|
746
749
|
)
|
|
747
|
-
if
|
|
748
|
-
|
|
750
|
+
if session_unit_test is not None:
|
|
751
|
+
session_unit_test = parse_unit_test_mode(session_unit_test, "session unit_test_mode")
|
|
749
752
|
if session_threshold is not None:
|
|
750
|
-
session_threshold =
|
|
751
|
-
session_threshold, "session
|
|
753
|
+
session_threshold = parse_ut_threshold(
|
|
754
|
+
session_threshold, "session ut_coverage_threshold"
|
|
752
755
|
)
|
|
753
756
|
return (
|
|
754
757
|
project_approval,
|
|
@@ -757,9 +760,9 @@ def resolve_behavior(
|
|
|
757
760
|
project_workflow,
|
|
758
761
|
str(session_workflow) if session_workflow else None,
|
|
759
762
|
str(session_workflow or project_workflow),
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
+
project_unit_test,
|
|
764
|
+
session_unit_test,
|
|
765
|
+
session_unit_test if session_unit_test is not None else project_unit_test,
|
|
763
766
|
project_threshold,
|
|
764
767
|
session_threshold,
|
|
765
768
|
session_threshold if session_threshold is not None else project_threshold,
|
|
@@ -771,7 +774,22 @@ def resolve_approval_mode(root: Path, session: dict) -> tuple[str, str | None, s
|
|
|
771
774
|
return behavior[0], behavior[1], behavior[2]
|
|
772
775
|
|
|
773
776
|
|
|
777
|
+
def migrate_unit_test_settings(record: dict) -> bool:
|
|
778
|
+
changed = False
|
|
779
|
+
if "tdd_enabled" in record:
|
|
780
|
+
enabled = record.pop("tdd_enabled")
|
|
781
|
+
if "unit_test_mode" not in record and isinstance(enabled, bool):
|
|
782
|
+
record["unit_test_mode"] = "tdd" if enabled else "none"
|
|
783
|
+
changed = True
|
|
784
|
+
if "tdd_coverage_threshold" in record:
|
|
785
|
+
threshold = record.pop("tdd_coverage_threshold")
|
|
786
|
+
record.setdefault("ut_coverage_threshold", threshold)
|
|
787
|
+
changed = True
|
|
788
|
+
return changed
|
|
789
|
+
|
|
790
|
+
|
|
774
791
|
def materialize_legacy_session_behavior(session: dict) -> None:
|
|
792
|
+
migrate_unit_test_settings(session)
|
|
775
793
|
legacy = session.get("confirm_mode")
|
|
776
794
|
if legacy == "lite":
|
|
777
795
|
session.setdefault("approval_mode", "guard")
|
|
@@ -1260,7 +1278,7 @@ def normalize_legacy_stage(stage: object) -> object:
|
|
|
1260
1278
|
def normalize_legacy_task(task: dict) -> bool:
|
|
1261
1279
|
"""Normalize legacy task state without touching artifacts outside task.json."""
|
|
1262
1280
|
legacy_status = str(task.get("status") or "")
|
|
1263
|
-
changed =
|
|
1281
|
+
changed = migrate_unit_test_settings(task)
|
|
1264
1282
|
|
|
1265
1283
|
for field in ("created_by", "last_agent"):
|
|
1266
1284
|
normalized_agent = canonical_agent_identity(
|
|
@@ -1825,13 +1843,42 @@ def is_valid_review_finding(value: object) -> bool:
|
|
|
1825
1843
|
)
|
|
1826
1844
|
|
|
1827
1845
|
|
|
1846
|
+
def gate_identity(record: dict) -> tuple:
|
|
1847
|
+
return (str(record.get("source_task_id") or ""), str(record.get("unit_id") or ""),
|
|
1848
|
+
str(record.get("dimension") or record.get("check") or ""),
|
|
1849
|
+
str(record.get("review_scope") or record.get("coverage_scope") or ""))
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
def failure_label(record: dict) -> str:
|
|
1853
|
+
_, unit, name, scope = gate_identity(record)
|
|
1854
|
+
label = f"{record['type']}:{name}"
|
|
1855
|
+
if scope:
|
|
1856
|
+
label += f":{scope}"
|
|
1857
|
+
if unit:
|
|
1858
|
+
label += f":unit={unit}"
|
|
1859
|
+
return label
|
|
1860
|
+
|
|
1861
|
+
|
|
1862
|
+
def require_unit_evidence(plan: dict, records: list[dict], label: str, dimensions: int = 0) -> None:
|
|
1863
|
+
if not any(record.get("unit_id") for record in records):
|
|
1864
|
+
return
|
|
1865
|
+
for unit in plan.get("units", []):
|
|
1866
|
+
applicable = [record for record in records
|
|
1867
|
+
if record.get("unit_id") == unit["id"] or
|
|
1868
|
+
(not record.get("unit_id") and
|
|
1869
|
+
(not record.get("source_task_id") or
|
|
1870
|
+
record["source_task_id"] == unit.get("source_task_id")))]
|
|
1871
|
+
if not applicable or (dimensions and len({r.get("dimension") for r in applicable}) < dimensions):
|
|
1872
|
+
raise StateError(f"{label} evidence does not cover Unit {unit['id']} at the required depth.")
|
|
1873
|
+
|
|
1874
|
+
|
|
1828
1875
|
def validate_quality_gate_record_schemas(
|
|
1829
1876
|
review_records: list[dict], verification_records: list[dict]
|
|
1830
1877
|
) -> None:
|
|
1831
1878
|
latest_reviews: dict[tuple[str, str], dict] = {}
|
|
1832
1879
|
for index, record in enumerate(review_records):
|
|
1833
1880
|
dimension = str(record.get("dimension") or f"<missing-{index}>")
|
|
1834
|
-
latest_reviews[(
|
|
1881
|
+
latest_reviews[gate_identity(record)] = record
|
|
1835
1882
|
for record in latest_reviews.values():
|
|
1836
1883
|
findings = record.get("findings")
|
|
1837
1884
|
if (
|
|
@@ -1859,13 +1906,7 @@ def validate_quality_gate_record_schemas(
|
|
|
1859
1906
|
latest_verifications: dict[tuple[str, str, str], dict] = {}
|
|
1860
1907
|
for index, record in enumerate(verification_records):
|
|
1861
1908
|
check = str(record.get("check") or f"<missing-{index}>")
|
|
1862
|
-
latest_verifications[
|
|
1863
|
-
(
|
|
1864
|
-
str(record.get("source_task_id") or ""),
|
|
1865
|
-
check,
|
|
1866
|
-
str(record.get("coverage_scope") or ""),
|
|
1867
|
-
)
|
|
1868
|
-
] = record
|
|
1909
|
+
latest_verifications[gate_identity(record)] = record
|
|
1869
1910
|
for record in latest_verifications.values():
|
|
1870
1911
|
applicable = record.get("applicable") is not False
|
|
1871
1912
|
if (
|
|
@@ -2249,6 +2290,34 @@ def require_spec_context(
|
|
|
2249
2290
|
raise StateError("Current session must consume the bound Canonical Spec via resume-spec-context before advancing.")
|
|
2250
2291
|
|
|
2251
2292
|
|
|
2293
|
+
def refresh_correction_plan(root: Path, task_id: str, task: dict, inspection: dict) -> None:
|
|
2294
|
+
plan = latest_execution_plan(root, task_id)
|
|
2295
|
+
if plan is None:
|
|
2296
|
+
raise StateError("A correction must preserve its original execution plan.")
|
|
2297
|
+
selection = select_tasks(inspection, task["selected_spec_tasks"], allow_pending_hard_dependencies=True)
|
|
2298
|
+
steps = {s["step_id"]: s for s in selection["selected_steps"]}
|
|
2299
|
+
changes = {c["change_id"]: c for c in selection["selected_changes"]}
|
|
2300
|
+
tests = {t["test_id"]: t for t in selection["selected_tests"]}
|
|
2301
|
+
correction = task["correction"]
|
|
2302
|
+
plan["spec_design_sha256"] = inspection["design_sha256"]
|
|
2303
|
+
for unit in plan["units"]:
|
|
2304
|
+
if unit["id"] not in correction["unit_ids"]:
|
|
2305
|
+
continue
|
|
2306
|
+
source_steps = [steps[s] for s in unit["source_step_ids"] if s in steps]
|
|
2307
|
+
selected_changes = [changes[c] for s in source_steps for c in s.get("change_ids", [])]
|
|
2308
|
+
original_files = set(unit["files"])
|
|
2309
|
+
mapped_files = {c["path"] for c in selected_changes}
|
|
2310
|
+
if not mapped_files <= original_files:
|
|
2311
|
+
raise StateError("Source design expands the correction scope; analyze the new requirement.")
|
|
2312
|
+
unit["source_step_ids"] = [s["step_id"] for s in source_steps]
|
|
2313
|
+
unit["files"] = sorted(mapped_files | (original_files & set(correction["files"])))
|
|
2314
|
+
unit["symbols"] = sorted({symbol for c in selected_changes for symbol in c.get("symbols", [])})
|
|
2315
|
+
unit["test_commands"] = sorted({tests[t]["command"] for s in source_steps for t in s.get("test_ids", [])})
|
|
2316
|
+
unit["acceptance_criteria"] = [correction["summary"]]
|
|
2317
|
+
unit["contracts"] = [correction["summary"]]
|
|
2318
|
+
append_execution_record(root, task_id, plan)
|
|
2319
|
+
|
|
2320
|
+
|
|
2252
2321
|
def begin_spec_change(
|
|
2253
2322
|
root: Path, affected_task_ids: list[str], summary: str, agent: str,
|
|
2254
2323
|
task_id: str | None = None, session_file: str | Path | None = None,
|
|
@@ -2375,7 +2444,7 @@ def is_valid_spec_execution_plan(root: Path, task: dict, plan: object) -> bool:
|
|
|
2375
2444
|
if (
|
|
2376
2445
|
not step_change_ids.issubset(change_by_id)
|
|
2377
2446
|
or not step_test_ids.issubset(test_by_id)
|
|
2378
|
-
or set(unit.get("files", [])) != step_files
|
|
2447
|
+
or (set(unit.get("files", [])) - set(task.get("correction", {}).get("files", []))) != (step_files - set(task.get("correction", {}).get("files", [])))
|
|
2379
2448
|
or set(unit["symbols"]) != step_symbols
|
|
2380
2449
|
or not set(unit["test_commands"]).issuperset(step_commands)
|
|
2381
2450
|
):
|
|
@@ -2397,9 +2466,10 @@ def is_valid_spec_execution_plan(root: Path, task: dict, plan: object) -> bool:
|
|
|
2397
2466
|
steps = covered_steps[source_task_id]
|
|
2398
2467
|
if len(steps) != len(set(steps)) or set(steps) != set(source_task.get("step_ids", [])):
|
|
2399
2468
|
return False
|
|
2400
|
-
|
|
2469
|
+
restoration_files = set(task.get("correction", {}).get("files", []))
|
|
2470
|
+
if covered_files[source_task_id] - restoration_files != {
|
|
2401
2471
|
str(change["path"]) for change in changes_by_task[source_task_id]
|
|
2402
|
-
}:
|
|
2472
|
+
} - restoration_files:
|
|
2403
2473
|
return False
|
|
2404
2474
|
if covered_symbols[source_task_id] != {
|
|
2405
2475
|
str(symbol)
|
|
@@ -2545,7 +2615,7 @@ def has_valid_execution_plan(root: Path, task_id: str) -> bool:
|
|
|
2545
2615
|
return False
|
|
2546
2616
|
if isinstance(record, dict) and record.get("type") == "plan":
|
|
2547
2617
|
latest_plan = record
|
|
2548
|
-
elif isinstance(record, dict) and record.get("type") == "spec-design-sync":
|
|
2618
|
+
elif isinstance(record, dict) and record.get("type") == "spec-design-sync" and not record.get("preserve_plan"):
|
|
2549
2619
|
latest_plan = None
|
|
2550
2620
|
except OSError:
|
|
2551
2621
|
return False
|
|
@@ -2583,7 +2653,7 @@ def latest_execution_plan(root: Path, task_id: str) -> dict | None:
|
|
|
2583
2653
|
for record in execution_records(root, task_id):
|
|
2584
2654
|
if record.get("type") == "plan":
|
|
2585
2655
|
latest = record
|
|
2586
|
-
elif record.get("type") == "spec-design-sync":
|
|
2656
|
+
elif record.get("type") == "spec-design-sync" and not record.get("preserve_plan"):
|
|
2587
2657
|
latest = None
|
|
2588
2658
|
if latest is None or not is_valid_execution_plan(latest, allow_empty_files=True):
|
|
2589
2659
|
return None
|
|
@@ -2833,7 +2903,7 @@ def tdd_baseline_marker_reasons(
|
|
|
2833
2903
|
def contains_tdd_threshold(content: str, threshold: int) -> bool:
|
|
2834
2904
|
return re.search(
|
|
2835
2905
|
rf"(?<!\d){threshold}\s*%|--threshold(?:\s+|=){threshold}(?!\d)|"
|
|
2836
|
-
rf"
|
|
2906
|
+
rf"ut_coverage_threshold\s*[:=]\s*{threshold}(?!\d)",
|
|
2837
2907
|
content,
|
|
2838
2908
|
re.IGNORECASE,
|
|
2839
2909
|
) is not None
|
|
@@ -3076,220 +3146,176 @@ def tdd_infrastructure_fingerprint(repositories: set[Path]) -> str:
|
|
|
3076
3146
|
return digest.hexdigest()
|
|
3077
3147
|
|
|
3078
3148
|
|
|
3149
|
+
def check_identity(record: dict) -> tuple:
|
|
3150
|
+
return tuple(str(record.get(key) or "") for key in (
|
|
3151
|
+
"type", "unit_id", "source_task_id", "dimension", "review_scope", "check", "check_type", "command", "coverage_scope"
|
|
3152
|
+
))
|
|
3153
|
+
|
|
3154
|
+
|
|
3155
|
+
def prepare_check(root: Path, task_id: str, task: dict, descriptor: dict, agent: str) -> dict:
|
|
3156
|
+
if task.get("status") not in {"IMPLEMENT", "QUALITY"}:
|
|
3157
|
+
raise StateError("Checks belong to IMPLEMENT or QUALITY.")
|
|
3158
|
+
if descriptor.get("type") not in {"review", "verify"}:
|
|
3159
|
+
raise StateError("A check must identify type review or verify.")
|
|
3160
|
+
if descriptor.get("type") == "verify" and not is_non_empty_string(descriptor.get("command")):
|
|
3161
|
+
raise StateError("Verification must identify the actual command before execution.")
|
|
3162
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3163
|
+
if descriptor.get("unit_id"):
|
|
3164
|
+
owner = next((u for u in plan.get("units", []) if u["id"] == descriptor["unit_id"]), None)
|
|
3165
|
+
if owner is None:
|
|
3166
|
+
raise StateError("Check unit does not belong to the current plan.")
|
|
3167
|
+
for field in ("repo_id", "source_task_id"):
|
|
3168
|
+
if field in owner:
|
|
3169
|
+
descriptor[field] = owner[field]
|
|
3170
|
+
inputs = capture(input_spec(root, task, plan, descriptor))
|
|
3171
|
+
records = execution_records(root, task_id)
|
|
3172
|
+
previous = next((r for r in reversed(records)
|
|
3173
|
+
if check_identity(r) == check_identity(descriptor)), None)
|
|
3174
|
+
if previous and previous.get("passed") is True and previous.get("inputs") == inputs:
|
|
3175
|
+
return {"reusable": True, "evidence_index": records.index(previous),
|
|
3176
|
+
"input_signature": inputs["signature"], "changed_inputs": []}
|
|
3177
|
+
prepared_id = digest([descriptor, inputs["signature"]])
|
|
3178
|
+
if not any(r.get("prepared_id") == prepared_id and r.get("type") == "check-inputs" for r in records):
|
|
3179
|
+
append_execution_record(root, task_id, {
|
|
3180
|
+
"type": "check-inputs", "prepared_id": prepared_id, "descriptor": descriptor,
|
|
3181
|
+
"inputs": inputs, "timestamp": now_iso(), "agent": agent,
|
|
3182
|
+
})
|
|
3183
|
+
return {"reusable": False, "prepared_id": prepared_id,
|
|
3184
|
+
"input_signature": inputs["signature"],
|
|
3185
|
+
"changed_inputs": changed_inputs(previous["inputs"], inputs)
|
|
3186
|
+
if previous and isinstance(previous.get("inputs"), dict) else ["no matching input-bound evidence"]}
|
|
3187
|
+
|
|
3188
|
+
|
|
3189
|
+
def record_check(root: Path, task_id: str, task: dict, prepared_id: str, result: dict, agent: str) -> dict:
|
|
3190
|
+
prepared = next((r for r in reversed(execution_records(root, task_id))
|
|
3191
|
+
if r.get("type") == "check-inputs" and r.get("prepared_id") == prepared_id), None)
|
|
3192
|
+
if prepared is None:
|
|
3193
|
+
raise StateError("Prepare the check before executing it.")
|
|
3194
|
+
descriptor = prepared["descriptor"]
|
|
3195
|
+
current = capture(input_spec(root, task, latest_execution_plan(root, task_id) or {}, descriptor))
|
|
3196
|
+
changes = changed_inputs(prepared["inputs"], current)
|
|
3197
|
+
if changes:
|
|
3198
|
+
raise StateError("Check inputs changed during execution: " + "; ".join(changes))
|
|
3199
|
+
if type(result.get("passed")) is not bool:
|
|
3200
|
+
raise StateError("Check result must include passed.")
|
|
3201
|
+
if descriptor["type"] == "verify" and result.get("applicable") is not False:
|
|
3202
|
+
if type(result.get("exit_code")) is not int or result["passed"] != (result["exit_code"] == 0):
|
|
3203
|
+
raise StateError("Verification passed must agree with its real exit_code.")
|
|
3204
|
+
context = None
|
|
3205
|
+
if task.get("status") == "QUALITY":
|
|
3206
|
+
context = ensure_quality_attempt_context(root, task_id, task, agent, persist=True)
|
|
3207
|
+
elif task.get("status") != "IMPLEMENT":
|
|
3208
|
+
raise StateError("Record checks only during implementation or quality.")
|
|
3209
|
+
record = {
|
|
3210
|
+
**result, **descriptor, **evidence_fingerprints(root, task_id),
|
|
3211
|
+
"inputs": current, "prepared_id": prepared_id, "timestamp": now_iso(),
|
|
3212
|
+
"quality_attempt": context["attempt"] if context else 0,
|
|
3213
|
+
}
|
|
3214
|
+
append_execution_record(root, task_id, record)
|
|
3215
|
+
return {"recorded": True, "passed": record["passed"], "input_signature": current["signature"]}
|
|
3216
|
+
|
|
3217
|
+
|
|
3218
|
+
def carry_forward_scoped_evidence(root: Path, task_id: str, task: dict, context: dict) -> None:
|
|
3219
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3220
|
+
latest = {}
|
|
3221
|
+
for index, record in enumerate(execution_records(root, task_id)):
|
|
3222
|
+
if record.get("type") in {"review", "verify"}:
|
|
3223
|
+
latest[check_identity(record)] = (index, record)
|
|
3224
|
+
for index, record in latest.values():
|
|
3225
|
+
if record.get("passed") is not True or not isinstance(record.get("inputs"), dict):
|
|
3226
|
+
continue
|
|
3227
|
+
if any(f.get("severity") == "error" for f in record.get("findings", [])):
|
|
3228
|
+
continue
|
|
3229
|
+
if record.get("unit_id") and not any(u["id"] == record["unit_id"] for u in plan.get("units", [])):
|
|
3230
|
+
continue
|
|
3231
|
+
current = capture(input_spec(root, task, plan, record))
|
|
3232
|
+
if current != record["inputs"]:
|
|
3233
|
+
continue
|
|
3234
|
+
if record.get("quality_attempt") == context["attempt"]:
|
|
3235
|
+
continue
|
|
3236
|
+
# 仅运行时生成引用,保留原始执行时间与输入;Agent 不重新包装历史结论。
|
|
3237
|
+
append_execution_record(root, task_id, {
|
|
3238
|
+
**record, "reused_from": index, "quality_attempt": context["attempt"],
|
|
3239
|
+
"implementation_fingerprint": context["implementation_fingerprint"],
|
|
3240
|
+
"config_fingerprint": context["config_fingerprint"],
|
|
3241
|
+
})
|
|
3242
|
+
|
|
3243
|
+
|
|
3244
|
+
def begin_correction(root: Path, task_id: str, task: dict, files: list[str], summary: str,
|
|
3245
|
+
risks: list[str], agent: str) -> dict:
|
|
3246
|
+
if task.get("status") not in {"IMPLEMENT", "QUALITY", "MEMORY", "ANALYSIS"}:
|
|
3247
|
+
raise StateError("A correction needs an active implementation task.")
|
|
3248
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3249
|
+
units = plan.get("units", [])
|
|
3250
|
+
allowed = {f for unit in units for f in unit.get("files", [])}
|
|
3251
|
+
if not files or not set(files) <= allowed or not summary.strip():
|
|
3252
|
+
raise StateError("A correction must name existing task files and the confirmed change.")
|
|
3253
|
+
cancel_active_quality_attempt(root, task_id, task, agent, summary, "manual-return")
|
|
3254
|
+
task = load_task(root, task_id) or task
|
|
3255
|
+
cleanup_verification_checkpoint(root, task_id, task)
|
|
3256
|
+
task["correction"] = {
|
|
3257
|
+
"files": sorted(set(files)), "summary": summary.strip(), "risks": risks,
|
|
3258
|
+
"unit_ids": [u["id"] for u in units if set(u.get("files", [])) & set(files)],
|
|
3259
|
+
"started_at": now_iso(),
|
|
3260
|
+
}
|
|
3261
|
+
records = validated_quality_records(root, task_id)
|
|
3262
|
+
if records:
|
|
3263
|
+
task["quality_consumed_attempt"] = records[-1][1]["attempt"]
|
|
3264
|
+
task.pop("quality_return_required", None)
|
|
3265
|
+
task.pop("pending_transition", None)
|
|
3266
|
+
task["status"] = "IMPLEMENT"
|
|
3267
|
+
append_stage_history(task, "IMPLEMENT", agent)
|
|
3268
|
+
write_task(root, task_id, task)
|
|
3269
|
+
task["workflow_mode"], reasons = calculate_workflow_floor(root, task_id)
|
|
3270
|
+
write_task(root, task_id, task)
|
|
3271
|
+
if isinstance(task.get("spec_source"), dict):
|
|
3272
|
+
writeback_ready_tasks_for_implement(root, task_id, task, agent, source_task_ids={
|
|
3273
|
+
str(u["source_task_id"]) for u in units if u["id"] in task["correction"]["unit_ids"]
|
|
3274
|
+
})
|
|
3275
|
+
append_execution_record(root, task_id, {
|
|
3276
|
+
"type": "correction", **task["correction"], "workflow_mode": task["workflow_mode"],
|
|
3277
|
+
})
|
|
3278
|
+
return {"task_id": task_id, "status": "IMPLEMENT", "workflow_mode": task["workflow_mode"],
|
|
3279
|
+
"reasons": reasons, "correction": task["correction"]}
|
|
3280
|
+
|
|
3281
|
+
|
|
3079
3282
|
def implementation_fingerprint(root: Path, task_id: str) -> str:
|
|
3080
3283
|
plan = latest_execution_plan(root, task_id)
|
|
3081
3284
|
if not plan:
|
|
3082
3285
|
raise StateError("Cannot calculate implementation fingerprint without a valid plan.")
|
|
3083
|
-
task = load_task(root, task_id)
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
digest.update(b"workflow-mode\0")
|
|
3087
|
-
digest.update(workflow_mode.encode("utf-8"))
|
|
3088
|
-
digest.update(b"\0")
|
|
3089
|
-
if task and task.get("tdd_enabled") is True:
|
|
3090
|
-
digest.update(b"tdd\0enabled\0")
|
|
3091
|
-
digest.update(str(task.get("tdd_coverage_threshold") or "").encode("utf-8"))
|
|
3092
|
-
digest.update(b"\0")
|
|
3093
|
-
digest.update(
|
|
3094
|
-
json.dumps(
|
|
3095
|
-
task.get("tdd_baselines") or {},
|
|
3096
|
-
sort_keys=True,
|
|
3097
|
-
separators=(",", ":"),
|
|
3098
|
-
).encode("utf-8")
|
|
3099
|
-
)
|
|
3100
|
-
digest.update(b"\0")
|
|
3101
|
-
digest.update(tdd_infrastructure_fingerprint(
|
|
3102
|
-
{root.resolve(), *task_repository_roots(root, task, plan)}
|
|
3103
|
-
).encode("ascii"))
|
|
3104
|
-
digest.update(b"execution-plan\0")
|
|
3105
|
-
digest.update(
|
|
3106
|
-
json.dumps(
|
|
3107
|
-
plan,
|
|
3108
|
-
ensure_ascii=False,
|
|
3109
|
-
sort_keys=True,
|
|
3110
|
-
separators=(",", ":"),
|
|
3111
|
-
).encode("utf-8")
|
|
3112
|
-
)
|
|
3113
|
-
digest.update(b"\0")
|
|
3114
|
-
if task and isinstance(task.get("spec_source"), dict):
|
|
3115
|
-
digest.update(b"canonical-spec\0")
|
|
3116
|
-
source = task.get("spec_source") or {}
|
|
3117
|
-
digest.update(
|
|
3118
|
-
json.dumps(
|
|
3119
|
-
{
|
|
3120
|
-
"source": {
|
|
3121
|
-
"schema": source.get("schema"),
|
|
3122
|
-
"spec_id": source.get("spec_id"),
|
|
3123
|
-
"revision": source.get("revision"),
|
|
3124
|
-
"design_sha256": source.get("design_sha256"),
|
|
3125
|
-
},
|
|
3126
|
-
"selected_tasks": task.get("selected_spec_tasks"),
|
|
3127
|
-
},
|
|
3128
|
-
ensure_ascii=False,
|
|
3129
|
-
sort_keys=True,
|
|
3130
|
-
separators=(",", ":"),
|
|
3131
|
-
).encode("utf-8")
|
|
3132
|
-
)
|
|
3133
|
-
digest.update(b"\0")
|
|
3134
|
-
update_git_worktree_fingerprint(digest, root, task, plan)
|
|
3135
|
-
repo_paths = task.get("repo_paths") if task else None
|
|
3136
|
-
file_entries: set[tuple[str, str | None]] = {
|
|
3137
|
-
(str(file_name), str(unit.get("repo_id")) if unit.get("repo_id") else None)
|
|
3138
|
-
for unit in plan.get("units", [])
|
|
3139
|
-
if isinstance(unit, dict)
|
|
3140
|
-
for file_name in unit.get("files", [])
|
|
3141
|
-
if is_non_empty_string(file_name)
|
|
3142
|
-
}
|
|
3143
|
-
for file_name, repo_id in sorted(file_entries, key=lambda item: (item[0], item[1] or "")):
|
|
3144
|
-
candidate = Path(file_name)
|
|
3145
|
-
was_absolute = candidate.is_absolute()
|
|
3146
|
-
base = root
|
|
3147
|
-
if (
|
|
3148
|
-
task
|
|
3149
|
-
and isinstance(task.get("spec_source"), dict)
|
|
3150
|
-
and isinstance(repo_paths, dict)
|
|
3151
|
-
and repo_id
|
|
3152
|
-
and is_non_empty_string(repo_paths.get(repo_id))
|
|
3153
|
-
):
|
|
3154
|
-
raw_base = Path(str(repo_paths[repo_id]))
|
|
3155
|
-
base = raw_base if raw_base.is_absolute() else root / raw_base
|
|
3156
|
-
if not was_absolute:
|
|
3157
|
-
candidate = base / candidate
|
|
3158
|
-
resolved = candidate.resolve()
|
|
3159
|
-
if not was_absolute:
|
|
3160
|
-
try:
|
|
3161
|
-
resolved.relative_to(base.resolve())
|
|
3162
|
-
except ValueError as error:
|
|
3163
|
-
raise StateError(f"Execution plan file escapes repository: {file_name}") from error
|
|
3164
|
-
digest.update(f"{repo_id or ''}:{file_name}".encode("utf-8"))
|
|
3165
|
-
digest.update(b"\0")
|
|
3166
|
-
try:
|
|
3167
|
-
digest.update(resolved.read_bytes())
|
|
3168
|
-
except OSError:
|
|
3169
|
-
digest.update(b"<missing>")
|
|
3170
|
-
digest.update(b"\0")
|
|
3171
|
-
return digest.hexdigest()
|
|
3286
|
+
task = load_task(root, task_id) or {}
|
|
3287
|
+
# 候选只绑定实际输入与验收契约,执行状态、revision、模式和计划说明不参与。
|
|
3288
|
+
return capture(input_spec(root, task, plan, {"type": "review"}))["signature"]
|
|
3172
3289
|
|
|
3173
3290
|
|
|
3174
|
-
def canonical_repository_fingerprints(
|
|
3175
|
-
root: Path, task_id: str, task: dict
|
|
3176
|
-
) -> dict[str, str]:
|
|
3291
|
+
def canonical_repository_fingerprints(root: Path, task_id: str, task: dict) -> dict[str, str]:
|
|
3177
3292
|
if not isinstance(task.get("spec_source"), dict):
|
|
3178
3293
|
return {}
|
|
3179
3294
|
plan = latest_execution_plan(root, task_id) or {}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
if isinstance(unit, dict) and is_non_empty_string(unit.get("repo_id"))
|
|
3187
|
-
}
|
|
3188
|
-
):
|
|
3189
|
-
raw_base = repo_paths.get(repo_id)
|
|
3190
|
-
if not is_non_empty_string(raw_base):
|
|
3191
|
-
continue
|
|
3192
|
-
base = Path(str(raw_base))
|
|
3193
|
-
if not base.is_absolute():
|
|
3194
|
-
base = root / base
|
|
3195
|
-
base = base.resolve()
|
|
3196
|
-
digest = hashlib.sha256()
|
|
3197
|
-
if task.get("tdd_enabled") is True:
|
|
3198
|
-
digest.update(tdd_infrastructure_fingerprint({root.resolve(), base}).encode("ascii"))
|
|
3199
|
-
units = [
|
|
3200
|
-
unit
|
|
3201
|
-
for unit in plan.get("units", [])
|
|
3202
|
-
if isinstance(unit, dict) and unit.get("repo_id") == repo_id
|
|
3203
|
-
]
|
|
3204
|
-
digest.update(
|
|
3205
|
-
json.dumps(
|
|
3206
|
-
units,
|
|
3207
|
-
ensure_ascii=False,
|
|
3208
|
-
sort_keys=True,
|
|
3209
|
-
separators=(",", ":"),
|
|
3210
|
-
).encode("utf-8")
|
|
3211
|
-
)
|
|
3212
|
-
digest.update(b"\0")
|
|
3213
|
-
repository = git_repository_root(base)
|
|
3214
|
-
if repository is not None and repository.resolve() == base:
|
|
3215
|
-
update_git_repository_content_fingerprint(
|
|
3216
|
-
digest,
|
|
3217
|
-
root,
|
|
3218
|
-
repository,
|
|
3219
|
-
[base],
|
|
3220
|
-
set(),
|
|
3221
|
-
)
|
|
3222
|
-
else:
|
|
3223
|
-
for unit in units:
|
|
3224
|
-
for file_name in sorted(
|
|
3225
|
-
str(value)
|
|
3226
|
-
for value in unit.get("files", [])
|
|
3227
|
-
if is_non_empty_string(value)
|
|
3228
|
-
):
|
|
3229
|
-
candidate = (base / file_name).resolve()
|
|
3230
|
-
try:
|
|
3231
|
-
candidate.relative_to(base)
|
|
3232
|
-
except ValueError as error:
|
|
3233
|
-
raise StateError(
|
|
3234
|
-
f"Execution plan file escapes repository: {file_name}"
|
|
3235
|
-
) from error
|
|
3236
|
-
digest.update(file_name.encode("utf-8"))
|
|
3237
|
-
digest.update(b"\0")
|
|
3238
|
-
try:
|
|
3239
|
-
digest.update(candidate.read_bytes())
|
|
3240
|
-
except OSError:
|
|
3241
|
-
digest.update(b"<missing>")
|
|
3242
|
-
digest.update(b"\0")
|
|
3243
|
-
fingerprints[repo_id] = digest.hexdigest()
|
|
3244
|
-
return fingerprints
|
|
3295
|
+
return {
|
|
3296
|
+
repo: digest([capture(input_spec(root, task, plan, {
|
|
3297
|
+
"type": "review", "unit_id": unit["id"]
|
|
3298
|
+
}))["signature"] for unit in plan.get("units", []) if unit.get("repo_id") == repo])
|
|
3299
|
+
for repo in sorted({unit["repo_id"] for unit in plan.get("units", [])})
|
|
3300
|
+
}
|
|
3245
3301
|
|
|
3246
3302
|
|
|
3247
|
-
def
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
clean = line.split("#", 1)[0].rstrip()
|
|
3259
|
-
stripped = clean.strip()
|
|
3260
|
-
indent = len(clean) - len(clean.lstrip(" "))
|
|
3261
|
-
if stripped == "behavior:":
|
|
3262
|
-
in_behavior = True
|
|
3263
|
-
behavior_indent = indent
|
|
3264
|
-
behavior_key_indent = None
|
|
3265
|
-
filtered.append(line)
|
|
3266
|
-
continue
|
|
3267
|
-
if in_behavior and stripped and indent <= behavior_indent:
|
|
3268
|
-
in_behavior = False
|
|
3269
|
-
if in_behavior and stripped:
|
|
3270
|
-
if behavior_key_indent is None:
|
|
3271
|
-
behavior_key_indent = indent
|
|
3272
|
-
key = stripped.split(":", 1)[0]
|
|
3273
|
-
if (
|
|
3274
|
-
indent == behavior_key_indent
|
|
3275
|
-
and key in {"tdd_enabled", "tdd_coverage_threshold"}
|
|
3276
|
-
):
|
|
3277
|
-
continue
|
|
3278
|
-
filtered.append(line)
|
|
3279
|
-
return "".join(filtered).encode("utf-8")
|
|
3303
|
+
def unit_test_contract(task: dict) -> dict:
|
|
3304
|
+
mode = task.get("unit_test_mode")
|
|
3305
|
+
# 旧证据的序列化键保持不变,配置字段改名不触发全局失效;UT 单独标识。
|
|
3306
|
+
contract = {
|
|
3307
|
+
"tdd_enabled": None if mode is None else mode != "none",
|
|
3308
|
+
"tdd_coverage_threshold": task.get("ut_coverage_threshold"),
|
|
3309
|
+
"tdd_baselines": task.get("tdd_baselines"),
|
|
3310
|
+
}
|
|
3311
|
+
if mode == "ut":
|
|
3312
|
+
contract["unit_test_mode"] = "ut"
|
|
3313
|
+
return contract
|
|
3280
3314
|
|
|
3281
3315
|
|
|
3282
3316
|
def behavior_config_fingerprint(root: Path, task: dict | None = None) -> str:
|
|
3283
|
-
|
|
3284
|
-
digest
|
|
3285
|
-
try:
|
|
3286
|
-
payload = path.read_bytes()
|
|
3287
|
-
if task and isinstance(task.get("tdd_enabled"), bool):
|
|
3288
|
-
payload = config_without_frozen_tdd_settings(payload)
|
|
3289
|
-
digest.update(payload)
|
|
3290
|
-
except OSError:
|
|
3291
|
-
digest.update(b"<missing-config>")
|
|
3292
|
-
return digest.hexdigest()
|
|
3317
|
+
# 审批方式、记忆策略等配置不影响已经执行的测试。
|
|
3318
|
+
return digest(unit_test_contract(task or {}))
|
|
3293
3319
|
|
|
3294
3320
|
|
|
3295
3321
|
def evidence_fingerprints(root: Path, task_id: str) -> dict[str, str]:
|
|
@@ -3599,6 +3625,7 @@ def ensure_quality_attempt_context(
|
|
|
3599
3625
|
)
|
|
3600
3626
|
if (
|
|
3601
3627
|
finalized.get("outcome") in {"passed", "repair", "replan"}
|
|
3628
|
+
and finalized.get("attempt") != task.get("quality_consumed_attempt")
|
|
3602
3629
|
and finalized.get("implementation_fingerprint")
|
|
3603
3630
|
== expected["implementation_fingerprint"]
|
|
3604
3631
|
and finalized.get("config_fingerprint") == expected["config_fingerprint"]
|
|
@@ -3612,6 +3639,7 @@ def ensure_quality_attempt_context(
|
|
|
3612
3639
|
)
|
|
3613
3640
|
if persist:
|
|
3614
3641
|
append_canonical_quality_carry_forward(root, task_id, task, context, agent)
|
|
3642
|
+
carry_forward_scoped_evidence(root, task_id, task, context)
|
|
3615
3643
|
task["quality_attempt"] = context
|
|
3616
3644
|
task["last_agent"] = agent
|
|
3617
3645
|
write_task(root, task_id, task)
|
|
@@ -3888,19 +3916,9 @@ def finalize_quality_attempt(
|
|
|
3888
3916
|
latest_failure_records: dict[tuple[str, str], dict] = {}
|
|
3889
3917
|
for record in [*current_reviews, *current_verifications]:
|
|
3890
3918
|
owner = str(record.get("source_task_id")) if canonical else task_id
|
|
3891
|
-
if record.get("type")
|
|
3892
|
-
record.get("dimension")
|
|
3893
|
-
):
|
|
3894
|
-
label = f"review:{record['dimension']}"
|
|
3895
|
-
elif record.get("type") == "verify" and is_non_empty_string(
|
|
3896
|
-
record.get("check")
|
|
3897
|
-
):
|
|
3898
|
-
coverage_scope = str(record.get("coverage_scope") or "")
|
|
3899
|
-
label = f"verify:{record['check']}"
|
|
3900
|
-
if coverage_scope:
|
|
3901
|
-
label = f"{label}:{coverage_scope}"
|
|
3902
|
-
else:
|
|
3919
|
+
if record.get("type") not in {"review", "verify"}:
|
|
3903
3920
|
continue
|
|
3921
|
+
label = failure_label(record)
|
|
3904
3922
|
latest_failure_records[(owner, label)] = record
|
|
3905
3923
|
evidence_failure_classes: set[str] = set()
|
|
3906
3924
|
for owner, labels in failures.items():
|
|
@@ -4212,12 +4230,10 @@ def verification_contract_fingerprint(root: Path, task_id: str, task: dict) -> s
|
|
|
4212
4230
|
source = task.get("spec_source") if isinstance(task.get("spec_source"), dict) else {}
|
|
4213
4231
|
contract = {
|
|
4214
4232
|
"workflow_mode": task.get("workflow_mode"),
|
|
4215
|
-
|
|
4216
|
-
"tdd_coverage_threshold": task.get("tdd_coverage_threshold"),
|
|
4217
|
-
"tdd_baselines": task.get("tdd_baselines"),
|
|
4233
|
+
**unit_test_contract(task),
|
|
4218
4234
|
**({"tdd_infrastructure": tdd_infrastructure_fingerprint(
|
|
4219
4235
|
{root.resolve(), *task_repository_roots(root, task, plan)}
|
|
4220
|
-
)} if task.get("
|
|
4236
|
+
)} if task.get("unit_test_mode") in {"ut", "tdd"} else {}),
|
|
4221
4237
|
"plan": plan,
|
|
4222
4238
|
"canonical": {
|
|
4223
4239
|
"schema": source.get("schema"),
|
|
@@ -4981,8 +4997,10 @@ def validate_spec_implementation_results(root: Path, task_id: str, task: dict) -
|
|
|
4981
4997
|
default=-1,
|
|
4982
4998
|
)
|
|
4983
4999
|
lifecycle_by_unit: dict[str, list[dict]] = {unit_id: [] for unit_id in unit_by_id}
|
|
4984
|
-
for record in records
|
|
5000
|
+
for index, record in enumerate(records):
|
|
4985
5001
|
unit_id = str(record.get("unit_id") or "")
|
|
5002
|
+
if index <= latest_plan_index and unit_id in task.get("correction", {}).get("unit_ids", list(unit_by_id)):
|
|
5003
|
+
continue
|
|
4986
5004
|
if record.get("type") in {"dispatch", "result"} and unit_id in unit_by_id:
|
|
4987
5005
|
lifecycle_by_unit[unit_id].append(record)
|
|
4988
5006
|
missing_dispatches = sorted(
|
|
@@ -5065,7 +5083,7 @@ def validate_review_readiness(
|
|
|
5065
5083
|
):
|
|
5066
5084
|
dimension = str(record["dimension"])
|
|
5067
5085
|
source_task_id = str(record.get("source_task_id") or "")
|
|
5068
|
-
record_key =
|
|
5086
|
+
record_key = gate_identity(record)
|
|
5069
5087
|
latest_by_dimension[record_key] = record
|
|
5070
5088
|
if not latest_by_dimension:
|
|
5071
5089
|
raise StateError(
|
|
@@ -5121,6 +5139,9 @@ def validate_review_readiness(
|
|
|
5121
5139
|
"Canonical Spec review evidence does not cover selected source tasks: "
|
|
5122
5140
|
+ ", ".join(missing_review_tasks)
|
|
5123
5141
|
)
|
|
5142
|
+
require_unit_evidence(latest_execution_plan(root, task_id) or {},
|
|
5143
|
+
list(latest_by_dimension.values()), "Review",
|
|
5144
|
+
2 if task.get("workflow_mode") == "strict" else 1)
|
|
5124
5145
|
has_failed_dimension = False
|
|
5125
5146
|
for record in latest_by_dimension.values():
|
|
5126
5147
|
findings = record.get("findings")
|
|
@@ -5136,7 +5157,7 @@ def validate_review_readiness(
|
|
|
5136
5157
|
raise StateError(
|
|
5137
5158
|
"QUALITY cannot advance while a review dimension is not passed or has error findings."
|
|
5138
5159
|
)
|
|
5139
|
-
if task.get("
|
|
5160
|
+
if task.get("unit_test_mode") == "tdd":
|
|
5140
5161
|
if is_spec_task:
|
|
5141
5162
|
missing_tdd_reviews = sorted(
|
|
5142
5163
|
source_task_id
|
|
@@ -5167,7 +5188,7 @@ def validate_review_readiness(
|
|
|
5167
5188
|
"Strict Canonical Spec review requires at least two passed dimensions for "
|
|
5168
5189
|
"every selected source task: " + ", ".join(missing_strict_dimensions)
|
|
5169
5190
|
)
|
|
5170
|
-
elif len(latest_by_dimension) < 2:
|
|
5191
|
+
elif len({record["dimension"] for record in latest_by_dimension.values()}) < 2:
|
|
5171
5192
|
raise StateError(
|
|
5172
5193
|
"Strict workflow requires at least two passed review dimensions for the current implementation fingerprint."
|
|
5173
5194
|
)
|
|
@@ -5208,17 +5229,13 @@ def validate_verification_readiness(
|
|
|
5208
5229
|
and is_non_empty_string(record.get("check"))
|
|
5209
5230
|
):
|
|
5210
5231
|
if (
|
|
5211
|
-
task.get("
|
|
5232
|
+
task.get("unit_test_mode") in {"ut", "tdd"}
|
|
5212
5233
|
and record.get("check_type") == "coverage"
|
|
5213
5234
|
and record.get("coverage_scope") == "gitlab"
|
|
5214
5235
|
):
|
|
5215
5236
|
# 远程 CI 只作为生成的自动化能力,历史 pending/failed 记录不再参与本地验收。
|
|
5216
5237
|
continue
|
|
5217
|
-
check =
|
|
5218
|
-
if task.get("tdd_enabled") is True and record.get("check_type") == "coverage":
|
|
5219
|
-
check = f"{check}\0{record.get('coverage_scope') or ''}"
|
|
5220
|
-
if is_spec_task:
|
|
5221
|
-
check = f"{check}\0{record.get('source_task_id') or ''}"
|
|
5238
|
+
check = gate_identity(record)
|
|
5222
5239
|
previous = latest_by_check.get(check)
|
|
5223
5240
|
if (
|
|
5224
5241
|
record.get("applicable") is False
|
|
@@ -5273,6 +5290,7 @@ def validate_verification_readiness(
|
|
|
5273
5290
|
applicable_records = [
|
|
5274
5291
|
record for record in latest_by_check.values() if record.get("applicable") is not False
|
|
5275
5292
|
]
|
|
5293
|
+
require_unit_evidence(latest_execution_plan(root, task_id) or {}, applicable_records, "Verification")
|
|
5276
5294
|
if not applicable_records:
|
|
5277
5295
|
raise StateError(
|
|
5278
5296
|
"QUALITY cannot advance to MEMORY without at least one applicable executed check."
|
|
@@ -5314,13 +5332,13 @@ def validate_verification_readiness(
|
|
|
5314
5332
|
"TDD initialization cannot advance to MEMORY until readiness passes: "
|
|
5315
5333
|
+ "; ".join(str(reason) for reason in readiness["reasons"])
|
|
5316
5334
|
)
|
|
5317
|
-
if task.get("
|
|
5335
|
+
if task.get("unit_test_mode") not in {"ut", "tdd"} and any(
|
|
5318
5336
|
record.get("check_type") == "coverage" for record in latest_by_check.values()
|
|
5319
5337
|
):
|
|
5320
5338
|
raise StateError(
|
|
5321
|
-
"Coverage verification evidence is not allowed when the frozen
|
|
5339
|
+
"Coverage verification evidence is not allowed when the frozen unit test mode is none."
|
|
5322
5340
|
)
|
|
5323
|
-
if task.get("
|
|
5341
|
+
if task.get("unit_test_mode") in {"ut", "tdd"}:
|
|
5324
5342
|
require_tdd_readiness(root)
|
|
5325
5343
|
test_records = [
|
|
5326
5344
|
record
|
|
@@ -5335,7 +5353,7 @@ def validate_verification_readiness(
|
|
|
5335
5353
|
]
|
|
5336
5354
|
if not coverage_records:
|
|
5337
5355
|
raise StateError(
|
|
5338
|
-
"
|
|
5356
|
+
"Unit test verification requires changed-production-line JaCoCo coverage evidence."
|
|
5339
5357
|
)
|
|
5340
5358
|
if is_spec_task:
|
|
5341
5359
|
tested_source_tasks = {
|
|
@@ -5346,7 +5364,7 @@ def validate_verification_readiness(
|
|
|
5346
5364
|
)
|
|
5347
5365
|
if missing_test_tasks:
|
|
5348
5366
|
raise StateError(
|
|
5349
|
-
"
|
|
5367
|
+
"Unit test Canonical verification requires local unit-test evidence for every selected source task: "
|
|
5350
5368
|
+ ", ".join(missing_test_tasks)
|
|
5351
5369
|
)
|
|
5352
5370
|
covered_source_tasks = {
|
|
@@ -5357,33 +5375,33 @@ def validate_verification_readiness(
|
|
|
5357
5375
|
)
|
|
5358
5376
|
if missing_coverage_tasks:
|
|
5359
5377
|
raise StateError(
|
|
5360
|
-
"
|
|
5378
|
+
"Unit test Canonical verification requires separate coverage evidence for every selected source task: "
|
|
5361
5379
|
+ ", ".join(missing_coverage_tasks)
|
|
5362
5380
|
)
|
|
5363
5381
|
elif not test_records:
|
|
5364
5382
|
raise StateError(
|
|
5365
|
-
"
|
|
5383
|
+
"Unit test verification requires passed local unit-test evidence."
|
|
5366
5384
|
)
|
|
5367
5385
|
for record in coverage_records:
|
|
5368
5386
|
scope = str(record.get("coverage_scope") or "")
|
|
5369
5387
|
if scope != "local":
|
|
5370
5388
|
raise StateError(
|
|
5371
|
-
"
|
|
5389
|
+
"Unit test coverage evidence must identify coverage_scope as local."
|
|
5372
5390
|
)
|
|
5373
|
-
expected_threshold = task.get("
|
|
5391
|
+
expected_threshold = task.get("ut_coverage_threshold")
|
|
5374
5392
|
expected_baselines = task.get("tdd_baselines")
|
|
5375
5393
|
if (
|
|
5376
5394
|
type(expected_threshold) is not int
|
|
5377
5395
|
or expected_threshold < 1
|
|
5378
5396
|
or expected_threshold > 100
|
|
5379
5397
|
):
|
|
5380
|
-
raise StateError("
|
|
5398
|
+
raise StateError("Unit test task is missing a valid frozen coverage threshold.")
|
|
5381
5399
|
if not isinstance(expected_baselines, dict) or not expected_baselines:
|
|
5382
|
-
raise StateError("
|
|
5400
|
+
raise StateError("Unit test task is missing frozen Git baselines.")
|
|
5383
5401
|
for record in coverage_records:
|
|
5384
5402
|
coverage = record.get("coverage")
|
|
5385
5403
|
if not isinstance(coverage, dict):
|
|
5386
|
-
raise StateError("
|
|
5404
|
+
raise StateError("Unit test coverage evidence must include the coverage result object.")
|
|
5387
5405
|
total = coverage.get("total_lines")
|
|
5388
5406
|
covered = coverage.get("covered_lines")
|
|
5389
5407
|
percentage = coverage.get("percentage")
|
|
@@ -5419,7 +5437,7 @@ def validate_verification_readiness(
|
|
|
5419
5437
|
)
|
|
5420
5438
|
):
|
|
5421
5439
|
raise StateError(
|
|
5422
|
-
"
|
|
5440
|
+
"Unit test coverage evidence must preserve the exact gate command, baseline, counts, percentage, frozen threshold, reports, and report fingerprint."
|
|
5423
5441
|
)
|
|
5424
5442
|
if total == 0:
|
|
5425
5443
|
if record.get("applicable") is not False or record.get("passed") is not True:
|
|
@@ -5428,7 +5446,7 @@ def validate_verification_readiness(
|
|
|
5428
5446
|
)
|
|
5429
5447
|
elif abs(percentage - round(covered * 100.0 / total, 2)) > 0.01:
|
|
5430
5448
|
raise StateError(
|
|
5431
|
-
"
|
|
5449
|
+
"Unit test coverage evidence percentage does not match covered/total counts."
|
|
5432
5450
|
)
|
|
5433
5451
|
elif (
|
|
5434
5452
|
record.get("applicable") is False
|
|
@@ -5436,7 +5454,7 @@ def validate_verification_readiness(
|
|
|
5436
5454
|
or percentage < threshold
|
|
5437
5455
|
):
|
|
5438
5456
|
raise StateError(
|
|
5439
|
-
f"
|
|
5457
|
+
f"Unit test changed-line coverage must meet the frozen {threshold}% threshold."
|
|
5440
5458
|
)
|
|
5441
5459
|
if task.get("workflow_mode") == "strict":
|
|
5442
5460
|
if is_spec_task:
|
|
@@ -5506,7 +5524,10 @@ def validate_verification_readiness(
|
|
|
5506
5524
|
for record in applicable_records
|
|
5507
5525
|
if is_non_empty_string(record.get("command"))
|
|
5508
5526
|
}
|
|
5509
|
-
missing_commands = sorted(required_test_commands
|
|
5527
|
+
missing_commands = sorted(required for required in required_test_commands if not any(
|
|
5528
|
+
actual[:2] == required[:2] and command_covers(actual[2], required[2])
|
|
5529
|
+
for actual in executed_commands
|
|
5530
|
+
))
|
|
5510
5531
|
if missing_commands:
|
|
5511
5532
|
raise StateError(
|
|
5512
5533
|
"Canonical Spec verification is missing source test commands: "
|
|
@@ -5575,7 +5596,7 @@ def quality_repair_failures_for_window(
|
|
|
5575
5596
|
and is_non_empty_string(unit.get("source_task_id"))
|
|
5576
5597
|
and is_non_empty_string(unit.get("repo_id"))
|
|
5577
5598
|
}
|
|
5578
|
-
fingerprints = evidence_fingerprints(root, task_id)
|
|
5599
|
+
fingerprints = evidence_fingerprints(root, task_id) if not (implementation_fingerprint_value and config_fingerprint_value) else {}
|
|
5579
5600
|
implementation = (
|
|
5580
5601
|
implementation_fingerprint_value or fingerprints["implementation_fingerprint"]
|
|
5581
5602
|
)
|
|
@@ -5640,12 +5661,12 @@ def quality_repair_failures_for_window(
|
|
|
5640
5661
|
)
|
|
5641
5662
|
):
|
|
5642
5663
|
owner = source_task_id if canonical else task_id
|
|
5643
|
-
latest_reviews[(owner,
|
|
5664
|
+
latest_reviews[(owner, *gate_identity(record)[1:])] = record
|
|
5644
5665
|
elif record_type == "verify" and record.get(
|
|
5645
5666
|
"implementation_fingerprint"
|
|
5646
5667
|
) == implementation and record.get("config_fingerprint") == config:
|
|
5647
5668
|
if (
|
|
5648
|
-
task.get("
|
|
5669
|
+
task.get("unit_test_mode") in {"ut", "tdd"}
|
|
5649
5670
|
and record.get("check_type") == "coverage"
|
|
5650
5671
|
and record.get("coverage_scope") == "gitlab"
|
|
5651
5672
|
):
|
|
@@ -5669,16 +5690,10 @@ def quality_repair_failures_for_window(
|
|
|
5669
5690
|
):
|
|
5670
5691
|
continue
|
|
5671
5692
|
owner = source_task_id if canonical else task_id
|
|
5672
|
-
latest_verifications[
|
|
5673
|
-
(
|
|
5674
|
-
owner,
|
|
5675
|
-
str(record["check"]),
|
|
5676
|
-
str(record.get("coverage_scope") or ""),
|
|
5677
|
-
)
|
|
5678
|
-
] = record
|
|
5693
|
+
latest_verifications[(owner, *gate_identity(record)[1:])] = record
|
|
5679
5694
|
|
|
5680
5695
|
failures: dict[str, list[str]] = {}
|
|
5681
|
-
for (source_task_id, dimension), record in latest_reviews.items():
|
|
5696
|
+
for (source_task_id, unit_id, dimension, _scope), record in latest_reviews.items():
|
|
5682
5697
|
findings = record.get("findings")
|
|
5683
5698
|
has_error = isinstance(findings, list) and any(
|
|
5684
5699
|
isinstance(finding, dict)
|
|
@@ -5686,13 +5701,10 @@ def quality_repair_failures_for_window(
|
|
|
5686
5701
|
for finding in findings
|
|
5687
5702
|
)
|
|
5688
5703
|
if record.get("passed") is not True or has_error:
|
|
5689
|
-
failures.setdefault(source_task_id, []).append(
|
|
5690
|
-
for (source_task_id, check, scope), record in latest_verifications.items():
|
|
5704
|
+
failures.setdefault(source_task_id, []).append(failure_label(record))
|
|
5705
|
+
for (source_task_id, unit_id, check, scope), record in latest_verifications.items():
|
|
5691
5706
|
if record.get("applicable") is not False and record.get("passed") is not True:
|
|
5692
|
-
|
|
5693
|
-
if scope:
|
|
5694
|
-
label = f"{label}:{scope}"
|
|
5695
|
-
failures.setdefault(source_task_id, []).append(label)
|
|
5707
|
+
failures.setdefault(source_task_id, []).append(failure_label(record))
|
|
5696
5708
|
return failures
|
|
5697
5709
|
|
|
5698
5710
|
|
|
@@ -6419,9 +6431,27 @@ def validate_analysis_readiness(
|
|
|
6419
6431
|
test_strategy = task_dir / "test-strategy.md"
|
|
6420
6432
|
reasons: list[str] = []
|
|
6421
6433
|
behavior = resolve_behavior(root, session or default_session())
|
|
6422
|
-
|
|
6434
|
+
unit_test_mode = behavior[8] if task_type != TDD_INIT_TASK_TYPE else "none"
|
|
6423
6435
|
tdd_threshold = behavior[11]
|
|
6424
6436
|
|
|
6437
|
+
if unit_test_mode == "ut":
|
|
6438
|
+
require_tdd_readiness(root)
|
|
6439
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
6440
|
+
if not any(str(file).endswith(".java") for unit in plan.get("units", []) for file in unit.get("files", [])):
|
|
6441
|
+
reasons.append("UT is enabled but the confirmed implementation scope has no Java source")
|
|
6442
|
+
if reasons:
|
|
6443
|
+
raise StateError("; ".join(reasons))
|
|
6444
|
+
|
|
6445
|
+
if dev_spec.is_file() and unit_test_mode != "tdd":
|
|
6446
|
+
compact = dev_spec.read_text(encoding="utf-8")
|
|
6447
|
+
if compact.startswith("<!-- easy-coding:compact -->"):
|
|
6448
|
+
mode, _ = calculate_workflow_floor(root, task_id)
|
|
6449
|
+
if mode != "fast" or not has_valid_execution_plan(root, task_id):
|
|
6450
|
+
raise StateError("Compact analysis requires a valid Fast implementation plan.")
|
|
6451
|
+
if re.findall(r"^decision_status:\s*(\w+)\s*$", compact, re.MULTILINE) != ["closed"]:
|
|
6452
|
+
raise StateError("Compact analysis must record the confirmed scope.")
|
|
6453
|
+
return
|
|
6454
|
+
|
|
6425
6455
|
dev_spec_content = ""
|
|
6426
6456
|
if not dev_spec.exists():
|
|
6427
6457
|
reasons.append("dev-spec.md is missing")
|
|
@@ -6528,7 +6558,7 @@ def validate_analysis_readiness(
|
|
|
6528
6558
|
plan_is_valid = has_valid_execution_plan(root, task_id)
|
|
6529
6559
|
if not plan_is_valid:
|
|
6530
6560
|
reasons.append("execution.jsonl has no valid plan record")
|
|
6531
|
-
if
|
|
6561
|
+
if unit_test_mode == "tdd":
|
|
6532
6562
|
readiness = tdd_readiness(root)
|
|
6533
6563
|
if readiness["status"] != "ready":
|
|
6534
6564
|
reasons.append(
|
|
@@ -6591,6 +6621,8 @@ def validate_analysis_readiness(
|
|
|
6591
6621
|
dev_spec_content, strategy_content, baselines
|
|
6592
6622
|
)
|
|
6593
6623
|
)
|
|
6624
|
+
elif unit_test_mode == "ut":
|
|
6625
|
+
pass
|
|
6594
6626
|
elif task_type == TDD_INIT_TASK_TYPE:
|
|
6595
6627
|
try:
|
|
6596
6628
|
strategy_content = test_strategy.read_text(encoding="utf-8")
|
|
@@ -6940,12 +6972,12 @@ def snapshot_state(
|
|
|
6940
6972
|
project_workflow_mode,
|
|
6941
6973
|
session_workflow_mode,
|
|
6942
6974
|
configured_workflow_mode,
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6975
|
+
project_unit_test_mode,
|
|
6976
|
+
session_unit_test_mode,
|
|
6977
|
+
effective_unit_test_mode,
|
|
6978
|
+
project_ut_coverage_threshold,
|
|
6979
|
+
session_ut_coverage_threshold,
|
|
6980
|
+
effective_ut_coverage_threshold,
|
|
6949
6981
|
) = resolve_behavior(root, resolved_session)
|
|
6950
6982
|
concrete_workflow_mode = None
|
|
6951
6983
|
if task:
|
|
@@ -6953,26 +6985,26 @@ def snapshot_state(
|
|
|
6953
6985
|
proposal = task.get("workflow_mode_proposal")
|
|
6954
6986
|
if concrete_workflow_mode is None and isinstance(proposal, dict):
|
|
6955
6987
|
concrete_workflow_mode = proposal.get("selected_mode")
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6988
|
+
task_unit_test_mode = task.get("unit_test_mode") if task else None
|
|
6989
|
+
task_ut_coverage_threshold = task.get("ut_coverage_threshold") if task else None
|
|
6990
|
+
frozen_unit_test = bool(
|
|
6959
6991
|
task
|
|
6960
6992
|
and status not in {"ANALYSIS", "INIT"}
|
|
6961
|
-
and
|
|
6993
|
+
and task_unit_test_mode in {"none", "ut", "tdd"}
|
|
6962
6994
|
)
|
|
6963
6995
|
is_tdd_init = bool(
|
|
6964
6996
|
task and str(task.get("type") or "").strip().lower() == TDD_INIT_TASK_TYPE
|
|
6965
6997
|
)
|
|
6966
|
-
|
|
6967
|
-
|
|
6998
|
+
displayed_unit_test_mode = (
|
|
6999
|
+
"none" if is_tdd_init else task_unit_test_mode if frozen_unit_test else effective_unit_test_mode
|
|
6968
7000
|
)
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
if
|
|
6972
|
-
else
|
|
7001
|
+
displayed_ut_threshold = (
|
|
7002
|
+
task_ut_coverage_threshold
|
|
7003
|
+
if frozen_unit_test and isinstance(task_ut_coverage_threshold, int)
|
|
7004
|
+
else effective_ut_coverage_threshold
|
|
6973
7005
|
)
|
|
6974
7006
|
should_check_readiness = bool(
|
|
6975
|
-
|
|
7007
|
+
effective_unit_test_mode in {"ut", "tdd"} or task_unit_test_mode in {"ut", "tdd"} or is_tdd_init
|
|
6976
7008
|
)
|
|
6977
7009
|
readiness = (
|
|
6978
7010
|
tdd_readiness(root)
|
|
@@ -6999,19 +7031,19 @@ def snapshot_state(
|
|
|
6999
7031
|
"session_workflow_mode": session_workflow_mode,
|
|
7000
7032
|
"configured_workflow_mode": configured_workflow_mode,
|
|
7001
7033
|
"concrete_workflow_mode": concrete_workflow_mode,
|
|
7002
|
-
"
|
|
7003
|
-
"
|
|
7004
|
-
"
|
|
7005
|
-
"
|
|
7006
|
-
"
|
|
7007
|
-
"
|
|
7008
|
-
"
|
|
7009
|
-
"
|
|
7034
|
+
"project_unit_test_mode": project_unit_test_mode,
|
|
7035
|
+
"session_unit_test_mode": session_unit_test_mode,
|
|
7036
|
+
"effective_unit_test_mode": effective_unit_test_mode,
|
|
7037
|
+
"project_ut_coverage_threshold": project_ut_coverage_threshold,
|
|
7038
|
+
"session_ut_coverage_threshold": session_ut_coverage_threshold,
|
|
7039
|
+
"effective_ut_coverage_threshold": effective_ut_coverage_threshold,
|
|
7040
|
+
"task_unit_test_mode": task_unit_test_mode,
|
|
7041
|
+
"task_ut_coverage_threshold": task_ut_coverage_threshold,
|
|
7010
7042
|
"task_tdd_baselines": task.get("tdd_baselines") if task else None,
|
|
7011
|
-
"
|
|
7012
|
-
"
|
|
7013
|
-
"
|
|
7014
|
-
"
|
|
7043
|
+
"displayed_unit_test_mode": displayed_unit_test_mode,
|
|
7044
|
+
"displayed_ut_coverage_threshold": displayed_ut_threshold,
|
|
7045
|
+
"unit_test_readiness_status": readiness["status"],
|
|
7046
|
+
"unit_test_readiness_reasons": readiness["reasons"],
|
|
7015
7047
|
"spec_summary": spec_task_summary(task),
|
|
7016
7048
|
# Compatibility output aliases for pre-0.9 clients.
|
|
7017
7049
|
"project_confirm_mode": project_approval_mode,
|
|
@@ -7044,8 +7076,8 @@ def build_status_line(
|
|
|
7044
7076
|
approval = str(state["effective_approval_mode"]).capitalize()
|
|
7045
7077
|
workflow = str(state["concrete_workflow_mode"] or state["configured_workflow_mode"]).capitalize()
|
|
7046
7078
|
status_brand = f"> **Easy Coding** · **Approval: {approval}** · **Workflow: {workflow}**"
|
|
7047
|
-
if state["
|
|
7048
|
-
status_brand += " · **
|
|
7079
|
+
if state["displayed_unit_test_mode"] in {"ut", "tdd"}:
|
|
7080
|
+
status_brand += f" · **{state['displayed_unit_test_mode'].upper()}**"
|
|
7049
7081
|
task_id = state["current_task"]
|
|
7050
7082
|
if task_id:
|
|
7051
7083
|
status = str(state["status"])
|
|
@@ -7090,10 +7122,10 @@ def build_machine_breadcrumbs(
|
|
|
7090
7122
|
]
|
|
7091
7123
|
if state.get("concrete_workflow_mode"):
|
|
7092
7124
|
lines.append(f"[easy-coding:workflow-mode:{state['concrete_workflow_mode']}]")
|
|
7093
|
-
if state.get("
|
|
7094
|
-
lines.append("[easy-coding:
|
|
7125
|
+
if state.get("displayed_unit_test_mode") in {"ut", "tdd"}:
|
|
7126
|
+
lines.append(f"[easy-coding:unit-test-mode:{state['displayed_unit_test_mode']}]")
|
|
7095
7127
|
lines.append(
|
|
7096
|
-
f"[easy-coding:
|
|
7128
|
+
f"[easy-coding:ut-coverage-threshold:{state['displayed_ut_coverage_threshold']}]"
|
|
7097
7129
|
)
|
|
7098
7130
|
|
|
7099
7131
|
if task_id:
|
|
@@ -7423,42 +7455,43 @@ def clear_session_workflow_mode(
|
|
|
7423
7455
|
return snapshot
|
|
7424
7456
|
|
|
7425
7457
|
|
|
7426
|
-
def
|
|
7458
|
+
def set_session_unit_test_mode(
|
|
7427
7459
|
root: Path,
|
|
7428
|
-
|
|
7460
|
+
mode: str,
|
|
7429
7461
|
agent: str,
|
|
7430
7462
|
threshold: int | None = None,
|
|
7431
7463
|
session_file: str | Path | None = None,
|
|
7432
7464
|
) -> dict:
|
|
7433
|
-
|
|
7465
|
+
mode = parse_unit_test_mode(mode, "session unit_test_mode")
|
|
7466
|
+
if mode != "none":
|
|
7434
7467
|
require_tdd_readiness(root)
|
|
7435
7468
|
session = ensure_session(root, session_file)
|
|
7436
7469
|
materialize_legacy_session_behavior(session)
|
|
7437
|
-
session["
|
|
7470
|
+
session["unit_test_mode"] = mode
|
|
7438
7471
|
if threshold is not None:
|
|
7439
|
-
session["
|
|
7440
|
-
threshold, "session
|
|
7472
|
+
session["ut_coverage_threshold"] = parse_ut_threshold(
|
|
7473
|
+
threshold, "session ut_coverage_threshold"
|
|
7441
7474
|
)
|
|
7442
7475
|
session["last_agent"] = agent
|
|
7443
7476
|
write_session(root, session, session_file)
|
|
7444
7477
|
snapshot = snapshot_state(root, session_file, session)
|
|
7445
|
-
snapshot["action"] = "set-
|
|
7478
|
+
snapshot["action"] = "set-unit-test-mode"
|
|
7446
7479
|
return snapshot
|
|
7447
7480
|
|
|
7448
7481
|
|
|
7449
|
-
def
|
|
7482
|
+
def clear_session_unit_test_mode(
|
|
7450
7483
|
root: Path,
|
|
7451
7484
|
agent: str,
|
|
7452
7485
|
session_file: str | Path | None = None,
|
|
7453
7486
|
) -> dict:
|
|
7454
7487
|
session = ensure_session(root, session_file)
|
|
7455
7488
|
materialize_legacy_session_behavior(session)
|
|
7456
|
-
session.pop("
|
|
7457
|
-
session.pop("
|
|
7489
|
+
session.pop("unit_test_mode", None)
|
|
7490
|
+
session.pop("ut_coverage_threshold", None)
|
|
7458
7491
|
session["last_agent"] = agent
|
|
7459
7492
|
write_session(root, session, session_file)
|
|
7460
7493
|
snapshot = snapshot_state(root, session_file, session)
|
|
7461
|
-
snapshot["action"] = "clear-
|
|
7494
|
+
snapshot["action"] = "clear-unit-test-mode"
|
|
7462
7495
|
return snapshot
|
|
7463
7496
|
|
|
7464
7497
|
|
|
@@ -8592,7 +8625,8 @@ def reconcile_local_result_evidence(
|
|
|
8592
8625
|
and check.get("passed") is True
|
|
8593
8626
|
and is_non_empty_string(check.get("command"))
|
|
8594
8627
|
}
|
|
8595
|
-
missing_unit_commands = sorted(
|
|
8628
|
+
missing_unit_commands = sorted(command for command in unit.get("test_commands", [])
|
|
8629
|
+
if not any(command_covers(actual, command) for actual in passed_commands))
|
|
8596
8630
|
if missing_unit_commands:
|
|
8597
8631
|
unresolved.append(
|
|
8598
8632
|
f"{unit_id}:missing-passed-command=" + ",".join(missing_unit_commands)
|
|
@@ -8630,7 +8664,7 @@ def reconcile_local_result_evidence(
|
|
|
8630
8664
|
missing_commands = [
|
|
8631
8665
|
str(test.get("command"))
|
|
8632
8666
|
for test in tests
|
|
8633
|
-
if str(test.get("command"))
|
|
8667
|
+
if not any(command_covers(actual, str(test.get("command"))) for actual in passed_commands)
|
|
8634
8668
|
]
|
|
8635
8669
|
if missing_commands:
|
|
8636
8670
|
unresolved.append(
|
|
@@ -9035,23 +9069,29 @@ def sync_spec_design_state(
|
|
|
9035
9069
|
progress.pop("pending_action", None)
|
|
9036
9070
|
task.pop("spec_change", None)
|
|
9037
9071
|
task.pop("spec_context", None)
|
|
9038
|
-
if task.get("status") not in {"INIT", "ANALYSIS"}:
|
|
9039
|
-
cleanup_verification_checkpoint(root, resolved_task_id, task)
|
|
9040
|
-
task["status"] = "ANALYSIS"
|
|
9041
|
-
append_stage_history(task, "ANALYSIS", agent)
|
|
9042
|
-
task.pop("pending_transition", None)
|
|
9043
|
-
task["last_agent"] = agent
|
|
9044
9072
|
already_acknowledged = any(
|
|
9045
9073
|
record.get("type") == "spec-design-sync"
|
|
9046
9074
|
and record.get("idempotency_key") == idempotency_key
|
|
9047
9075
|
for record in execution_records(root, resolved_task_id)
|
|
9048
9076
|
)
|
|
9077
|
+
if task.get("correction") and not already_acknowledged:
|
|
9078
|
+
refresh_correction_plan(root, resolved_task_id, task, inspection)
|
|
9079
|
+
if task.get("correction") and not already_acknowledged:
|
|
9080
|
+
task["status"] = "IMPLEMENT"
|
|
9081
|
+
append_stage_history(task, "IMPLEMENT", agent)
|
|
9082
|
+
if not task.get("correction") and task.get("status") not in {"INIT", "ANALYSIS"}:
|
|
9083
|
+
cleanup_verification_checkpoint(root, resolved_task_id, task)
|
|
9084
|
+
task["status"] = "ANALYSIS"
|
|
9085
|
+
append_stage_history(task, "ANALYSIS", agent)
|
|
9086
|
+
task.pop("pending_transition", None)
|
|
9087
|
+
task["last_agent"] = agent
|
|
9049
9088
|
if not already_acknowledged:
|
|
9050
9089
|
append_execution_record(
|
|
9051
9090
|
root,
|
|
9052
9091
|
resolved_task_id,
|
|
9053
9092
|
{
|
|
9054
9093
|
"type": "spec-design-sync",
|
|
9094
|
+
"preserve_plan": bool(task.get("correction")),
|
|
9055
9095
|
"affected_task_ids": requested_task_ids,
|
|
9056
9096
|
"event_id": event["event_id"],
|
|
9057
9097
|
"design_sha256": details["design_sha256"],
|
|
@@ -9061,6 +9101,10 @@ def sync_spec_design_state(
|
|
|
9061
9101
|
},
|
|
9062
9102
|
)
|
|
9063
9103
|
write_task(root, resolved_task_id, task)
|
|
9104
|
+
if task.get("correction"):
|
|
9105
|
+
writeback_ready_tasks_for_implement(root, resolved_task_id, task, agent,
|
|
9106
|
+
source_task_ids=set(requested_task_ids),
|
|
9107
|
+
restart_statuses={"not_started"})
|
|
9064
9108
|
snapshot = snapshot_state(root, session_file, session)
|
|
9065
9109
|
snapshot["action"] = "sync-spec-design"
|
|
9066
9110
|
return snapshot
|
|
@@ -9297,7 +9341,7 @@ def writeback_verified_tasks(
|
|
|
9297
9341
|
if record.get("passed") is True
|
|
9298
9342
|
and str(record.get("source_task_id") or "") == source_task_id
|
|
9299
9343
|
and str(record.get("repo_id") or "") == repo_id
|
|
9300
|
-
and str(record.get("command") or "")
|
|
9344
|
+
and command_covers(str(record.get("command") or ""), command)
|
|
9301
9345
|
),
|
|
9302
9346
|
None,
|
|
9303
9347
|
)
|
|
@@ -9569,52 +9613,16 @@ def resolve_current_task(
|
|
|
9569
9613
|
|
|
9570
9614
|
|
|
9571
9615
|
def validate_workflow_mode_proposal(
|
|
9572
|
-
root: Path,
|
|
9573
|
-
session: dict,
|
|
9574
|
-
proposal: object,
|
|
9575
|
-
task_id: str | None = None,
|
|
9616
|
+
root: Path, session: dict, proposal: object, task_id: str | None = None,
|
|
9576
9617
|
) -> dict:
|
|
9577
|
-
if
|
|
9578
|
-
raise StateError("
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
if configured != effective_configured:
|
|
9586
|
-
raise StateError(
|
|
9587
|
-
"Workflow proposal configured_mode no longer matches the effective project/session setting."
|
|
9588
|
-
)
|
|
9589
|
-
if configured not in CONFIGURED_WORKFLOW_MODES:
|
|
9590
|
-
raise StateError("Invalid configured workflow mode.")
|
|
9591
|
-
if selected not in WORKFLOW_MODES or minimum not in WORKFLOW_MODES:
|
|
9592
|
-
raise StateError("selected_mode and minimum_mode must be fast, standard, or strict.")
|
|
9593
|
-
if source not in {"project", "session", "adaptive", "user", "migration"}:
|
|
9594
|
-
raise StateError("Invalid workflow proposal source.")
|
|
9595
|
-
if not is_string_list(reasons, allow_empty=False):
|
|
9596
|
-
raise StateError("Workflow proposal reasons must contain at least one non-empty reason.")
|
|
9597
|
-
required_rank = WORKFLOW_MODE_RANK[minimum]
|
|
9598
|
-
if configured in WORKFLOW_MODES and WORKFLOW_MODE_RANK[minimum] < WORKFLOW_MODE_RANK[configured]:
|
|
9599
|
-
raise StateError(
|
|
9600
|
-
f"Workflow minimum {minimum} is below configured floor {configured}."
|
|
9601
|
-
)
|
|
9602
|
-
if task_id:
|
|
9603
|
-
calculated_minimum, calculated_reasons = calculate_workflow_floor(root, task_id)
|
|
9604
|
-
calculated_rank = WORKFLOW_MODE_RANK[calculated_minimum]
|
|
9605
|
-
if WORKFLOW_MODE_RANK[minimum] < calculated_rank:
|
|
9606
|
-
raise StateError(
|
|
9607
|
-
f"Workflow minimum {minimum} is below calculated floor {calculated_minimum}: "
|
|
9608
|
-
+ ", ".join(calculated_reasons)
|
|
9609
|
-
)
|
|
9610
|
-
required_rank = max(required_rank, calculated_rank)
|
|
9611
|
-
if configured in WORKFLOW_MODES:
|
|
9612
|
-
required_rank = max(required_rank, WORKFLOW_MODE_RANK[configured])
|
|
9613
|
-
if WORKFLOW_MODE_RANK[selected] < required_rank:
|
|
9614
|
-
raise StateError(
|
|
9615
|
-
f"Workflow mode {selected} is below the allowed minimum for this task."
|
|
9616
|
-
)
|
|
9617
|
-
return proposal
|
|
9618
|
+
if task_id is None:
|
|
9619
|
+
raise StateError("A task is required to calculate the mechanical workflow mode.")
|
|
9620
|
+
mode, reasons = calculate_workflow_floor(root, task_id)
|
|
9621
|
+
return {
|
|
9622
|
+
"configured_mode": resolve_behavior(root, session)[5],
|
|
9623
|
+
"selected_mode": mode, "minimum_mode": mode, "source": "adaptive",
|
|
9624
|
+
"reasons": reasons,
|
|
9625
|
+
}
|
|
9618
9626
|
|
|
9619
9627
|
|
|
9620
9628
|
def calculate_workflow_floor(root: Path, task_id: str) -> tuple[str, list[str]]:
|
|
@@ -9626,6 +9634,12 @@ def calculate_workflow_floor(root: Path, task_id: str) -> tuple[str, list[str]]:
|
|
|
9626
9634
|
if not plan:
|
|
9627
9635
|
raise StateError("Cannot calculate workflow floor without a valid execution plan.")
|
|
9628
9636
|
units = [unit for unit in plan.get("units", []) if isinstance(unit, dict)]
|
|
9637
|
+
correction = task.get("correction")
|
|
9638
|
+
if isinstance(correction, dict):
|
|
9639
|
+
files = set(correction["files"])
|
|
9640
|
+
units = [{**unit, "files": sorted(set(unit.get("files", [])) & files),
|
|
9641
|
+
"risks": correction.get("risks", []), "contracts": []}
|
|
9642
|
+
for unit in units if unit["id"] in correction["unit_ids"]]
|
|
9629
9643
|
missing_local_baseline = [
|
|
9630
9644
|
str(unit.get("id") or "<unknown>")
|
|
9631
9645
|
for unit in units
|
|
@@ -9642,7 +9656,7 @@ def calculate_workflow_floor(root: Path, task_id: str) -> tuple[str, list[str]]:
|
|
|
9642
9656
|
for file_name in unit.get("files", [])
|
|
9643
9657
|
if is_non_empty_string(file_name)
|
|
9644
9658
|
}
|
|
9645
|
-
repositories = workflow_plan_repository_roots(root, task,
|
|
9659
|
+
repositories = workflow_plan_repository_roots(root, task, {"units": units})
|
|
9646
9660
|
ignored_values = {"none", "no", "n/a", "无", "无风险"}
|
|
9647
9661
|
risk_values = [
|
|
9648
9662
|
str(item)
|
|
@@ -9711,7 +9725,7 @@ def propose_workflow_mode(
|
|
|
9711
9725
|
"proposed_at": now_iso(),
|
|
9712
9726
|
"proposed_by": agent,
|
|
9713
9727
|
}
|
|
9714
|
-
validate_workflow_mode_proposal(root, session, proposal, resolved_task_id)
|
|
9728
|
+
proposal.update(validate_workflow_mode_proposal(root, session, proposal, resolved_task_id))
|
|
9715
9729
|
task["workflow_mode_proposal"] = proposal
|
|
9716
9730
|
task["last_agent"] = agent
|
|
9717
9731
|
write_task(root, resolved_task_id, task)
|
|
@@ -9731,35 +9745,36 @@ def freeze_workflow_mode(
|
|
|
9731
9745
|
task["workflow_mode_confirmed_by"] = agent
|
|
9732
9746
|
|
|
9733
9747
|
|
|
9734
|
-
def
|
|
9748
|
+
def freeze_unit_test_mode(
|
|
9735
9749
|
root: Path, session: dict, task_id: str, task: dict, agent: str
|
|
9736
9750
|
) -> None:
|
|
9737
9751
|
behavior = resolve_behavior(root, session)
|
|
9738
9752
|
task_type = str(task.get("type") or "").strip().lower()
|
|
9739
|
-
task["
|
|
9740
|
-
behavior[8] if task_type != TDD_INIT_TASK_TYPE else
|
|
9753
|
+
task["unit_test_mode"] = (
|
|
9754
|
+
behavior[8] if task_type != TDD_INIT_TASK_TYPE else "none"
|
|
9741
9755
|
)
|
|
9742
|
-
task["
|
|
9743
|
-
if task["
|
|
9756
|
+
task["ut_coverage_threshold"] = behavior[11]
|
|
9757
|
+
if task["unit_test_mode"] in {"ut", "tdd"}:
|
|
9744
9758
|
require_tdd_readiness(root)
|
|
9745
9759
|
plan = latest_execution_plan(root, task_id)
|
|
9746
9760
|
if plan is None:
|
|
9747
|
-
raise StateError("Cannot freeze
|
|
9761
|
+
raise StateError("Cannot freeze unit test baseline without a valid execution plan.")
|
|
9748
9762
|
baselines = {
|
|
9749
9763
|
key: git_head_sha(repository)
|
|
9750
9764
|
for key, repository in tdd_repositories(root, task, plan).items()
|
|
9751
9765
|
}
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9766
|
+
if task["unit_test_mode"] == "tdd":
|
|
9767
|
+
task_dir = task_json_path(root, task_id).parent
|
|
9768
|
+
try:
|
|
9769
|
+
dev_spec_content = (task_dir / "dev-spec.md").read_text(encoding="utf-8")
|
|
9770
|
+
strategy_content = (task_dir / "test-strategy.md").read_text(encoding="utf-8")
|
|
9771
|
+
except OSError as error:
|
|
9772
|
+
raise StateError("Cannot freeze TDD without readable analysis artifacts.") from error
|
|
9773
|
+
marker_reasons = tdd_baseline_marker_reasons(
|
|
9774
|
+
dev_spec_content, strategy_content, baselines
|
|
9775
|
+
)
|
|
9776
|
+
if marker_reasons:
|
|
9777
|
+
raise StateError("; ".join(marker_reasons))
|
|
9763
9778
|
task["tdd_baselines"] = baselines
|
|
9764
9779
|
else:
|
|
9765
9780
|
task.pop("tdd_baselines", None)
|
|
@@ -9784,21 +9799,8 @@ def raise_workflow_mode(
|
|
|
9784
9799
|
)
|
|
9785
9800
|
if stage != "IMPLEMENT":
|
|
9786
9801
|
raise StateError("A frozen workflow mode can only be raised during active execution.")
|
|
9787
|
-
|
|
9788
|
-
if current not in WORKFLOW_MODES or mode not in WORKFLOW_MODES:
|
|
9789
|
-
raise StateError("Workflow mode must be frozen before it can be raised.")
|
|
9790
|
-
if WORKFLOW_MODE_RANK[mode] <= WORKFLOW_MODE_RANK[current]:
|
|
9791
|
-
raise StateError(f"Workflow mode can only be raised above {current}.")
|
|
9802
|
+
mode, _reasons = calculate_workflow_floor(root, resolved_task_id)
|
|
9792
9803
|
task["workflow_mode"] = mode
|
|
9793
|
-
task.setdefault("workflow_mode_escalations", []).append(
|
|
9794
|
-
{
|
|
9795
|
-
"from": current,
|
|
9796
|
-
"to": mode,
|
|
9797
|
-
"reason": reason.strip(),
|
|
9798
|
-
"raised_at": now_iso(),
|
|
9799
|
-
"raised_by": agent,
|
|
9800
|
-
}
|
|
9801
|
-
)
|
|
9802
9804
|
task["last_agent"] = agent
|
|
9803
9805
|
write_task(root, resolved_task_id, task)
|
|
9804
9806
|
snapshot = snapshot_state(root, session_file, session)
|
|
@@ -9932,7 +9934,7 @@ def apply_transition(
|
|
|
9932
9934
|
validate_analysis_readiness(root, resolved_task_id, session)
|
|
9933
9935
|
if task.get("workflow_mode_legacy") is not True:
|
|
9934
9936
|
freeze_workflow_mode(root, session, resolved_task_id, task, agent)
|
|
9935
|
-
|
|
9937
|
+
freeze_unit_test_mode(root, session, resolved_task_id, task, agent)
|
|
9936
9938
|
repair_source_task_ids: set[str] | None = None
|
|
9937
9939
|
quality_exit_outcome: str | None = None
|
|
9938
9940
|
if previous == "QUALITY" and stage in {"IMPLEMENT", "ANALYSIS"}:
|
|
@@ -9987,7 +9989,7 @@ def apply_transition(
|
|
|
9987
9989
|
if (
|
|
9988
9990
|
previous == "QUALITY"
|
|
9989
9991
|
and stage in {"IMPLEMENT", "ANALYSIS"}
|
|
9990
|
-
and quality_exit_outcome in {"repair", "replan"}
|
|
9992
|
+
and quality_exit_outcome in {"repair", "replan", "cancelled"}
|
|
9991
9993
|
):
|
|
9992
9994
|
quality_records = validated_quality_records(root, resolved_task_id)
|
|
9993
9995
|
task["quality_consumed_attempt"] = quality_records[-1][1]["attempt"]
|
|
@@ -10012,6 +10014,11 @@ def apply_transition(
|
|
|
10012
10014
|
task = load_task(root, resolved_task_id) or task
|
|
10013
10015
|
if stage in {"ANALYSIS", "IMPLEMENT", "MEMORY", "COMPLETE", "CLOSED"}:
|
|
10014
10016
|
cleanup_verification_checkpoint(root, resolved_task_id, task)
|
|
10017
|
+
if previous == "IMPLEMENT" and stage == "QUALITY":
|
|
10018
|
+
quality_records = validated_quality_records(root, resolved_task_id)
|
|
10019
|
+
if quality_records:
|
|
10020
|
+
task["quality_consumed_attempt"] = quality_records[-1][1]["attempt"]
|
|
10021
|
+
task.pop("quality_return_required", None)
|
|
10015
10022
|
task.pop("pending_transition", None)
|
|
10016
10023
|
if stage == "MEMORY" and previous != stage:
|
|
10017
10024
|
task["memory_progress"] = {}
|
|
@@ -10501,6 +10508,7 @@ def parse_evidence_args(values: list[str]) -> list[dict]:
|
|
|
10501
10508
|
return evidence
|
|
10502
10509
|
|
|
10503
10510
|
|
|
10511
|
+
@evidence_operation()
|
|
10504
10512
|
def main() -> int:
|
|
10505
10513
|
configure_stdio()
|
|
10506
10514
|
common = argparse.ArgumentParser(add_help=False)
|
|
@@ -10638,13 +10646,13 @@ def main() -> int:
|
|
|
10638
10646
|
clear_workflow_mode_parser = subcommands.add_parser("clear-workflow-mode", parents=[common])
|
|
10639
10647
|
clear_workflow_mode_parser.add_argument("--agent", required=True)
|
|
10640
10648
|
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10649
|
+
set_unit_test_parser = subcommands.add_parser("set-unit-test-mode", parents=[common])
|
|
10650
|
+
set_unit_test_parser.add_argument("--mode", required=True, choices=["none", "ut", "tdd"])
|
|
10651
|
+
set_unit_test_parser.add_argument("--threshold", type=int)
|
|
10652
|
+
set_unit_test_parser.add_argument("--agent", required=True)
|
|
10645
10653
|
|
|
10646
|
-
|
|
10647
|
-
|
|
10654
|
+
clear_unit_test_parser = subcommands.add_parser("clear-unit-test-mode", parents=[common])
|
|
10655
|
+
clear_unit_test_parser.add_argument("--agent", required=True)
|
|
10648
10656
|
|
|
10649
10657
|
# Compatibility aliases for pre-0.9 callers.
|
|
10650
10658
|
set_confirm_mode_parser = subcommands.add_parser("set-confirm-mode", parents=[common])
|
|
@@ -10660,20 +10668,20 @@ def main() -> int:
|
|
|
10660
10668
|
"propose-workflow-mode", parents=[common]
|
|
10661
10669
|
)
|
|
10662
10670
|
propose_workflow_parser.add_argument(
|
|
10663
|
-
"--configured",
|
|
10671
|
+
"--configured", default="adaptive", choices=sorted(CONFIGURED_WORKFLOW_MODES)
|
|
10664
10672
|
)
|
|
10665
10673
|
propose_workflow_parser.add_argument(
|
|
10666
|
-
"--selected",
|
|
10674
|
+
"--selected", default="fast", choices=sorted(WORKFLOW_MODES)
|
|
10667
10675
|
)
|
|
10668
10676
|
propose_workflow_parser.add_argument(
|
|
10669
|
-
"--minimum",
|
|
10677
|
+
"--minimum", default="fast", choices=sorted(WORKFLOW_MODES)
|
|
10670
10678
|
)
|
|
10671
10679
|
propose_workflow_parser.add_argument(
|
|
10672
10680
|
"--source",
|
|
10673
|
-
|
|
10681
|
+
default="adaptive",
|
|
10674
10682
|
choices=["project", "session", "adaptive", "user", "migration"],
|
|
10675
10683
|
)
|
|
10676
|
-
propose_workflow_parser.add_argument("--reason",
|
|
10684
|
+
propose_workflow_parser.add_argument("--reason", action="append", default=[])
|
|
10677
10685
|
propose_workflow_parser.add_argument("--agent", required=True)
|
|
10678
10686
|
propose_workflow_parser.add_argument("--task-id")
|
|
10679
10687
|
|
|
@@ -10691,6 +10699,22 @@ def main() -> int:
|
|
|
10691
10699
|
fingerprints_parser.add_argument("--agent", required=True)
|
|
10692
10700
|
fingerprints_parser.add_argument("--task-id")
|
|
10693
10701
|
|
|
10702
|
+
for name in ("prepare-check", "record-check"):
|
|
10703
|
+
check_parser = subcommands.add_parser(name, parents=[common])
|
|
10704
|
+
check_parser.add_argument("--agent", required=True)
|
|
10705
|
+
check_parser.add_argument("--task-id")
|
|
10706
|
+
if name == "prepare-check":
|
|
10707
|
+
check_parser.add_argument("--record", required=True)
|
|
10708
|
+
else:
|
|
10709
|
+
check_parser.add_argument("--prepared-id", required=True)
|
|
10710
|
+
check_parser.add_argument("--result", required=True)
|
|
10711
|
+
correction_parser = subcommands.add_parser("begin-correction", parents=[common])
|
|
10712
|
+
correction_parser.add_argument("--file", action="append", required=True)
|
|
10713
|
+
correction_parser.add_argument("--summary", required=True)
|
|
10714
|
+
correction_parser.add_argument("--risk", action="append", default=[])
|
|
10715
|
+
correction_parser.add_argument("--agent", required=True)
|
|
10716
|
+
correction_parser.add_argument("--task-id")
|
|
10717
|
+
|
|
10694
10718
|
finalize_quality_parser = subcommands.add_parser(
|
|
10695
10719
|
"finalize-quality", parents=[common]
|
|
10696
10720
|
)
|
|
@@ -11139,13 +11163,13 @@ def main() -> int:
|
|
|
11139
11163
|
session_file,
|
|
11140
11164
|
)
|
|
11141
11165
|
)
|
|
11142
|
-
elif command == "set-
|
|
11166
|
+
elif command == "set-unit-test-mode":
|
|
11143
11167
|
emit(
|
|
11144
11168
|
attach_status_context(
|
|
11145
11169
|
root,
|
|
11146
|
-
|
|
11170
|
+
set_session_unit_test_mode(
|
|
11147
11171
|
root,
|
|
11148
|
-
args.
|
|
11172
|
+
args.mode,
|
|
11149
11173
|
agent,
|
|
11150
11174
|
args.threshold,
|
|
11151
11175
|
session_file,
|
|
@@ -11154,11 +11178,11 @@ def main() -> int:
|
|
|
11154
11178
|
session_file,
|
|
11155
11179
|
)
|
|
11156
11180
|
)
|
|
11157
|
-
elif command == "clear-
|
|
11181
|
+
elif command == "clear-unit-test-mode":
|
|
11158
11182
|
emit(
|
|
11159
11183
|
attach_status_context(
|
|
11160
11184
|
root,
|
|
11161
|
-
|
|
11185
|
+
clear_session_unit_test_mode(root, agent, session_file),
|
|
11162
11186
|
agent,
|
|
11163
11187
|
session_file,
|
|
11164
11188
|
)
|
|
@@ -11261,6 +11285,16 @@ def main() -> int:
|
|
|
11261
11285
|
session_file,
|
|
11262
11286
|
)
|
|
11263
11287
|
)
|
|
11288
|
+
elif command in {"prepare-check", "record-check", "begin-correction"}:
|
|
11289
|
+
session, task_id, task = resolve_current_task(root, args.task_id, session_file)
|
|
11290
|
+
require_spec_context(root, task, agent, session_file)
|
|
11291
|
+
if command == "prepare-check":
|
|
11292
|
+
result = prepare_check(root, task_id, task, json.loads(args.record), agent)
|
|
11293
|
+
elif command == "record-check":
|
|
11294
|
+
result = record_check(root, task_id, task, args.prepared_id, json.loads(args.result), agent)
|
|
11295
|
+
else:
|
|
11296
|
+
result = begin_correction(root, task_id, task, args.file, args.summary, args.risk, agent)
|
|
11297
|
+
emit(result)
|
|
11264
11298
|
elif command == "finalize-quality":
|
|
11265
11299
|
emit(
|
|
11266
11300
|
attach_status_context(
|
|
@@ -11547,7 +11581,7 @@ def main() -> int:
|
|
|
11547
11581
|
)
|
|
11548
11582
|
)
|
|
11549
11583
|
return 0
|
|
11550
|
-
except (StateError, EasyDevSpecError) as error:
|
|
11584
|
+
except (StateError, EasyDevSpecError, ValueError) as error:
|
|
11551
11585
|
print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)
|
|
11552
11586
|
return 1
|
|
11553
11587
|
finally:
|