easy-coding-harness 1.1.0-beta.0 → 1.1.0-beta.2
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 +21 -0
- package/README.md +23 -13
- package/dist/cli.js +559 -311
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- 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 +34 -43
- package/templates/common/skills/ec-config/SKILL.md +65 -46
- package/templates/common/skills/ec-implementing/SKILL.md +14 -5
- 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 +51 -14
- package/templates/common/skills/ec-task-management/SKILL.md +11 -4
- package/templates/common/skills/ec-tdd-init/SKILL.md +7 -7
- package/templates/common/skills/ec-workflow/SKILL.md +37 -17
- package/templates/main-constraint/AGENTS.md.tpl +37 -19
- package/templates/main-constraint/CLAUDE.md.tpl +37 -19
- package/templates/runtime/tools/easy_coding_java_coverage.py +5 -5
- package/templates/shared-hooks/easy_coding_inputs.py +19 -1
- package/templates/shared-hooks/easy_coding_state.py +536 -220
- package/templates/shared-hooks/inject-subagent-context.py +3 -3
|
@@ -17,7 +17,8 @@ from pathlib import Path
|
|
|
17
17
|
import sys
|
|
18
18
|
|
|
19
19
|
from easy_coding_inputs import (
|
|
20
|
-
evidence_operation, memo, digest, input_spec, capture, changed_inputs,
|
|
20
|
+
evidence_operation, memo, cached_memo, invalidate_memo, digest, input_spec, capture, changed_inputs,
|
|
21
|
+
command_covers, command_identity,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
from easy_dev_spec import (
|
|
@@ -125,8 +126,8 @@ WIDE_WORKFLOW_CONTRACT_PATTERN = re.compile(
|
|
|
125
126
|
)
|
|
126
127
|
DEFAULT_APPROVAL_MODE = "guard"
|
|
127
128
|
DEFAULT_WORKFLOW_MODE = "adaptive"
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
DEFAULT_UNIT_TEST_MODE = "none"
|
|
130
|
+
DEFAULT_UT_COVERAGE_THRESHOLD = 90
|
|
130
131
|
TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1"
|
|
131
132
|
TDD_READINESS_SCOPE = "changed-production-lines"
|
|
132
133
|
TDD_READINESS_PATH = Path(".easy-coding/tdd/readiness.json")
|
|
@@ -458,7 +459,7 @@ def read_memory_config(root: Path) -> dict[str, int]:
|
|
|
458
459
|
return config
|
|
459
460
|
|
|
460
461
|
|
|
461
|
-
def
|
|
462
|
+
def parse_ut_threshold(value: object, source: str) -> int:
|
|
462
463
|
if isinstance(value, bool):
|
|
463
464
|
raise StateError(f"Invalid {source}: expected an integer from 1 to 100.")
|
|
464
465
|
try:
|
|
@@ -470,28 +471,21 @@ def parse_tdd_threshold(value: object, source: str) -> int:
|
|
|
470
471
|
return threshold
|
|
471
472
|
|
|
472
473
|
|
|
473
|
-
def
|
|
474
|
-
if value
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if normalized in {"true", "yes", "on"}:
|
|
478
|
-
return True
|
|
479
|
-
if normalized in {"false", "no", "off"}:
|
|
480
|
-
return False
|
|
481
|
-
raise StateError(f"Invalid {source}: expected true or false.")
|
|
474
|
+
def parse_unit_test_mode(value: object, source: str) -> str:
|
|
475
|
+
if not isinstance(value, str) or value not in {"none", "ut", "tdd"}:
|
|
476
|
+
raise StateError(f"Invalid {source}: expected none, ut, or tdd.")
|
|
477
|
+
return str(value)
|
|
482
478
|
|
|
483
479
|
|
|
484
|
-
def
|
|
485
|
-
|
|
480
|
+
def read_behavior_file(path: Path) -> tuple[dict, int]:
|
|
481
|
+
return memo(("behavior", str(path)), lambda: _read_behavior_file(path))
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _read_behavior_file(path: Path) -> tuple[dict, int]:
|
|
486
485
|
try:
|
|
487
486
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
488
|
-
except
|
|
489
|
-
return
|
|
490
|
-
DEFAULT_APPROVAL_MODE,
|
|
491
|
-
DEFAULT_WORKFLOW_MODE,
|
|
492
|
-
DEFAULT_TDD_ENABLED,
|
|
493
|
-
DEFAULT_TDD_COVERAGE_THRESHOLD,
|
|
494
|
-
)
|
|
487
|
+
except FileNotFoundError:
|
|
488
|
+
return {}, 0
|
|
495
489
|
|
|
496
490
|
in_behavior = False
|
|
497
491
|
behavior_indent = 0
|
|
@@ -503,6 +497,8 @@ def read_project_behavior(root: Path) -> tuple[str, str, bool, int]:
|
|
|
503
497
|
if not stripped:
|
|
504
498
|
continue
|
|
505
499
|
indent = len(without_comment) - len(without_comment.lstrip(" "))
|
|
500
|
+
if stripped.startswith("behavior:") and stripped != "behavior:":
|
|
501
|
+
raise StateError("Behavior configuration must use an indented YAML mapping; write it with easy-coding config.")
|
|
506
502
|
if stripped == "behavior:":
|
|
507
503
|
in_behavior = True
|
|
508
504
|
behavior_indent = indent
|
|
@@ -520,6 +516,11 @@ def read_project_behavior(root: Path) -> tuple[str, str, bool, int]:
|
|
|
520
516
|
key, value = stripped.split(":", 1)
|
|
521
517
|
behavior[key] = value.strip().strip("'\"")
|
|
522
518
|
|
|
519
|
+
return behavior, schema_version
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def read_project_behavior(root: Path) -> tuple[str, str, str, int]:
|
|
523
|
+
behavior, schema_version = read_behavior_file(root / ".easy-coding" / "config.yaml")
|
|
523
524
|
legacy = behavior.get("confirm_mode")
|
|
524
525
|
approval_mode = behavior.get("approval_mode")
|
|
525
526
|
workflow_mode = behavior.get("workflow_mode")
|
|
@@ -542,16 +543,42 @@ def read_project_behavior(root: Path) -> tuple[str, str, bool, int]:
|
|
|
542
543
|
"Invalid behavior.workflow_mode in .easy-coding/config.yaml: "
|
|
543
544
|
"expected adaptive, fast, standard, or strict."
|
|
544
545
|
)
|
|
545
|
-
if schema_version >=
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
546
|
+
if schema_version >= 6:
|
|
547
|
+
unit_test_mode = parse_unit_test_mode(
|
|
548
|
+
behavior.get("unit_test_mode", DEFAULT_UNIT_TEST_MODE), "behavior.unit_test_mode"
|
|
549
|
+
)
|
|
550
|
+
threshold = parse_ut_threshold(
|
|
551
|
+
behavior.get("ut_coverage_threshold", DEFAULT_UT_COVERAGE_THRESHOLD),
|
|
552
|
+
"behavior.ut_coverage_threshold",
|
|
550
553
|
)
|
|
551
554
|
else:
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
+
if schema_version >= 4:
|
|
556
|
+
raise StateError("Run easy-coding upgrade to migrate unit-test settings to schema 6.")
|
|
557
|
+
unit_test_mode = DEFAULT_UNIT_TEST_MODE
|
|
558
|
+
threshold = DEFAULT_UT_COVERAGE_THRESHOLD
|
|
559
|
+
return approval_mode, workflow_mode, unit_test_mode, threshold
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def behavior_layers(root: Path, session: dict) -> dict:
|
|
563
|
+
project, _ = read_behavior_file(root / ".easy-coding" / "config.yaml")
|
|
564
|
+
local, _ = read_behavior_file(Path.home() / ".easy-coding" / "config.yaml")
|
|
565
|
+
defaults = {"approval_mode": DEFAULT_APPROVAL_MODE, "cooperate_mode": "default",
|
|
566
|
+
"unit_test_mode": DEFAULT_UNIT_TEST_MODE,
|
|
567
|
+
"ut_coverage_threshold": DEFAULT_UT_COVERAGE_THRESHOLD}
|
|
568
|
+
layers = {"project": project, "local": local, "session": session}
|
|
569
|
+
result = {}
|
|
570
|
+
for key, default in defaults.items():
|
|
571
|
+
source = next((name for name in ("session", "local", "project")
|
|
572
|
+
if layers[name].get(key) is not None), "default")
|
|
573
|
+
value = layers[source][key] if source != "default" else default
|
|
574
|
+
if key == "ut_coverage_threshold":
|
|
575
|
+
value = parse_ut_threshold(value, f"{source} {key}")
|
|
576
|
+
elif key == "unit_test_mode":
|
|
577
|
+
value = parse_unit_test_mode(value, f"{source} {key}")
|
|
578
|
+
elif value not in (APPROVAL_MODES if key == "approval_mode" else {"default", "dispatch"}):
|
|
579
|
+
raise StateError(f"Invalid {source} {key}: {value}")
|
|
580
|
+
result[key] = {"value": value, "source": source}
|
|
581
|
+
return result
|
|
555
582
|
|
|
556
583
|
|
|
557
584
|
def safe_tdd_report_pattern(value: object) -> bool:
|
|
@@ -723,13 +750,13 @@ def require_tdd_readiness(root: Path) -> None:
|
|
|
723
750
|
|
|
724
751
|
def resolve_behavior(
|
|
725
752
|
root: Path, session: dict
|
|
726
|
-
) -> tuple[str, str | None, str, str, str | None, str,
|
|
727
|
-
project_approval, project_workflow,
|
|
753
|
+
) -> tuple[str, str | None, str, str, str | None, str, str, str | None, str, int, int | None, int]:
|
|
754
|
+
project_approval, project_workflow, project_unit_test, project_threshold = read_project_behavior(root)
|
|
728
755
|
legacy = session.get("confirm_mode")
|
|
729
756
|
session_approval = session.get("approval_mode")
|
|
730
757
|
session_workflow = session.get("workflow_mode")
|
|
731
|
-
|
|
732
|
-
session_threshold = session.get("
|
|
758
|
+
session_unit_test = session.get("unit_test_mode")
|
|
759
|
+
session_threshold = session.get("ut_coverage_threshold")
|
|
733
760
|
if session_approval is None:
|
|
734
761
|
if legacy == "lite":
|
|
735
762
|
session_approval = "guard"
|
|
@@ -748,25 +775,29 @@ def resolve_behavior(
|
|
|
748
775
|
raise StateError(
|
|
749
776
|
"Invalid session workflow_mode: expected adaptive, fast, standard, or strict."
|
|
750
777
|
)
|
|
751
|
-
if
|
|
752
|
-
|
|
778
|
+
if session_unit_test is not None:
|
|
779
|
+
session_unit_test = parse_unit_test_mode(session_unit_test, "session unit_test_mode")
|
|
753
780
|
if session_threshold is not None:
|
|
754
|
-
session_threshold =
|
|
755
|
-
session_threshold, "session
|
|
781
|
+
session_threshold = parse_ut_threshold(
|
|
782
|
+
session_threshold, "session ut_coverage_threshold"
|
|
756
783
|
)
|
|
784
|
+
local, _ = read_behavior_file(Path.home() / ".easy-coding" / "config.yaml")
|
|
785
|
+
effective = behavior_layers(root, session)
|
|
757
786
|
return (
|
|
758
787
|
project_approval,
|
|
759
788
|
str(session_approval) if session_approval else None,
|
|
760
|
-
str(session_approval or project_approval),
|
|
789
|
+
str(session_approval or (effective["approval_mode"]["value"] if "approval_mode" in local else project_approval)),
|
|
761
790
|
project_workflow,
|
|
762
791
|
str(session_workflow) if session_workflow else None,
|
|
763
792
|
str(session_workflow or project_workflow),
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
793
|
+
project_unit_test,
|
|
794
|
+
session_unit_test,
|
|
795
|
+
session_unit_test if session_unit_test is not None else (
|
|
796
|
+
effective["unit_test_mode"]["value"] if "unit_test_mode" in local else project_unit_test),
|
|
767
797
|
project_threshold,
|
|
768
798
|
session_threshold,
|
|
769
|
-
session_threshold if session_threshold is not None else
|
|
799
|
+
session_threshold if session_threshold is not None else (
|
|
800
|
+
effective["ut_coverage_threshold"]["value"] if "ut_coverage_threshold" in local else project_threshold),
|
|
770
801
|
)
|
|
771
802
|
|
|
772
803
|
|
|
@@ -775,7 +806,22 @@ def resolve_approval_mode(root: Path, session: dict) -> tuple[str, str | None, s
|
|
|
775
806
|
return behavior[0], behavior[1], behavior[2]
|
|
776
807
|
|
|
777
808
|
|
|
809
|
+
def migrate_unit_test_settings(record: dict) -> bool:
|
|
810
|
+
changed = False
|
|
811
|
+
if "tdd_enabled" in record:
|
|
812
|
+
enabled = record.pop("tdd_enabled")
|
|
813
|
+
if "unit_test_mode" not in record and isinstance(enabled, bool):
|
|
814
|
+
record["unit_test_mode"] = "tdd" if enabled else "none"
|
|
815
|
+
changed = True
|
|
816
|
+
if "tdd_coverage_threshold" in record:
|
|
817
|
+
threshold = record.pop("tdd_coverage_threshold")
|
|
818
|
+
record.setdefault("ut_coverage_threshold", threshold)
|
|
819
|
+
changed = True
|
|
820
|
+
return changed
|
|
821
|
+
|
|
822
|
+
|
|
778
823
|
def materialize_legacy_session_behavior(session: dict) -> None:
|
|
824
|
+
migrate_unit_test_settings(session)
|
|
779
825
|
legacy = session.get("confirm_mode")
|
|
780
826
|
if legacy == "lite":
|
|
781
827
|
session.setdefault("approval_mode", "guard")
|
|
@@ -1264,7 +1310,7 @@ def normalize_legacy_stage(stage: object) -> object:
|
|
|
1264
1310
|
def normalize_legacy_task(task: dict) -> bool:
|
|
1265
1311
|
"""Normalize legacy task state without touching artifacts outside task.json."""
|
|
1266
1312
|
legacy_status = str(task.get("status") or "")
|
|
1267
|
-
changed =
|
|
1313
|
+
changed = migrate_unit_test_settings(task)
|
|
1268
1314
|
|
|
1269
1315
|
for field in ("created_by", "last_agent"):
|
|
1270
1316
|
normalized_agent = canonical_agent_identity(
|
|
@@ -1801,6 +1847,11 @@ def append_execution_record(root: Path, task_id: str, record: dict) -> None:
|
|
|
1801
1847
|
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
1802
1848
|
handle.flush()
|
|
1803
1849
|
os.fsync(handle.fileno())
|
|
1850
|
+
records = cached_memo(("execution", str(path)))
|
|
1851
|
+
if records is not None:
|
|
1852
|
+
records.append(record)
|
|
1853
|
+
if record.get("type") in {"plan", "spec-design-sync"}:
|
|
1854
|
+
invalidate_memo(("plan", str(path)))
|
|
1804
1855
|
|
|
1805
1856
|
|
|
1806
1857
|
def is_non_empty_string(value: object) -> bool:
|
|
@@ -1830,13 +1881,18 @@ def is_valid_review_finding(value: object) -> bool:
|
|
|
1830
1881
|
|
|
1831
1882
|
|
|
1832
1883
|
def gate_identity(record: dict) -> tuple:
|
|
1884
|
+
# 已有手写证据没有输入凭据,保留原覆盖语义;新证据与 prepare-check 共用身份。
|
|
1885
|
+
if isinstance(record.get("inputs"), dict):
|
|
1886
|
+
return check_identity(record)
|
|
1833
1887
|
return (str(record.get("source_task_id") or ""), str(record.get("unit_id") or ""),
|
|
1834
1888
|
str(record.get("dimension") or record.get("check") or ""),
|
|
1835
1889
|
str(record.get("review_scope") or record.get("coverage_scope") or ""))
|
|
1836
1890
|
|
|
1837
1891
|
|
|
1838
1892
|
def failure_label(record: dict) -> str:
|
|
1839
|
-
|
|
1893
|
+
unit = str(record.get("unit_id") or "")
|
|
1894
|
+
name = str(record.get("dimension") or record.get("check") or "")
|
|
1895
|
+
scope = str(record.get("review_scope") or record.get("coverage_scope") or "")
|
|
1840
1896
|
label = f"{record['type']}:{name}"
|
|
1841
1897
|
if scope:
|
|
1842
1898
|
label += f":{scope}"
|
|
@@ -2209,30 +2265,42 @@ def inspect_task_spec(
|
|
|
2209
2265
|
def restore_spec_context(
|
|
2210
2266
|
root: Path, task_id: str, task: dict, agent: str,
|
|
2211
2267
|
session_file: str | Path | None = None,
|
|
2268
|
+
*, force: bool = False,
|
|
2212
2269
|
) -> dict | None:
|
|
2213
2270
|
if not isinstance(task.get("spec_source"), dict):
|
|
2214
2271
|
return None
|
|
2215
|
-
|
|
2272
|
+
session_key = resolve_session_path(root, session_file).relative_to(root.resolve()).as_posix()
|
|
2216
2273
|
try:
|
|
2217
2274
|
inspection, _ = inspect_task_spec(root, task, allow_pending_hard_dependencies=True)
|
|
2275
|
+
if isinstance(task.get("spec_change"), dict):
|
|
2276
|
+
raise StateError("Confirmed Spec change is pending; update the bound source and run sync-spec-design.")
|
|
2277
|
+
expected = {
|
|
2278
|
+
"session_file": session_key, "agent": normalize_session_agent(agent),
|
|
2279
|
+
"spec_id": inspection["spec_id"], "revision": inspection["revision"],
|
|
2280
|
+
"design_sha256": inspection["design_sha256"],
|
|
2281
|
+
"selected_spec_tasks": task["selected_spec_tasks"],
|
|
2282
|
+
}
|
|
2283
|
+
receipts = task.setdefault("spec_contexts", {})
|
|
2284
|
+
receipt = receipts.get(session_key) or task.get("spec_context")
|
|
2285
|
+
if not force and isinstance(receipt, dict) and all(receipt.get(k) == v for k, v in expected.items()):
|
|
2286
|
+
receipts[session_key] = receipt
|
|
2287
|
+
task["spec_context"] = receipt
|
|
2288
|
+
write_task(root, task_id, task)
|
|
2289
|
+
return {"status": "ready", "reused": True, "receipt": receipt}
|
|
2218
2290
|
context = select_consumption_scopes(
|
|
2219
2291
|
stored_spec_path(root, task), root, task["selected_spec_tasks"]
|
|
2220
2292
|
)
|
|
2221
2293
|
if context.get("design_sha256") != inspection.get("design_sha256"):
|
|
2222
2294
|
raise StateError("Canonical Spec changed while restoring context; retry resume-spec-context.")
|
|
2223
|
-
if isinstance(task.get("spec_change"), dict):
|
|
2224
|
-
raise StateError("Confirmed Spec change is pending; update the bound source and run sync-spec-design.")
|
|
2225
2295
|
task["spec_context"] = {
|
|
2226
|
-
|
|
2227
|
-
"agent": normalize_session_agent(agent),
|
|
2228
|
-
"spec_id": inspection["spec_id"],
|
|
2229
|
-
"revision": inspection["revision"],
|
|
2230
|
-
"design_sha256": inspection["design_sha256"],
|
|
2231
|
-
"selected_spec_tasks": task["selected_spec_tasks"],
|
|
2296
|
+
**expected,
|
|
2232
2297
|
"loaded_at": now_iso(),
|
|
2233
2298
|
}
|
|
2299
|
+
receipts[session_key] = task["spec_context"]
|
|
2234
2300
|
result = {"status": "ready", "consumption": context}
|
|
2235
2301
|
except (StateError, EasyDevSpecError, OSError, UnicodeError) as exc:
|
|
2302
|
+
task.pop("spec_context", None)
|
|
2303
|
+
task.get("spec_contexts", {}).pop(session_key, None)
|
|
2236
2304
|
# 接手仍可成功,修复来源与同步状态后必须重新加载,不能沿用旧会话的消费记录。
|
|
2237
2305
|
result = {"status": "blocked", "reason": str(exc), "source": task["spec_source"]}
|
|
2238
2306
|
write_task(root, task_id, task)
|
|
@@ -2246,7 +2314,7 @@ def resume_spec_context(
|
|
|
2246
2314
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
2247
2315
|
if not isinstance(task.get("spec_source"), dict):
|
|
2248
2316
|
raise StateError("Current task is not backed by a Canonical Spec.")
|
|
2249
|
-
context = restore_spec_context(root, resolved_task_id, task, agent, session_file)
|
|
2317
|
+
context = restore_spec_context(root, resolved_task_id, task, agent, session_file, force=True)
|
|
2250
2318
|
snapshot = snapshot_state(root, session_file, session)
|
|
2251
2319
|
snapshot.update({"action": "resume-spec-context", "spec_context": context})
|
|
2252
2320
|
return snapshot
|
|
@@ -2263,9 +2331,10 @@ def require_spec_context(
|
|
|
2263
2331
|
inspection, _ = inspect_task_spec(
|
|
2264
2332
|
root, task, allow_pending_hard_dependencies=allow_pending_hard_dependencies,
|
|
2265
2333
|
)
|
|
2266
|
-
|
|
2334
|
+
session_key = resolve_session_path(root, session_file).relative_to(root.resolve()).as_posix()
|
|
2335
|
+
receipt = task.get("spec_contexts", {}).get(session_key) or task.get("spec_context")
|
|
2267
2336
|
expected = {
|
|
2268
|
-
"session_file":
|
|
2337
|
+
"session_file": session_key,
|
|
2269
2338
|
"agent": normalize_session_agent(agent),
|
|
2270
2339
|
"spec_id": inspection["spec_id"],
|
|
2271
2340
|
"revision": inspection["revision"],
|
|
@@ -2619,6 +2688,10 @@ def has_valid_execution_plan(root: Path, task_id: str) -> bool:
|
|
|
2619
2688
|
|
|
2620
2689
|
def execution_records(root: Path, task_id: str) -> list[dict]:
|
|
2621
2690
|
path = execution_log_path(root, task_id)
|
|
2691
|
+
return memo(("execution", str(path)), lambda: _execution_records(path))
|
|
2692
|
+
|
|
2693
|
+
|
|
2694
|
+
def _execution_records(path: Path) -> list[dict]:
|
|
2622
2695
|
if not path.exists():
|
|
2623
2696
|
return []
|
|
2624
2697
|
records: list[dict] = []
|
|
@@ -2635,6 +2708,11 @@ def execution_records(root: Path, task_id: str) -> list[dict]:
|
|
|
2635
2708
|
|
|
2636
2709
|
|
|
2637
2710
|
def latest_execution_plan(root: Path, task_id: str) -> dict | None:
|
|
2711
|
+
return memo(("plan", str(execution_log_path(root, task_id))),
|
|
2712
|
+
lambda: _latest_execution_plan(root, task_id))
|
|
2713
|
+
|
|
2714
|
+
|
|
2715
|
+
def _latest_execution_plan(root: Path, task_id: str) -> dict | None:
|
|
2638
2716
|
latest: dict | None = None
|
|
2639
2717
|
for record in execution_records(root, task_id):
|
|
2640
2718
|
if record.get("type") == "plan":
|
|
@@ -2889,7 +2967,7 @@ def tdd_baseline_marker_reasons(
|
|
|
2889
2967
|
def contains_tdd_threshold(content: str, threshold: int) -> bool:
|
|
2890
2968
|
return re.search(
|
|
2891
2969
|
rf"(?<!\d){threshold}\s*%|--threshold(?:\s+|=){threshold}(?!\d)|"
|
|
2892
|
-
rf"
|
|
2970
|
+
rf"ut_coverage_threshold\s*[:=]\s*{threshold}(?!\d)",
|
|
2893
2971
|
content,
|
|
2894
2972
|
re.IGNORECASE,
|
|
2895
2973
|
) is not None
|
|
@@ -3133,9 +3211,9 @@ def tdd_infrastructure_fingerprint(repositories: set[Path]) -> str:
|
|
|
3133
3211
|
|
|
3134
3212
|
|
|
3135
3213
|
def check_identity(record: dict) -> tuple:
|
|
3136
|
-
return tuple(str(record.get(key) or "") for key in (
|
|
3137
|
-
"type", "unit_id", "source_task_id", "dimension", "review_scope", "
|
|
3138
|
-
))
|
|
3214
|
+
return (*tuple(str(record.get(key) or "") for key in (
|
|
3215
|
+
"type", "repo_id", "unit_id", "source_task_id", "dimension", "review_scope", "check_type", "coverage_scope"
|
|
3216
|
+
)), command_identity(str(record.get("command") or "")))
|
|
3139
3217
|
|
|
3140
3218
|
|
|
3141
3219
|
def prepare_check(root: Path, task_id: str, task: dict, descriptor: dict, agent: str) -> dict:
|
|
@@ -3188,9 +3266,12 @@ def record_check(root: Path, task_id: str, task: dict, prepared_id: str, result:
|
|
|
3188
3266
|
if type(result.get("exit_code")) is not int or result["passed"] != (result["exit_code"] == 0):
|
|
3189
3267
|
raise StateError("Verification passed must agree with its real exit_code.")
|
|
3190
3268
|
context = None
|
|
3191
|
-
|
|
3269
|
+
repair = task.get("quality_repair")
|
|
3270
|
+
repairing = (task.get("status") == "QUALITY" and descriptor["type"] == "verify"
|
|
3271
|
+
and isinstance(repair, dict) and repair.get("approved_at") and not repair.get("completed_at"))
|
|
3272
|
+
if task.get("status") == "QUALITY" and not repairing:
|
|
3192
3273
|
context = ensure_quality_attempt_context(root, task_id, task, agent, persist=True)
|
|
3193
|
-
elif task.get("status") != "IMPLEMENT":
|
|
3274
|
+
elif task.get("status") != "IMPLEMENT" and not repairing:
|
|
3194
3275
|
raise StateError("Record checks only during implementation or quality.")
|
|
3195
3276
|
record = {
|
|
3196
3277
|
**result, **descriptor, **evidence_fingerprints(root, task_id),
|
|
@@ -3236,6 +3317,8 @@ def begin_correction(root: Path, task_id: str, task: dict, files: list[str], sum
|
|
|
3236
3317
|
allowed = {f for unit in units for f in unit.get("files", [])}
|
|
3237
3318
|
if not files or not set(files) <= allowed or not summary.strip():
|
|
3238
3319
|
raise StateError("A correction must name existing task files and the confirmed change.")
|
|
3320
|
+
if task.get("status") == "QUALITY":
|
|
3321
|
+
return prepare_quality_repair(root, task_id, task, files, summary, agent)
|
|
3239
3322
|
cancel_active_quality_attempt(root, task_id, task, agent, summary, "manual-return")
|
|
3240
3323
|
task = load_task(root, task_id) or task
|
|
3241
3324
|
cleanup_verification_checkpoint(root, task_id, task)
|
|
@@ -3265,6 +3348,136 @@ def begin_correction(root: Path, task_id: str, task: dict, files: list[str], sum
|
|
|
3265
3348
|
"reasons": reasons, "correction": task["correction"]}
|
|
3266
3349
|
|
|
3267
3350
|
|
|
3351
|
+
def prepare_quality_repair(root: Path, task_id: str, task: dict, files: list[str],
|
|
3352
|
+
summary: str, agent: str) -> dict:
|
|
3353
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3354
|
+
records = validated_quality_records(root, task_id)
|
|
3355
|
+
last = records[-1][1] if records else {}
|
|
3356
|
+
if last.get("outcome") == "replan" and last.get("attempt") != task.get("quality_consumed_attempt"):
|
|
3357
|
+
raise StateError("The confirmed contract changed; finish the ANALYSIS replan first.")
|
|
3358
|
+
inputs = capture(input_spec(root, task, plan, {"type": "review"}))
|
|
3359
|
+
repair_id = digest([last.get("attempt", 0), inputs["signature"], sorted(files), summary.strip()])
|
|
3360
|
+
existing = task.get("quality_repair")
|
|
3361
|
+
if isinstance(existing, dict) and not existing.get("completed_at"):
|
|
3362
|
+
if existing["repair_id"] == repair_id:
|
|
3363
|
+
return {"task_id": task_id, "status": "QUALITY", "quality_repair": existing}
|
|
3364
|
+
if existing.get("approved_at"):
|
|
3365
|
+
raise StateError("Complete the approved repair before preparing another one.")
|
|
3366
|
+
repair = {
|
|
3367
|
+
"repair_id": repair_id, "summary": summary.strip(), "files": sorted(set(files)),
|
|
3368
|
+
"unit_ids": [u["id"] for u in plan.get("units", []) if set(u["files"]) & set(files)],
|
|
3369
|
+
"quality_attempt": last.get("attempt", 0),
|
|
3370
|
+
"implementation_fingerprint": inputs["signature"], "inputs": inputs,
|
|
3371
|
+
}
|
|
3372
|
+
task["quality_repair"] = repair
|
|
3373
|
+
write_task(root, task_id, task)
|
|
3374
|
+
return {"task_id": task_id, "status": "QUALITY", "quality_repair": repair}
|
|
3375
|
+
|
|
3376
|
+
|
|
3377
|
+
def start_quality_repair(root: Path, repair_id: str, executor: str, confirmed: bool, agent: str,
|
|
3378
|
+
task_id: str | None = None, session_file: str | Path | None = None) -> dict:
|
|
3379
|
+
session, task_id, task = resolve_current_task(root, task_id, session_file)
|
|
3380
|
+
require_spec_context(root, task, agent, session_file)
|
|
3381
|
+
repair = task.get("quality_repair")
|
|
3382
|
+
if task.get("status") != "QUALITY" or not isinstance(repair, dict) or repair.get("repair_id") != repair_id:
|
|
3383
|
+
raise StateError("Select the current QUALITY repair bundle.")
|
|
3384
|
+
if repair.get("completed_at"):
|
|
3385
|
+
return snapshot_state(root, session_file, session)
|
|
3386
|
+
mode = behavior_layers(root, session)["cooperate_mode"]["value"]
|
|
3387
|
+
if executor not in {"current", "other"} or (not repair.get("approved_at") and executor == "other" and mode != "dispatch"):
|
|
3388
|
+
raise StateError("QUALITY repair handoff requires cooperate_mode dispatch.")
|
|
3389
|
+
if repair.get("approved_at"):
|
|
3390
|
+
if repair.get("executor") != executor:
|
|
3391
|
+
raise StateError("This repair already has an approved executor; resume that handoff.")
|
|
3392
|
+
else:
|
|
3393
|
+
if (mode == "dispatch" or resolve_approval_mode(root, session)[2] == "approve") and not confirmed:
|
|
3394
|
+
raise StateError("Confirm the repair scope and executor once before starting this repair.")
|
|
3395
|
+
if implementation_fingerprint(root, task_id) != repair["implementation_fingerprint"]:
|
|
3396
|
+
raise StateError("Repair inputs changed before approval; refresh the displayed repair bundle.")
|
|
3397
|
+
if isinstance(task.get("spec_source"), dict):
|
|
3398
|
+
task, failed_sources = prepare_canonical_repair_transition(root, task_id, task, agent)
|
|
3399
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3400
|
+
repair_sources = {u.get("source_task_id") for u in plan.get("units", [])
|
|
3401
|
+
if u["id"] in repair["unit_ids"]}
|
|
3402
|
+
if not failed_sources <= repair_sources:
|
|
3403
|
+
raise StateError("The repair bundle must cover all failed Canonical source tasks: "
|
|
3404
|
+
+ ", ".join(sorted(failed_sources - repair_sources)))
|
|
3405
|
+
cancel_active_quality_attempt(root, task_id, task, agent, repair["summary"], "manual-return")
|
|
3406
|
+
task = load_task(root, task_id) or task
|
|
3407
|
+
repair = task["quality_repair"]
|
|
3408
|
+
repair.update(approved_at=now_iso(), approved_by=agent, executor=executor,
|
|
3409
|
+
authorization="explicit-user" if confirmed else "approval-policy",
|
|
3410
|
+
execution_start_index=len(execution_records(root, task_id)))
|
|
3411
|
+
cleanup_verification_checkpoint(root, task_id, task)
|
|
3412
|
+
task.pop("pending_transition", None)
|
|
3413
|
+
write_task(root, task_id, task)
|
|
3414
|
+
if isinstance(task.get("canonical_repair_transition"), dict):
|
|
3415
|
+
sources = set(task["canonical_repair_transition"]["source_task_ids"])
|
|
3416
|
+
writeback_ready_tasks_for_implement(root, task_id, task, agent, {"blocked"}, sources,
|
|
3417
|
+
repair_id=repair_id)
|
|
3418
|
+
task = load_task(root, task_id) or task
|
|
3419
|
+
task.pop("canonical_repair_transition", None)
|
|
3420
|
+
write_task(root, task_id, task)
|
|
3421
|
+
if executor == "other" and not repair.get("handed_off"):
|
|
3422
|
+
# 同一授权直接生成接力,接手不再经过阶段审批。
|
|
3423
|
+
result = handoff_task(root, agent, repair["summary"], task_id, session_file,
|
|
3424
|
+
{"next_action": "repair", "unit_ids": repair["unit_ids"],
|
|
3425
|
+
"stop_after": "repair", "repair_id": repair_id})
|
|
3426
|
+
task = load_task(root, task_id)
|
|
3427
|
+
task["quality_repair"]["handed_off"] = True
|
|
3428
|
+
write_task(root, task_id, task)
|
|
3429
|
+
return result
|
|
3430
|
+
return snapshot_state(root, session_file, session)
|
|
3431
|
+
|
|
3432
|
+
|
|
3433
|
+
def complete_quality_repair(root: Path, repair_id: str, agent: str,
|
|
3434
|
+
task_id: str | None = None, session_file: str | Path | None = None) -> dict:
|
|
3435
|
+
session, task_id, task = resolve_current_task(root, task_id, session_file)
|
|
3436
|
+
require_spec_context(root, task, agent, session_file)
|
|
3437
|
+
repair = task.get("quality_repair")
|
|
3438
|
+
if task.get("status") != "QUALITY" or not isinstance(repair, dict) or repair.get("repair_id") != repair_id or not repair.get("approved_at"):
|
|
3439
|
+
raise StateError("Complete only the approved current QUALITY repair.")
|
|
3440
|
+
if repair.get("completed_at"):
|
|
3441
|
+
return snapshot_state(root, session_file, session)
|
|
3442
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3443
|
+
current = capture(input_spec(root, task, plan, {"type": "review"}))
|
|
3444
|
+
previous = repair["inputs"]
|
|
3445
|
+
if previous["spec"] != current["spec"]:
|
|
3446
|
+
raise StateError("Repair changed the contract or input scope; reconcile the plan before verification.")
|
|
3447
|
+
allowed = set()
|
|
3448
|
+
for unit in plan.get("units", []):
|
|
3449
|
+
if unit["id"] not in repair["unit_ids"]:
|
|
3450
|
+
continue
|
|
3451
|
+
binding = (task.get("repo_paths") or {}).get(unit.get("repo_id"), str(root))
|
|
3452
|
+
repository = Path(binding)
|
|
3453
|
+
if not repository.is_absolute():
|
|
3454
|
+
repository = root / repository
|
|
3455
|
+
allowed.update(str((repository / file).resolve()) for file in repair["files"] if file in unit["files"])
|
|
3456
|
+
changed = [str((Path(repo) / file).resolve()) for repo, files in current["files"].items()
|
|
3457
|
+
for file in set(files) | set(previous["files"].get(repo, {}))
|
|
3458
|
+
if files.get(file) != previous["files"].get(repo, {}).get(file)]
|
|
3459
|
+
if any(not any(path == scope or path.startswith(scope + "/") for scope in allowed) for path in changed):
|
|
3460
|
+
raise StateError("Repair changed files outside the approved bundle.")
|
|
3461
|
+
if isinstance(task.get("spec_source"), dict):
|
|
3462
|
+
inspection, _ = inspect_task_spec(root, task)
|
|
3463
|
+
snapshots = _selected_execution_snapshots(inspection, task)
|
|
3464
|
+
sources = {u["source_task_id"] for u in plan.get("units", []) if u["id"] in repair["unit_ids"]}
|
|
3465
|
+
if any(snapshots.get(source, {}).get("status") not in {"implemented", "verified", "completed"}
|
|
3466
|
+
for source in sources):
|
|
3467
|
+
raise StateError("Write back the repaired Canonical tasks as implemented before returning.")
|
|
3468
|
+
repair["completed_at"] = now_iso()
|
|
3469
|
+
task.pop("quality_return_required", None)
|
|
3470
|
+
records = validated_quality_records(root, task_id)
|
|
3471
|
+
if records:
|
|
3472
|
+
task["quality_consumed_attempt"] = records[-1][1]["attempt"]
|
|
3473
|
+
task["continuation"] = {"next_action": "quality", "unit_ids": repair["unit_ids"]}
|
|
3474
|
+
write_task(root, task_id, task)
|
|
3475
|
+
if repair["executor"] == "other":
|
|
3476
|
+
return handoff_task(root, agent, "Repair complete; review the delta and verify affected inputs.",
|
|
3477
|
+
task_id, session_file, task["continuation"])
|
|
3478
|
+
return snapshot_state(root, session_file, session)
|
|
3479
|
+
|
|
3480
|
+
|
|
3268
3481
|
def implementation_fingerprint(root: Path, task_id: str) -> str:
|
|
3269
3482
|
plan = latest_execution_plan(root, task_id)
|
|
3270
3483
|
if not plan:
|
|
@@ -3286,11 +3499,22 @@ def canonical_repository_fingerprints(root: Path, task_id: str, task: dict) -> d
|
|
|
3286
3499
|
}
|
|
3287
3500
|
|
|
3288
3501
|
|
|
3502
|
+
def unit_test_contract(task: dict) -> dict:
|
|
3503
|
+
mode = task.get("unit_test_mode")
|
|
3504
|
+
# 旧证据的序列化键保持不变,配置字段改名不触发全局失效;UT 单独标识。
|
|
3505
|
+
contract = {
|
|
3506
|
+
"tdd_enabled": None if mode is None else mode != "none",
|
|
3507
|
+
"tdd_coverage_threshold": task.get("ut_coverage_threshold"),
|
|
3508
|
+
"tdd_baselines": task.get("tdd_baselines"),
|
|
3509
|
+
}
|
|
3510
|
+
if mode == "ut":
|
|
3511
|
+
contract["unit_test_mode"] = "ut"
|
|
3512
|
+
return contract
|
|
3513
|
+
|
|
3514
|
+
|
|
3289
3515
|
def behavior_config_fingerprint(root: Path, task: dict | None = None) -> str:
|
|
3290
3516
|
# 审批方式、记忆策略等配置不影响已经执行的测试。
|
|
3291
|
-
return digest(
|
|
3292
|
-
"tdd_enabled", "tdd_coverage_threshold", "tdd_baselines"
|
|
3293
|
-
)})
|
|
3517
|
+
return digest(unit_test_contract(task or {}))
|
|
3294
3518
|
|
|
3295
3519
|
|
|
3296
3520
|
def evidence_fingerprints(root: Path, task_id: str) -> dict[str, str]:
|
|
@@ -3499,6 +3723,12 @@ def ensure_quality_attempt_context(
|
|
|
3499
3723
|
persist: bool = False,
|
|
3500
3724
|
infer_existing_evidence: bool = False,
|
|
3501
3725
|
) -> dict:
|
|
3726
|
+
repair = task.get("quality_repair")
|
|
3727
|
+
if isinstance(repair, dict) and not repair.get("completed_at"):
|
|
3728
|
+
raise StateError("Complete the current QUALITY repair before collecting gate evidence.")
|
|
3729
|
+
if persist and (task.get("continuation") or {}).get("next_action") == "quality":
|
|
3730
|
+
task.pop("continuation", None)
|
|
3731
|
+
write_task(root, task_id, task)
|
|
3502
3732
|
if isinstance(task.get("canonical_repair_transition"), dict):
|
|
3503
3733
|
raise StateError(
|
|
3504
3734
|
"Canonical repair transition is incomplete; resume it before collecting new QUALITY evidence."
|
|
@@ -4205,12 +4435,10 @@ def verification_contract_fingerprint(root: Path, task_id: str, task: dict) -> s
|
|
|
4205
4435
|
source = task.get("spec_source") if isinstance(task.get("spec_source"), dict) else {}
|
|
4206
4436
|
contract = {
|
|
4207
4437
|
"workflow_mode": task.get("workflow_mode"),
|
|
4208
|
-
|
|
4209
|
-
"tdd_coverage_threshold": task.get("tdd_coverage_threshold"),
|
|
4210
|
-
"tdd_baselines": task.get("tdd_baselines"),
|
|
4438
|
+
**unit_test_contract(task),
|
|
4211
4439
|
**({"tdd_infrastructure": tdd_infrastructure_fingerprint(
|
|
4212
4440
|
{root.resolve(), *task_repository_roots(root, task, plan)}
|
|
4213
|
-
)} if task.get("
|
|
4441
|
+
)} if task.get("unit_test_mode") in {"ut", "tdd"} else {}),
|
|
4214
4442
|
"plan": plan,
|
|
4215
4443
|
"canonical": {
|
|
4216
4444
|
"schema": source.get("schema"),
|
|
@@ -5134,7 +5362,7 @@ def validate_review_readiness(
|
|
|
5134
5362
|
raise StateError(
|
|
5135
5363
|
"QUALITY cannot advance while a review dimension is not passed or has error findings."
|
|
5136
5364
|
)
|
|
5137
|
-
if task.get("
|
|
5365
|
+
if task.get("unit_test_mode") == "tdd":
|
|
5138
5366
|
if is_spec_task:
|
|
5139
5367
|
missing_tdd_reviews = sorted(
|
|
5140
5368
|
source_task_id
|
|
@@ -5206,7 +5434,7 @@ def validate_verification_readiness(
|
|
|
5206
5434
|
and is_non_empty_string(record.get("check"))
|
|
5207
5435
|
):
|
|
5208
5436
|
if (
|
|
5209
|
-
task.get("
|
|
5437
|
+
task.get("unit_test_mode") in {"ut", "tdd"}
|
|
5210
5438
|
and record.get("check_type") == "coverage"
|
|
5211
5439
|
and record.get("coverage_scope") == "gitlab"
|
|
5212
5440
|
):
|
|
@@ -5309,13 +5537,13 @@ def validate_verification_readiness(
|
|
|
5309
5537
|
"TDD initialization cannot advance to MEMORY until readiness passes: "
|
|
5310
5538
|
+ "; ".join(str(reason) for reason in readiness["reasons"])
|
|
5311
5539
|
)
|
|
5312
|
-
if task.get("
|
|
5540
|
+
if task.get("unit_test_mode") not in {"ut", "tdd"} and any(
|
|
5313
5541
|
record.get("check_type") == "coverage" for record in latest_by_check.values()
|
|
5314
5542
|
):
|
|
5315
5543
|
raise StateError(
|
|
5316
|
-
"Coverage verification evidence is not allowed when the frozen
|
|
5544
|
+
"Coverage verification evidence is not allowed when the frozen unit test mode is none."
|
|
5317
5545
|
)
|
|
5318
|
-
if task.get("
|
|
5546
|
+
if task.get("unit_test_mode") in {"ut", "tdd"}:
|
|
5319
5547
|
require_tdd_readiness(root)
|
|
5320
5548
|
test_records = [
|
|
5321
5549
|
record
|
|
@@ -5330,7 +5558,7 @@ def validate_verification_readiness(
|
|
|
5330
5558
|
]
|
|
5331
5559
|
if not coverage_records:
|
|
5332
5560
|
raise StateError(
|
|
5333
|
-
"
|
|
5561
|
+
"Unit test verification requires changed-production-line JaCoCo coverage evidence."
|
|
5334
5562
|
)
|
|
5335
5563
|
if is_spec_task:
|
|
5336
5564
|
tested_source_tasks = {
|
|
@@ -5341,7 +5569,7 @@ def validate_verification_readiness(
|
|
|
5341
5569
|
)
|
|
5342
5570
|
if missing_test_tasks:
|
|
5343
5571
|
raise StateError(
|
|
5344
|
-
"
|
|
5572
|
+
"Unit test Canonical verification requires local unit-test evidence for every selected source task: "
|
|
5345
5573
|
+ ", ".join(missing_test_tasks)
|
|
5346
5574
|
)
|
|
5347
5575
|
covered_source_tasks = {
|
|
@@ -5352,33 +5580,33 @@ def validate_verification_readiness(
|
|
|
5352
5580
|
)
|
|
5353
5581
|
if missing_coverage_tasks:
|
|
5354
5582
|
raise StateError(
|
|
5355
|
-
"
|
|
5583
|
+
"Unit test Canonical verification requires separate coverage evidence for every selected source task: "
|
|
5356
5584
|
+ ", ".join(missing_coverage_tasks)
|
|
5357
5585
|
)
|
|
5358
5586
|
elif not test_records:
|
|
5359
5587
|
raise StateError(
|
|
5360
|
-
"
|
|
5588
|
+
"Unit test verification requires passed local unit-test evidence."
|
|
5361
5589
|
)
|
|
5362
5590
|
for record in coverage_records:
|
|
5363
5591
|
scope = str(record.get("coverage_scope") or "")
|
|
5364
5592
|
if scope != "local":
|
|
5365
5593
|
raise StateError(
|
|
5366
|
-
"
|
|
5594
|
+
"Unit test coverage evidence must identify coverage_scope as local."
|
|
5367
5595
|
)
|
|
5368
|
-
expected_threshold = task.get("
|
|
5596
|
+
expected_threshold = task.get("ut_coverage_threshold")
|
|
5369
5597
|
expected_baselines = task.get("tdd_baselines")
|
|
5370
5598
|
if (
|
|
5371
5599
|
type(expected_threshold) is not int
|
|
5372
5600
|
or expected_threshold < 1
|
|
5373
5601
|
or expected_threshold > 100
|
|
5374
5602
|
):
|
|
5375
|
-
raise StateError("
|
|
5603
|
+
raise StateError("Unit test task is missing a valid frozen coverage threshold.")
|
|
5376
5604
|
if not isinstance(expected_baselines, dict) or not expected_baselines:
|
|
5377
|
-
raise StateError("
|
|
5605
|
+
raise StateError("Unit test task is missing frozen Git baselines.")
|
|
5378
5606
|
for record in coverage_records:
|
|
5379
5607
|
coverage = record.get("coverage")
|
|
5380
5608
|
if not isinstance(coverage, dict):
|
|
5381
|
-
raise StateError("
|
|
5609
|
+
raise StateError("Unit test coverage evidence must include the coverage result object.")
|
|
5382
5610
|
total = coverage.get("total_lines")
|
|
5383
5611
|
covered = coverage.get("covered_lines")
|
|
5384
5612
|
percentage = coverage.get("percentage")
|
|
@@ -5414,7 +5642,7 @@ def validate_verification_readiness(
|
|
|
5414
5642
|
)
|
|
5415
5643
|
):
|
|
5416
5644
|
raise StateError(
|
|
5417
|
-
"
|
|
5645
|
+
"Unit test coverage evidence must preserve the exact gate command, baseline, counts, percentage, frozen threshold, reports, and report fingerprint."
|
|
5418
5646
|
)
|
|
5419
5647
|
if total == 0:
|
|
5420
5648
|
if record.get("applicable") is not False or record.get("passed") is not True:
|
|
@@ -5423,7 +5651,7 @@ def validate_verification_readiness(
|
|
|
5423
5651
|
)
|
|
5424
5652
|
elif abs(percentage - round(covered * 100.0 / total, 2)) > 0.01:
|
|
5425
5653
|
raise StateError(
|
|
5426
|
-
"
|
|
5654
|
+
"Unit test coverage evidence percentage does not match covered/total counts."
|
|
5427
5655
|
)
|
|
5428
5656
|
elif (
|
|
5429
5657
|
record.get("applicable") is False
|
|
@@ -5431,7 +5659,7 @@ def validate_verification_readiness(
|
|
|
5431
5659
|
or percentage < threshold
|
|
5432
5660
|
):
|
|
5433
5661
|
raise StateError(
|
|
5434
|
-
f"
|
|
5662
|
+
f"Unit test changed-line coverage must meet the frozen {threshold}% threshold."
|
|
5435
5663
|
)
|
|
5436
5664
|
if task.get("workflow_mode") == "strict":
|
|
5437
5665
|
if is_spec_task:
|
|
@@ -5637,13 +5865,12 @@ def quality_repair_failures_for_window(
|
|
|
5637
5865
|
and repo_id == task_repositories[source_task_id]
|
|
5638
5866
|
)
|
|
5639
5867
|
):
|
|
5640
|
-
|
|
5641
|
-
latest_reviews[(owner, *gate_identity(record)[1:])] = record
|
|
5868
|
+
latest_reviews[gate_identity(record)] = record
|
|
5642
5869
|
elif record_type == "verify" and record.get(
|
|
5643
5870
|
"implementation_fingerprint"
|
|
5644
5871
|
) == implementation and record.get("config_fingerprint") == config:
|
|
5645
5872
|
if (
|
|
5646
|
-
task.get("
|
|
5873
|
+
task.get("unit_test_mode") in {"ut", "tdd"}
|
|
5647
5874
|
and record.get("check_type") == "coverage"
|
|
5648
5875
|
and record.get("coverage_scope") == "gitlab"
|
|
5649
5876
|
):
|
|
@@ -5666,11 +5893,11 @@ def quality_repair_failures_for_window(
|
|
|
5666
5893
|
)
|
|
5667
5894
|
):
|
|
5668
5895
|
continue
|
|
5669
|
-
|
|
5670
|
-
latest_verifications[(owner, *gate_identity(record)[1:])] = record
|
|
5896
|
+
latest_verifications[gate_identity(record)] = record
|
|
5671
5897
|
|
|
5672
5898
|
failures: dict[str, list[str]] = {}
|
|
5673
|
-
for
|
|
5899
|
+
for record in latest_reviews.values():
|
|
5900
|
+
source_task_id = str(record.get("source_task_id")) if canonical else task_id
|
|
5674
5901
|
findings = record.get("findings")
|
|
5675
5902
|
has_error = isinstance(findings, list) and any(
|
|
5676
5903
|
isinstance(finding, dict)
|
|
@@ -5679,7 +5906,8 @@ def quality_repair_failures_for_window(
|
|
|
5679
5906
|
)
|
|
5680
5907
|
if record.get("passed") is not True or has_error:
|
|
5681
5908
|
failures.setdefault(source_task_id, []).append(failure_label(record))
|
|
5682
|
-
for
|
|
5909
|
+
for record in latest_verifications.values():
|
|
5910
|
+
source_task_id = str(record.get("source_task_id")) if canonical else task_id
|
|
5683
5911
|
if record.get("applicable") is not False and record.get("passed") is not True:
|
|
5684
5912
|
failures.setdefault(source_task_id, []).append(failure_label(record))
|
|
5685
5913
|
return failures
|
|
@@ -6408,10 +6636,18 @@ def validate_analysis_readiness(
|
|
|
6408
6636
|
test_strategy = task_dir / "test-strategy.md"
|
|
6409
6637
|
reasons: list[str] = []
|
|
6410
6638
|
behavior = resolve_behavior(root, session or default_session())
|
|
6411
|
-
|
|
6639
|
+
unit_test_mode = behavior[8] if task_type != TDD_INIT_TASK_TYPE else "none"
|
|
6412
6640
|
tdd_threshold = behavior[11]
|
|
6413
6641
|
|
|
6414
|
-
if
|
|
6642
|
+
if unit_test_mode == "ut":
|
|
6643
|
+
require_tdd_readiness(root)
|
|
6644
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
6645
|
+
if not any(str(file).endswith(".java") for unit in plan.get("units", []) for file in unit.get("files", [])):
|
|
6646
|
+
reasons.append("UT is enabled but the confirmed implementation scope has no Java source")
|
|
6647
|
+
if reasons:
|
|
6648
|
+
raise StateError("; ".join(reasons))
|
|
6649
|
+
|
|
6650
|
+
if dev_spec.is_file() and unit_test_mode != "tdd":
|
|
6415
6651
|
compact = dev_spec.read_text(encoding="utf-8")
|
|
6416
6652
|
if compact.startswith("<!-- easy-coding:compact -->"):
|
|
6417
6653
|
mode, _ = calculate_workflow_floor(root, task_id)
|
|
@@ -6527,7 +6763,7 @@ def validate_analysis_readiness(
|
|
|
6527
6763
|
plan_is_valid = has_valid_execution_plan(root, task_id)
|
|
6528
6764
|
if not plan_is_valid:
|
|
6529
6765
|
reasons.append("execution.jsonl has no valid plan record")
|
|
6530
|
-
if
|
|
6766
|
+
if unit_test_mode == "tdd":
|
|
6531
6767
|
readiness = tdd_readiness(root)
|
|
6532
6768
|
if readiness["status"] != "ready":
|
|
6533
6769
|
reasons.append(
|
|
@@ -6590,6 +6826,8 @@ def validate_analysis_readiness(
|
|
|
6590
6826
|
dev_spec_content, strategy_content, baselines
|
|
6591
6827
|
)
|
|
6592
6828
|
)
|
|
6829
|
+
elif unit_test_mode == "ut":
|
|
6830
|
+
pass
|
|
6593
6831
|
elif task_type == TDD_INIT_TASK_TYPE:
|
|
6594
6832
|
try:
|
|
6595
6833
|
strategy_content = test_strategy.read_text(encoding="utf-8")
|
|
@@ -6772,45 +7010,14 @@ def validate_analysis_readiness(
|
|
|
6772
7010
|
|
|
6773
7011
|
|
|
6774
7012
|
def latest_handoff_record(root: Path, task_id: str) -> dict | None:
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
return None
|
|
6778
|
-
latest: dict | None = None
|
|
6779
|
-
try:
|
|
6780
|
-
for line in path.read_text(encoding="utf-8").splitlines():
|
|
6781
|
-
if not line.strip():
|
|
6782
|
-
continue
|
|
6783
|
-
try:
|
|
6784
|
-
record = json.loads(line)
|
|
6785
|
-
except json.JSONDecodeError:
|
|
6786
|
-
continue
|
|
6787
|
-
if isinstance(record, dict) and record.get("type") == "handoff":
|
|
6788
|
-
latest = record
|
|
6789
|
-
except OSError:
|
|
6790
|
-
return None
|
|
6791
|
-
return latest
|
|
7013
|
+
return next((r for r in reversed(execution_records(root, task_id))
|
|
7014
|
+
if r.get("type") == "handoff"), None)
|
|
6792
7015
|
|
|
6793
7016
|
|
|
6794
7017
|
def pending_handoff_record(root: Path, task_id: str) -> dict | None:
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
latest_coordination: dict | None = None
|
|
6799
|
-
try:
|
|
6800
|
-
for line in path.read_text(encoding="utf-8").splitlines():
|
|
6801
|
-
if not line.strip():
|
|
6802
|
-
continue
|
|
6803
|
-
try:
|
|
6804
|
-
record = json.loads(line)
|
|
6805
|
-
except json.JSONDecodeError:
|
|
6806
|
-
continue
|
|
6807
|
-
if isinstance(record, dict) and record.get("type") in {"handoff", "claim"}:
|
|
6808
|
-
latest_coordination = record
|
|
6809
|
-
except OSError:
|
|
6810
|
-
return None
|
|
6811
|
-
if latest_coordination and latest_coordination.get("type") == "handoff":
|
|
6812
|
-
return latest_coordination
|
|
6813
|
-
return None
|
|
7018
|
+
latest = next((r for r in reversed(execution_records(root, task_id))
|
|
7019
|
+
if r.get("type") in {"handoff", "claim"}), None)
|
|
7020
|
+
return latest if latest and latest["type"] == "handoff" else None
|
|
6814
7021
|
|
|
6815
7022
|
|
|
6816
7023
|
def assert_safe_task_id(task_id: str) -> None:
|
|
@@ -6939,12 +7146,12 @@ def snapshot_state(
|
|
|
6939
7146
|
project_workflow_mode,
|
|
6940
7147
|
session_workflow_mode,
|
|
6941
7148
|
configured_workflow_mode,
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
7149
|
+
project_unit_test_mode,
|
|
7150
|
+
session_unit_test_mode,
|
|
7151
|
+
effective_unit_test_mode,
|
|
7152
|
+
project_ut_coverage_threshold,
|
|
7153
|
+
session_ut_coverage_threshold,
|
|
7154
|
+
effective_ut_coverage_threshold,
|
|
6948
7155
|
) = resolve_behavior(root, resolved_session)
|
|
6949
7156
|
concrete_workflow_mode = None
|
|
6950
7157
|
if task:
|
|
@@ -6952,26 +7159,26 @@ def snapshot_state(
|
|
|
6952
7159
|
proposal = task.get("workflow_mode_proposal")
|
|
6953
7160
|
if concrete_workflow_mode is None and isinstance(proposal, dict):
|
|
6954
7161
|
concrete_workflow_mode = proposal.get("selected_mode")
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
7162
|
+
task_unit_test_mode = task.get("unit_test_mode") if task else None
|
|
7163
|
+
task_ut_coverage_threshold = task.get("ut_coverage_threshold") if task else None
|
|
7164
|
+
frozen_unit_test = bool(
|
|
6958
7165
|
task
|
|
6959
7166
|
and status not in {"ANALYSIS", "INIT"}
|
|
6960
|
-
and
|
|
7167
|
+
and task_unit_test_mode in {"none", "ut", "tdd"}
|
|
6961
7168
|
)
|
|
6962
7169
|
is_tdd_init = bool(
|
|
6963
7170
|
task and str(task.get("type") or "").strip().lower() == TDD_INIT_TASK_TYPE
|
|
6964
7171
|
)
|
|
6965
|
-
|
|
6966
|
-
|
|
7172
|
+
displayed_unit_test_mode = (
|
|
7173
|
+
"none" if is_tdd_init else task_unit_test_mode if frozen_unit_test else effective_unit_test_mode
|
|
6967
7174
|
)
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
if
|
|
6971
|
-
else
|
|
7175
|
+
displayed_ut_threshold = (
|
|
7176
|
+
task_ut_coverage_threshold
|
|
7177
|
+
if frozen_unit_test and isinstance(task_ut_coverage_threshold, int)
|
|
7178
|
+
else effective_ut_coverage_threshold
|
|
6972
7179
|
)
|
|
6973
7180
|
should_check_readiness = bool(
|
|
6974
|
-
|
|
7181
|
+
effective_unit_test_mode in {"ut", "tdd"} or task_unit_test_mode in {"ut", "tdd"} or is_tdd_init
|
|
6975
7182
|
)
|
|
6976
7183
|
readiness = (
|
|
6977
7184
|
tdd_readiness(root)
|
|
@@ -6979,8 +7186,16 @@ def snapshot_state(
|
|
|
6979
7186
|
else {"status": "not_checked", "reasons": []}
|
|
6980
7187
|
)
|
|
6981
7188
|
|
|
7189
|
+
layers = behavior_layers(root, resolved_session)
|
|
7190
|
+
local, _ = read_behavior_file(Path.home() / ".easy-coding" / "config.yaml")
|
|
6982
7191
|
return {
|
|
6983
7192
|
"session_file": display_path(root, session_path),
|
|
7193
|
+
"behavior_sources": {key: item["source"] for key, item in layers.items()},
|
|
7194
|
+
"local_behavior": local,
|
|
7195
|
+
"effective_cooperate_mode": layers["cooperate_mode"]["value"],
|
|
7196
|
+
"cooperation": task.get("cooperation") if task else None,
|
|
7197
|
+
"quality_repair": task.get("quality_repair") if task else None,
|
|
7198
|
+
"continuation": task.get("continuation") if task else None,
|
|
6984
7199
|
"current_task": str(task_id) if task_id else None,
|
|
6985
7200
|
"task": task,
|
|
6986
7201
|
"pending_transition": task.get("pending_transition") if task else None,
|
|
@@ -6998,19 +7213,19 @@ def snapshot_state(
|
|
|
6998
7213
|
"session_workflow_mode": session_workflow_mode,
|
|
6999
7214
|
"configured_workflow_mode": configured_workflow_mode,
|
|
7000
7215
|
"concrete_workflow_mode": concrete_workflow_mode,
|
|
7001
|
-
"
|
|
7002
|
-
"
|
|
7003
|
-
"
|
|
7004
|
-
"
|
|
7005
|
-
"
|
|
7006
|
-
"
|
|
7007
|
-
"
|
|
7008
|
-
"
|
|
7216
|
+
"project_unit_test_mode": project_unit_test_mode,
|
|
7217
|
+
"session_unit_test_mode": session_unit_test_mode,
|
|
7218
|
+
"effective_unit_test_mode": effective_unit_test_mode,
|
|
7219
|
+
"project_ut_coverage_threshold": project_ut_coverage_threshold,
|
|
7220
|
+
"session_ut_coverage_threshold": session_ut_coverage_threshold,
|
|
7221
|
+
"effective_ut_coverage_threshold": effective_ut_coverage_threshold,
|
|
7222
|
+
"task_unit_test_mode": task_unit_test_mode,
|
|
7223
|
+
"task_ut_coverage_threshold": task_ut_coverage_threshold,
|
|
7009
7224
|
"task_tdd_baselines": task.get("tdd_baselines") if task else None,
|
|
7010
|
-
"
|
|
7011
|
-
"
|
|
7012
|
-
"
|
|
7013
|
-
"
|
|
7225
|
+
"displayed_unit_test_mode": displayed_unit_test_mode,
|
|
7226
|
+
"displayed_ut_coverage_threshold": displayed_ut_threshold,
|
|
7227
|
+
"unit_test_readiness_status": readiness["status"],
|
|
7228
|
+
"unit_test_readiness_reasons": readiness["reasons"],
|
|
7014
7229
|
"spec_summary": spec_task_summary(task),
|
|
7015
7230
|
# Compatibility output aliases for pre-0.9 clients.
|
|
7016
7231
|
"project_confirm_mode": project_approval_mode,
|
|
@@ -7027,8 +7242,9 @@ def build_status_line(
|
|
|
7027
7242
|
session: dict,
|
|
7028
7243
|
agent: str | None = None,
|
|
7029
7244
|
session_file: str | Path | None = None,
|
|
7245
|
+
state: dict | None = None,
|
|
7030
7246
|
) -> str:
|
|
7031
|
-
state = snapshot_state(root, session_file, session)
|
|
7247
|
+
state = state if state is not None else snapshot_state(root, session_file, session)
|
|
7032
7248
|
if state["lite_mode"]:
|
|
7033
7249
|
lite_state = (
|
|
7034
7250
|
"Awaiting Confirmation"
|
|
@@ -7043,8 +7259,10 @@ def build_status_line(
|
|
|
7043
7259
|
approval = str(state["effective_approval_mode"]).capitalize()
|
|
7044
7260
|
workflow = str(state["concrete_workflow_mode"] or state["configured_workflow_mode"]).capitalize()
|
|
7045
7261
|
status_brand = f"> **Easy Coding** · **Approval: {approval}** · **Workflow: {workflow}**"
|
|
7046
|
-
if state["
|
|
7047
|
-
status_brand += " · **
|
|
7262
|
+
if state["effective_cooperate_mode"] == "dispatch":
|
|
7263
|
+
status_brand += " · **Dispatch**"
|
|
7264
|
+
if state["displayed_unit_test_mode"] in {"ut", "tdd"}:
|
|
7265
|
+
status_brand += f" · **{state['displayed_unit_test_mode'].upper()}**"
|
|
7048
7266
|
task_id = state["current_task"]
|
|
7049
7267
|
if task_id:
|
|
7050
7268
|
status = str(state["status"])
|
|
@@ -7075,8 +7293,9 @@ def build_machine_breadcrumbs(
|
|
|
7075
7293
|
session: dict,
|
|
7076
7294
|
agent: str | None = None,
|
|
7077
7295
|
session_file: str | Path | None = None,
|
|
7296
|
+
state: dict | None = None,
|
|
7078
7297
|
) -> list[str]:
|
|
7079
|
-
state = snapshot_state(root, session_file, session)
|
|
7298
|
+
state = state if state is not None else snapshot_state(root, session_file, session)
|
|
7080
7299
|
task_id = state["current_task"]
|
|
7081
7300
|
task = state["task"]
|
|
7082
7301
|
stage = str(state["status"]) if task else "idle"
|
|
@@ -7086,21 +7305,27 @@ def build_machine_breadcrumbs(
|
|
|
7086
7305
|
f"[easy-coding:session-file:{resolved_session_file}]",
|
|
7087
7306
|
f"[easy-coding:approval-mode:{state['effective_approval_mode']}]",
|
|
7088
7307
|
f"[easy-coding:configured-workflow-mode:{state['configured_workflow_mode']}]",
|
|
7308
|
+
f"[easy-coding:cooperate-mode:{state['effective_cooperate_mode']}]",
|
|
7089
7309
|
]
|
|
7090
7310
|
if state.get("concrete_workflow_mode"):
|
|
7091
7311
|
lines.append(f"[easy-coding:workflow-mode:{state['concrete_workflow_mode']}]")
|
|
7092
|
-
if state.get("
|
|
7093
|
-
lines.append("[easy-coding:
|
|
7312
|
+
if state.get("displayed_unit_test_mode") in {"ut", "tdd"}:
|
|
7313
|
+
lines.append(f"[easy-coding:unit-test-mode:{state['displayed_unit_test_mode']}]")
|
|
7094
7314
|
lines.append(
|
|
7095
|
-
f"[easy-coding:
|
|
7315
|
+
f"[easy-coding:ut-coverage-threshold:{state['displayed_ut_coverage_threshold']}]"
|
|
7096
7316
|
)
|
|
7097
7317
|
|
|
7098
7318
|
if task_id:
|
|
7099
7319
|
lines.append(f"[current-task:{task_id}]")
|
|
7320
|
+
continuation = state.get("continuation") or {}
|
|
7321
|
+
if continuation.get("next_action"):
|
|
7322
|
+
lines.append(f"[easy-coding:next-action:{continuation['next_action']}]")
|
|
7323
|
+
if continuation.get("stop_after"):
|
|
7324
|
+
lines.append(f"[easy-coding:stop-after:{continuation['stop_after']}]")
|
|
7100
7325
|
if task and isinstance(task.get("spec_source"), dict):
|
|
7101
7326
|
source = task["spec_source"]
|
|
7102
7327
|
lines.append(f"[easy-coding:spec:{source.get('spec_id')}:revision:{source.get('revision')}]")
|
|
7103
|
-
lines.append("[easy-coding:spec-context:
|
|
7328
|
+
lines.append("[easy-coding:spec-context:reuse-current-session-or-resume-if-missing]")
|
|
7104
7329
|
if task.get("spec_change"):
|
|
7105
7330
|
lines.append("[easy-coding:spec-change:pending-sync-spec-design]")
|
|
7106
7331
|
if (task.get("spec_writeback_progress") or {}).get("pending_action"):
|
|
@@ -7174,6 +7399,7 @@ def build_status_context(
|
|
|
7174
7399
|
session: dict,
|
|
7175
7400
|
agent: str | None = None,
|
|
7176
7401
|
session_file: str | Path | None = None,
|
|
7402
|
+
state: dict | None = None,
|
|
7177
7403
|
) -> str:
|
|
7178
7404
|
if session.get("harness_disabled") is True:
|
|
7179
7405
|
session_path = resolve_session_path(root, session_file)
|
|
@@ -7194,10 +7420,11 @@ def build_status_context(
|
|
|
7194
7420
|
if isinstance(proposal, dict):
|
|
7195
7421
|
lines.append(f"[easy-coding:lite-proposal:{proposal.get('digest', 'missing')}]")
|
|
7196
7422
|
return "\n".join(lines)
|
|
7423
|
+
state = state if state is not None else snapshot_state(root, session_file, session)
|
|
7197
7424
|
return "\n".join(
|
|
7198
7425
|
[
|
|
7199
|
-
build_status_line(root, session, agent, session_file),
|
|
7200
|
-
*build_machine_breadcrumbs(root, session, agent, session_file),
|
|
7426
|
+
build_status_line(root, session, agent, session_file, state),
|
|
7427
|
+
*build_machine_breadcrumbs(root, session, agent, session_file, state),
|
|
7201
7428
|
]
|
|
7202
7429
|
)
|
|
7203
7430
|
|
|
@@ -7212,7 +7439,8 @@ def attach_status_context(
|
|
|
7212
7439
|
session = load_session(root, resolved_session_file)
|
|
7213
7440
|
if session is None:
|
|
7214
7441
|
session = default_session()
|
|
7215
|
-
|
|
7442
|
+
state = data if "current_task" in data and "effective_approval_mode" in data else None
|
|
7443
|
+
context = build_status_context(root, session, agent, resolved_session_file, state)
|
|
7216
7444
|
first_line = context.splitlines()[0] if context.startswith("> ") else ""
|
|
7217
7445
|
enriched = dict(data)
|
|
7218
7446
|
enriched["status_line"] = first_line
|
|
@@ -7422,42 +7650,43 @@ def clear_session_workflow_mode(
|
|
|
7422
7650
|
return snapshot
|
|
7423
7651
|
|
|
7424
7652
|
|
|
7425
|
-
def
|
|
7653
|
+
def set_session_unit_test_mode(
|
|
7426
7654
|
root: Path,
|
|
7427
|
-
|
|
7655
|
+
mode: str,
|
|
7428
7656
|
agent: str,
|
|
7429
7657
|
threshold: int | None = None,
|
|
7430
7658
|
session_file: str | Path | None = None,
|
|
7431
7659
|
) -> dict:
|
|
7432
|
-
|
|
7660
|
+
mode = parse_unit_test_mode(mode, "session unit_test_mode")
|
|
7661
|
+
if mode != "none":
|
|
7433
7662
|
require_tdd_readiness(root)
|
|
7434
7663
|
session = ensure_session(root, session_file)
|
|
7435
7664
|
materialize_legacy_session_behavior(session)
|
|
7436
|
-
session["
|
|
7665
|
+
session["unit_test_mode"] = mode
|
|
7437
7666
|
if threshold is not None:
|
|
7438
|
-
session["
|
|
7439
|
-
threshold, "session
|
|
7667
|
+
session["ut_coverage_threshold"] = parse_ut_threshold(
|
|
7668
|
+
threshold, "session ut_coverage_threshold"
|
|
7440
7669
|
)
|
|
7441
7670
|
session["last_agent"] = agent
|
|
7442
7671
|
write_session(root, session, session_file)
|
|
7443
7672
|
snapshot = snapshot_state(root, session_file, session)
|
|
7444
|
-
snapshot["action"] = "set-
|
|
7673
|
+
snapshot["action"] = "set-unit-test-mode"
|
|
7445
7674
|
return snapshot
|
|
7446
7675
|
|
|
7447
7676
|
|
|
7448
|
-
def
|
|
7677
|
+
def clear_session_unit_test_mode(
|
|
7449
7678
|
root: Path,
|
|
7450
7679
|
agent: str,
|
|
7451
7680
|
session_file: str | Path | None = None,
|
|
7452
7681
|
) -> dict:
|
|
7453
7682
|
session = ensure_session(root, session_file)
|
|
7454
7683
|
materialize_legacy_session_behavior(session)
|
|
7455
|
-
session.pop("
|
|
7456
|
-
session.pop("
|
|
7684
|
+
session.pop("unit_test_mode", None)
|
|
7685
|
+
session.pop("ut_coverage_threshold", None)
|
|
7457
7686
|
session["last_agent"] = agent
|
|
7458
7687
|
write_session(root, session, session_file)
|
|
7459
7688
|
snapshot = snapshot_state(root, session_file, session)
|
|
7460
|
-
snapshot["action"] = "clear-
|
|
7689
|
+
snapshot["action"] = "clear-unit-test-mode"
|
|
7461
7690
|
return snapshot
|
|
7462
7691
|
|
|
7463
7692
|
|
|
@@ -7818,6 +8047,7 @@ def handoff_task(
|
|
|
7818
8047
|
summary: str,
|
|
7819
8048
|
task_id: str | None = None,
|
|
7820
8049
|
session_file: str | Path | None = None,
|
|
8050
|
+
continuation: dict | None = None,
|
|
7821
8051
|
) -> dict:
|
|
7822
8052
|
if not summary.strip():
|
|
7823
8053
|
raise StateError("Handoff summary is required.")
|
|
@@ -7831,6 +8061,24 @@ def handoff_task(
|
|
|
7831
8061
|
stage = str(task.get("status") or "PENDING")
|
|
7832
8062
|
if stage in TERMINAL_STATUSES:
|
|
7833
8063
|
raise StateError(f"Cannot hand off terminal task: {resolved_task_id}")
|
|
8064
|
+
if continuation is not None:
|
|
8065
|
+
if not isinstance(continuation, dict) or set(continuation) - {
|
|
8066
|
+
"next_action", "unit_ids", "evidence_refs", "stop_after", "repair_id"
|
|
8067
|
+
}:
|
|
8068
|
+
raise StateError("Unknown handoff continuation fields.")
|
|
8069
|
+
if continuation.get("next_action") not in {"implement", "repair", "quality", "continue"}:
|
|
8070
|
+
raise StateError("Handoff must name the next action.")
|
|
8071
|
+
plan = latest_execution_plan(root, str(resolved_task_id)) or {}
|
|
8072
|
+
unit_ids = continuation.get("unit_ids", [])
|
|
8073
|
+
if not is_string_list(unit_ids) or not set(unit_ids) <= {u["id"] for u in plan.get("units", [])}:
|
|
8074
|
+
raise StateError("Handoff units must belong to the existing plan.")
|
|
8075
|
+
refs = continuation.get("evidence_refs", [])
|
|
8076
|
+
count = len(execution_records(root, str(resolved_task_id)))
|
|
8077
|
+
if not isinstance(refs, list) or any(type(i) is not int or i < 0 or i >= count for i in refs):
|
|
8078
|
+
raise StateError("Handoff evidence must reference existing execution records.")
|
|
8079
|
+
if continuation.get("stop_after") not in {None, "IMPLEMENT", "repair"}:
|
|
8080
|
+
raise StateError("Unknown handoff stop point.")
|
|
8081
|
+
task["continuation"] = continuation
|
|
7834
8082
|
|
|
7835
8083
|
record = {
|
|
7836
8084
|
"type": "handoff",
|
|
@@ -7838,6 +8086,7 @@ def handoff_task(
|
|
|
7838
8086
|
"stage": stage,
|
|
7839
8087
|
"summary": summary.strip(),
|
|
7840
8088
|
"timestamp": now_iso(),
|
|
8089
|
+
**(continuation or {}),
|
|
7841
8090
|
}
|
|
7842
8091
|
append_execution_record(root, str(resolved_task_id), record)
|
|
7843
8092
|
task["last_agent"] = agent
|
|
@@ -7873,6 +8122,9 @@ def claim_task(root: Path, task_id: str, agent: str, session_file: str | Path |
|
|
|
7873
8122
|
else "takeover"
|
|
7874
8123
|
)
|
|
7875
8124
|
latest_handoff = latest_handoff_record(root, task_id)
|
|
8125
|
+
already_current = (session.get("current_task") == task_id
|
|
8126
|
+
and agents_equivalent(previous_agent, agent)
|
|
8127
|
+
and pending_handoff_record(root, task_id) is None)
|
|
7876
8128
|
task["last_agent"] = agent
|
|
7877
8129
|
write_task(root, task_id, task)
|
|
7878
8130
|
|
|
@@ -7889,7 +8141,8 @@ def claim_task(root: Path, task_id: str, agent: str, session_file: str | Path |
|
|
|
7889
8141
|
"action": action,
|
|
7890
8142
|
"timestamp": now_iso(),
|
|
7891
8143
|
}
|
|
7892
|
-
|
|
8144
|
+
if not already_current:
|
|
8145
|
+
append_execution_record(root, task_id, claim)
|
|
7893
8146
|
|
|
7894
8147
|
context = restore_spec_context(root, task_id, task, agent, session_file)
|
|
7895
8148
|
snapshot = snapshot_state(root, session_file, session)
|
|
@@ -7900,9 +8153,32 @@ def claim_task(root: Path, task_id: str, agent: str, session_file: str | Path |
|
|
|
7900
8153
|
snapshot["previous_agent"] = previous_agent
|
|
7901
8154
|
snapshot["latest_handoff"] = latest_handoff
|
|
7902
8155
|
snapshot["claim"] = claim
|
|
8156
|
+
snapshot["claim_recorded"] = not already_current
|
|
7903
8157
|
return snapshot
|
|
7904
8158
|
|
|
7905
8159
|
|
|
8160
|
+
def set_cooperate_mode(root: Path, mode: str | None, agent: str,
|
|
8161
|
+
session_file: str | Path | None = None) -> dict:
|
|
8162
|
+
if mode not in {None, "default", "dispatch"}:
|
|
8163
|
+
raise StateError("cooperate_mode must be default or dispatch.")
|
|
8164
|
+
session = ensure_session(root, session_file)
|
|
8165
|
+
if mode is None:
|
|
8166
|
+
session.pop("cooperate_mode", None)
|
|
8167
|
+
else:
|
|
8168
|
+
session["cooperate_mode"] = mode
|
|
8169
|
+
write_session(root, session, session_file)
|
|
8170
|
+
task_id = session.get("current_task")
|
|
8171
|
+
task = load_task(root, task_id)
|
|
8172
|
+
if task and pending_handoff_record(root, task_id) is None:
|
|
8173
|
+
task["cooperation"] = {
|
|
8174
|
+
"mode": behavior_layers(root, session)["cooperate_mode"]["value"],
|
|
8175
|
+
"coordinator": (task.get("cooperation") or {}).get("coordinator") or {
|
|
8176
|
+
"agent": agent, "session_file": display_path(root, resolve_session_path(root, session_file))},
|
|
8177
|
+
}
|
|
8178
|
+
write_task(root, task_id, task)
|
|
8179
|
+
return snapshot_state(root, session_file, session)
|
|
8180
|
+
|
|
8181
|
+
|
|
7906
8182
|
def create_task(
|
|
7907
8183
|
root: Path,
|
|
7908
8184
|
task_id: str,
|
|
@@ -7933,6 +8209,8 @@ def create_task(
|
|
|
7933
8209
|
"created_by": agent,
|
|
7934
8210
|
"last_agent": agent,
|
|
7935
8211
|
"stage_history": [{"stage": "INIT", "agent": agent, "entered_at": timestamp}],
|
|
8212
|
+
"cooperation": {"mode": behavior_layers(root, session)["cooperate_mode"]["value"],
|
|
8213
|
+
"coordinator": {"agent": agent, "session_file": display_path(root, resolve_session_path(root, session_file))}},
|
|
7936
8214
|
"context": {},
|
|
7937
8215
|
"spawned_from": None,
|
|
7938
8216
|
"spawned_tasks": [],
|
|
@@ -9035,6 +9313,7 @@ def sync_spec_design_state(
|
|
|
9035
9313
|
progress.pop("pending_action", None)
|
|
9036
9314
|
task.pop("spec_change", None)
|
|
9037
9315
|
task.pop("spec_context", None)
|
|
9316
|
+
task.pop("spec_contexts", None)
|
|
9038
9317
|
already_acknowledged = any(
|
|
9039
9318
|
record.get("type") == "spec-design-sync"
|
|
9040
9319
|
and record.get("idempotency_key") == idempotency_key
|
|
@@ -9143,6 +9422,7 @@ def writeback_ready_tasks_for_implement(
|
|
|
9143
9422
|
agent: str,
|
|
9144
9423
|
restart_statuses: set[str] | None = None,
|
|
9145
9424
|
source_task_ids: set[str] | None = None,
|
|
9425
|
+
*, repair_id: str | None = None,
|
|
9146
9426
|
) -> None:
|
|
9147
9427
|
inspection, _ = inspect_task_spec(root, task)
|
|
9148
9428
|
implement_attempt = 1 + sum(
|
|
@@ -9170,11 +9450,15 @@ def writeback_ready_tasks_for_implement(
|
|
|
9170
9450
|
f"{harness_task_id}:{source_task_id}:enter-implement:"
|
|
9171
9451
|
f"{task['spec_source']['revision']}:attempt-{implement_attempt}"
|
|
9172
9452
|
)
|
|
9453
|
+
if repair_id:
|
|
9454
|
+
key = f"{harness_task_id}:{source_task_id}:quality-repair:{repair_id}:start"
|
|
9455
|
+
summary = ("Harness started an approved repair within QUALITY" if repair_id else
|
|
9456
|
+
"Harness entered IMPLEMENT for a dependency-ready Canonical task")
|
|
9173
9457
|
action = {
|
|
9174
9458
|
"kind": "task",
|
|
9175
9459
|
"source_task_id": source_task_id,
|
|
9176
9460
|
"status": "in_progress",
|
|
9177
|
-
"summary":
|
|
9461
|
+
"summary": summary,
|
|
9178
9462
|
"evidence": [],
|
|
9179
9463
|
"idempotency_key": key,
|
|
9180
9464
|
"agent": agent,
|
|
@@ -9189,7 +9473,7 @@ def writeback_ready_tasks_for_implement(
|
|
|
9189
9473
|
stored_spec_path(root, task),
|
|
9190
9474
|
str(source_task_id),
|
|
9191
9475
|
"in_progress",
|
|
9192
|
-
|
|
9476
|
+
summary,
|
|
9193
9477
|
SPEC_WRITEBACK_APP,
|
|
9194
9478
|
spec_writeback_agent(agent),
|
|
9195
9479
|
design_digest,
|
|
@@ -9711,35 +9995,36 @@ def freeze_workflow_mode(
|
|
|
9711
9995
|
task["workflow_mode_confirmed_by"] = agent
|
|
9712
9996
|
|
|
9713
9997
|
|
|
9714
|
-
def
|
|
9998
|
+
def freeze_unit_test_mode(
|
|
9715
9999
|
root: Path, session: dict, task_id: str, task: dict, agent: str
|
|
9716
10000
|
) -> None:
|
|
9717
10001
|
behavior = resolve_behavior(root, session)
|
|
9718
10002
|
task_type = str(task.get("type") or "").strip().lower()
|
|
9719
|
-
task["
|
|
9720
|
-
behavior[8] if task_type != TDD_INIT_TASK_TYPE else
|
|
10003
|
+
task["unit_test_mode"] = (
|
|
10004
|
+
behavior[8] if task_type != TDD_INIT_TASK_TYPE else "none"
|
|
9721
10005
|
)
|
|
9722
|
-
task["
|
|
9723
|
-
if task["
|
|
10006
|
+
task["ut_coverage_threshold"] = behavior[11]
|
|
10007
|
+
if task["unit_test_mode"] in {"ut", "tdd"}:
|
|
9724
10008
|
require_tdd_readiness(root)
|
|
9725
10009
|
plan = latest_execution_plan(root, task_id)
|
|
9726
10010
|
if plan is None:
|
|
9727
|
-
raise StateError("Cannot freeze
|
|
10011
|
+
raise StateError("Cannot freeze unit test baseline without a valid execution plan.")
|
|
9728
10012
|
baselines = {
|
|
9729
10013
|
key: git_head_sha(repository)
|
|
9730
10014
|
for key, repository in tdd_repositories(root, task, plan).items()
|
|
9731
10015
|
}
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
10016
|
+
if task["unit_test_mode"] == "tdd":
|
|
10017
|
+
task_dir = task_json_path(root, task_id).parent
|
|
10018
|
+
try:
|
|
10019
|
+
dev_spec_content = (task_dir / "dev-spec.md").read_text(encoding="utf-8")
|
|
10020
|
+
strategy_content = (task_dir / "test-strategy.md").read_text(encoding="utf-8")
|
|
10021
|
+
except OSError as error:
|
|
10022
|
+
raise StateError("Cannot freeze TDD without readable analysis artifacts.") from error
|
|
10023
|
+
marker_reasons = tdd_baseline_marker_reasons(
|
|
10024
|
+
dev_spec_content, strategy_content, baselines
|
|
10025
|
+
)
|
|
10026
|
+
if marker_reasons:
|
|
10027
|
+
raise StateError("; ".join(marker_reasons))
|
|
9743
10028
|
task["tdd_baselines"] = baselines
|
|
9744
10029
|
else:
|
|
9745
10030
|
task.pop("tdd_baselines", None)
|
|
@@ -9899,7 +10184,7 @@ def apply_transition(
|
|
|
9899
10184
|
validate_analysis_readiness(root, resolved_task_id, session)
|
|
9900
10185
|
if task.get("workflow_mode_legacy") is not True:
|
|
9901
10186
|
freeze_workflow_mode(root, session, resolved_task_id, task, agent)
|
|
9902
|
-
|
|
10187
|
+
freeze_unit_test_mode(root, session, resolved_task_id, task, agent)
|
|
9903
10188
|
repair_source_task_ids: set[str] | None = None
|
|
9904
10189
|
quality_exit_outcome: str | None = None
|
|
9905
10190
|
if previous == "QUALITY" and stage in {"IMPLEMENT", "ANALYSIS"}:
|
|
@@ -10611,13 +10896,13 @@ def main() -> int:
|
|
|
10611
10896
|
clear_workflow_mode_parser = subcommands.add_parser("clear-workflow-mode", parents=[common])
|
|
10612
10897
|
clear_workflow_mode_parser.add_argument("--agent", required=True)
|
|
10613
10898
|
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10899
|
+
set_unit_test_parser = subcommands.add_parser("set-unit-test-mode", parents=[common])
|
|
10900
|
+
set_unit_test_parser.add_argument("--mode", required=True, choices=["none", "ut", "tdd"])
|
|
10901
|
+
set_unit_test_parser.add_argument("--threshold", type=int)
|
|
10902
|
+
set_unit_test_parser.add_argument("--agent", required=True)
|
|
10618
10903
|
|
|
10619
|
-
|
|
10620
|
-
|
|
10904
|
+
clear_unit_test_parser = subcommands.add_parser("clear-unit-test-mode", parents=[common])
|
|
10905
|
+
clear_unit_test_parser.add_argument("--agent", required=True)
|
|
10621
10906
|
|
|
10622
10907
|
# Compatibility aliases for pre-0.9 callers.
|
|
10623
10908
|
set_confirm_mode_parser = subcommands.add_parser("set-confirm-mode", parents=[common])
|
|
@@ -10671,7 +10956,7 @@ def main() -> int:
|
|
|
10671
10956
|
if name == "prepare-check":
|
|
10672
10957
|
check_parser.add_argument("--record", required=True)
|
|
10673
10958
|
else:
|
|
10674
|
-
check_parser.add_argument("--prepared-id"
|
|
10959
|
+
check_parser.add_argument("--prepared-id")
|
|
10675
10960
|
check_parser.add_argument("--result", required=True)
|
|
10676
10961
|
correction_parser = subcommands.add_parser("begin-correction", parents=[common])
|
|
10677
10962
|
correction_parser.add_argument("--file", action="append", required=True)
|
|
@@ -10680,6 +10965,21 @@ def main() -> int:
|
|
|
10680
10965
|
correction_parser.add_argument("--agent", required=True)
|
|
10681
10966
|
correction_parser.add_argument("--task-id")
|
|
10682
10967
|
|
|
10968
|
+
for name in ("start-quality-repair", "complete-quality-repair"):
|
|
10969
|
+
repair_parser = subcommands.add_parser(name, parents=[common])
|
|
10970
|
+
repair_parser.add_argument("--repair-id", required=True)
|
|
10971
|
+
repair_parser.add_argument("--agent", required=True)
|
|
10972
|
+
repair_parser.add_argument("--task-id")
|
|
10973
|
+
if name == "start-quality-repair":
|
|
10974
|
+
repair_parser.add_argument("--executor", choices=["current", "other"], required=True)
|
|
10975
|
+
repair_parser.add_argument("--confirmed", action="store_true")
|
|
10976
|
+
|
|
10977
|
+
for name in ("set-cooperate-mode", "clear-cooperate-mode"):
|
|
10978
|
+
cooperate_parser = subcommands.add_parser(name, parents=[common])
|
|
10979
|
+
cooperate_parser.add_argument("--agent", required=True)
|
|
10980
|
+
if name == "set-cooperate-mode":
|
|
10981
|
+
cooperate_parser.add_argument("--mode", choices=["default", "dispatch"], required=True)
|
|
10982
|
+
|
|
10683
10983
|
finalize_quality_parser = subcommands.add_parser(
|
|
10684
10984
|
"finalize-quality", parents=[common]
|
|
10685
10985
|
)
|
|
@@ -10757,6 +11057,7 @@ def main() -> int:
|
|
|
10757
11057
|
handoff.add_argument("--agent", required=True)
|
|
10758
11058
|
handoff.add_argument("--summary", required=True)
|
|
10759
11059
|
handoff.add_argument("--task-id")
|
|
11060
|
+
handoff.add_argument("--continuation", help="JSON next_action, unit_ids, evidence_refs and stop_after")
|
|
10760
11061
|
|
|
10761
11062
|
claim = subcommands.add_parser("claim-task", parents=[common])
|
|
10762
11063
|
claim.add_argument("--task-id", required=True)
|
|
@@ -11128,13 +11429,13 @@ def main() -> int:
|
|
|
11128
11429
|
session_file,
|
|
11129
11430
|
)
|
|
11130
11431
|
)
|
|
11131
|
-
elif command == "set-
|
|
11432
|
+
elif command == "set-unit-test-mode":
|
|
11132
11433
|
emit(
|
|
11133
11434
|
attach_status_context(
|
|
11134
11435
|
root,
|
|
11135
|
-
|
|
11436
|
+
set_session_unit_test_mode(
|
|
11136
11437
|
root,
|
|
11137
|
-
args.
|
|
11438
|
+
args.mode,
|
|
11138
11439
|
agent,
|
|
11139
11440
|
args.threshold,
|
|
11140
11441
|
session_file,
|
|
@@ -11143,11 +11444,11 @@ def main() -> int:
|
|
|
11143
11444
|
session_file,
|
|
11144
11445
|
)
|
|
11145
11446
|
)
|
|
11146
|
-
elif command == "clear-
|
|
11447
|
+
elif command == "clear-unit-test-mode":
|
|
11147
11448
|
emit(
|
|
11148
11449
|
attach_status_context(
|
|
11149
11450
|
root,
|
|
11150
|
-
|
|
11451
|
+
clear_session_unit_test_mode(root, agent, session_file),
|
|
11151
11452
|
agent,
|
|
11152
11453
|
session_file,
|
|
11153
11454
|
)
|
|
@@ -11254,12 +11555,26 @@ def main() -> int:
|
|
|
11254
11555
|
session, task_id, task = resolve_current_task(root, args.task_id, session_file)
|
|
11255
11556
|
require_spec_context(root, task, agent, session_file)
|
|
11256
11557
|
if command == "prepare-check":
|
|
11257
|
-
|
|
11558
|
+
descriptors = json.loads(args.record)
|
|
11559
|
+
result = ([prepare_check(root, task_id, task, item, agent) for item in descriptors]
|
|
11560
|
+
if isinstance(descriptors, list) else prepare_check(root, task_id, task, descriptors, agent))
|
|
11258
11561
|
elif command == "record-check":
|
|
11259
|
-
|
|
11562
|
+
results = json.loads(args.result)
|
|
11563
|
+
result = ([record_check(root, task_id, task, item["prepared_id"], item["result"], agent) for item in results]
|
|
11564
|
+
if isinstance(results, list) else record_check(root, task_id, task, args.prepared_id, results, agent))
|
|
11260
11565
|
else:
|
|
11261
11566
|
result = begin_correction(root, task_id, task, args.file, args.summary, args.risk, agent)
|
|
11262
11567
|
emit(result)
|
|
11568
|
+
elif command in {"start-quality-repair", "complete-quality-repair"}:
|
|
11569
|
+
if command == "start-quality-repair":
|
|
11570
|
+
result = start_quality_repair(root, args.repair_id, args.executor, args.confirmed,
|
|
11571
|
+
agent, args.task_id, session_file)
|
|
11572
|
+
else:
|
|
11573
|
+
result = complete_quality_repair(root, args.repair_id, agent, args.task_id, session_file)
|
|
11574
|
+
emit(attach_status_context(root, result, agent, session_file))
|
|
11575
|
+
elif command in {"set-cooperate-mode", "clear-cooperate-mode"}:
|
|
11576
|
+
emit(attach_status_context(root, set_cooperate_mode(root,
|
|
11577
|
+
args.mode if command == "set-cooperate-mode" else None, agent, session_file), agent, session_file))
|
|
11263
11578
|
elif command == "finalize-quality":
|
|
11264
11579
|
emit(
|
|
11265
11580
|
attach_status_context(
|
|
@@ -11376,7 +11691,8 @@ def main() -> int:
|
|
|
11376
11691
|
emit(
|
|
11377
11692
|
attach_status_context(
|
|
11378
11693
|
root,
|
|
11379
|
-
handoff_task(root, agent, args.summary, args.task_id, session_file
|
|
11694
|
+
handoff_task(root, agent, args.summary, args.task_id, session_file,
|
|
11695
|
+
json.loads(args.continuation) if args.continuation else None),
|
|
11380
11696
|
agent,
|
|
11381
11697
|
session_file,
|
|
11382
11698
|
)
|