easy-coding-harness 0.10.0-beta.7 → 0.10.0-beta.8
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 +17 -0
- package/README.md +10 -3
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +10 -4
- package/templates/common/skills/ec-config/SKILL.md +6 -0
- package/templates/common/skills/ec-memory/SKILL.md +7 -0
- package/templates/common/skills/ec-verification/SKILL.md +45 -9
- package/templates/common/skills/ec-workflow/SKILL.md +22 -2
- package/templates/main-constraint/AGENTS.md.tpl +13 -4
- package/templates/main-constraint/CLAUDE.md.tpl +13 -4
- package/templates/shared-hooks/easy_coding_state.py +1088 -10
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import argparse
|
|
3
|
+
import base64
|
|
4
|
+
import difflib
|
|
3
5
|
import hashlib
|
|
4
6
|
import json
|
|
5
7
|
import os
|
|
@@ -139,6 +141,8 @@ ARCHITECTURE_ABSTRACT_PATH = Path(".easy-coding/ABSTRACT.md")
|
|
|
139
141
|
ARCHITECTURE_CHANGELOG_PATH = Path(".easy-coding/CHANGELOG.md")
|
|
140
142
|
# MEMORY 架构评估唯一允许的动作集合;状态 API 和 CLI 参数共享该契约。
|
|
141
143
|
ARCHITECTURE_ACTIONS = {"no-op", "backfill", "update"}
|
|
144
|
+
ACCEPTANCE_SNAPSHOT_SCHEMA = 1
|
|
145
|
+
ACCEPTANCE_VERIFICATION_POLICIES = {"carry-forward", "targeted", "waived"}
|
|
142
146
|
SESSION_STALE_THRESHOLD_HOURS = 30 * 24
|
|
143
147
|
SESSION_COMPONENT_PATTERN = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
144
148
|
SESSION_AGENT_NAMESPACES = {"claude-code", "codex", "qoder", "unknown"}
|
|
@@ -2751,6 +2755,665 @@ def evidence_fingerprints(root: Path, task_id: str) -> dict[str, str]:
|
|
|
2751
2755
|
}
|
|
2752
2756
|
|
|
2753
2757
|
|
|
2758
|
+
def acceptance_snapshot_path(root: Path, task_id: str) -> Path:
|
|
2759
|
+
assert_safe_task_id(task_id)
|
|
2760
|
+
return root / ".easy-coding" / "sessions" / "acceptance" / f"{task_id}.json"
|
|
2761
|
+
|
|
2762
|
+
|
|
2763
|
+
def canonical_json_sha256(value: object) -> str:
|
|
2764
|
+
payload = json.dumps(
|
|
2765
|
+
value,
|
|
2766
|
+
ensure_ascii=False,
|
|
2767
|
+
sort_keys=True,
|
|
2768
|
+
separators=(",", ":"),
|
|
2769
|
+
).encode("utf-8")
|
|
2770
|
+
return hashlib.sha256(payload).hexdigest()
|
|
2771
|
+
|
|
2772
|
+
|
|
2773
|
+
def verification_contract_fingerprint(root: Path, task_id: str, task: dict) -> str:
|
|
2774
|
+
plan = latest_execution_plan(root, task_id)
|
|
2775
|
+
if plan is None:
|
|
2776
|
+
raise StateError("Cannot fingerprint verification contract without a valid plan.")
|
|
2777
|
+
source = task.get("spec_source") if isinstance(task.get("spec_source"), dict) else {}
|
|
2778
|
+
contract = {
|
|
2779
|
+
"workflow_mode": task.get("workflow_mode"),
|
|
2780
|
+
"tdd_enabled": task.get("tdd_enabled"),
|
|
2781
|
+
"tdd_coverage_threshold": task.get("tdd_coverage_threshold"),
|
|
2782
|
+
"tdd_baselines": task.get("tdd_baselines"),
|
|
2783
|
+
"plan": plan,
|
|
2784
|
+
"canonical": {
|
|
2785
|
+
"schema": source.get("schema"),
|
|
2786
|
+
"spec_id": source.get("spec_id"),
|
|
2787
|
+
"revision": source.get("revision"),
|
|
2788
|
+
"design_sha256": source.get("design_sha256"),
|
|
2789
|
+
"selected_tasks": task.get("selected_spec_tasks"),
|
|
2790
|
+
"repository_bindings": task.get("spec_repositories"),
|
|
2791
|
+
"repo_paths": task.get("repo_paths"),
|
|
2792
|
+
}
|
|
2793
|
+
if source
|
|
2794
|
+
else None,
|
|
2795
|
+
}
|
|
2796
|
+
return canonical_json_sha256(contract)
|
|
2797
|
+
|
|
2798
|
+
|
|
2799
|
+
def acceptance_repository_entries(repository: Path, scopes: list[Path]) -> list[dict]:
|
|
2800
|
+
pathspecs = repository_scope_pathspecs(repository, scopes)
|
|
2801
|
+
index_entries = git_index_entries(repository, pathspecs)
|
|
2802
|
+
listed = run_git(
|
|
2803
|
+
repository,
|
|
2804
|
+
"ls-files",
|
|
2805
|
+
"--cached",
|
|
2806
|
+
"--others",
|
|
2807
|
+
"--exclude-standard",
|
|
2808
|
+
"-z",
|
|
2809
|
+
"--",
|
|
2810
|
+
*pathspecs,
|
|
2811
|
+
)
|
|
2812
|
+
modified = run_git(
|
|
2813
|
+
repository,
|
|
2814
|
+
"diff-files",
|
|
2815
|
+
"--name-only",
|
|
2816
|
+
"-z",
|
|
2817
|
+
"--ignore-submodules=none",
|
|
2818
|
+
"--",
|
|
2819
|
+
*pathspecs,
|
|
2820
|
+
)
|
|
2821
|
+
if listed is None or listed.returncode != 0 or modified is None or modified.returncode != 0:
|
|
2822
|
+
raise StateError(f"Cannot capture verification snapshot for {repository}.")
|
|
2823
|
+
modified_paths = set(filter(None, modified.stdout.split(b"\0")))
|
|
2824
|
+
raw_paths = set(filter(None, listed.stdout.split(b"\0"))) | set(index_entries)
|
|
2825
|
+
entries: list[dict] = []
|
|
2826
|
+
for raw_path in sorted(raw_paths):
|
|
2827
|
+
relative_name = os.fsdecode(raw_path)
|
|
2828
|
+
if is_easy_coding_state_path(repository, relative_name, scopes):
|
|
2829
|
+
continue
|
|
2830
|
+
candidate = repository / relative_name
|
|
2831
|
+
index_entry = index_entries.get(raw_path)
|
|
2832
|
+
if index_entry is not None and index_entry[0] == b"160000":
|
|
2833
|
+
entries.append(
|
|
2834
|
+
{
|
|
2835
|
+
"path": relative_name,
|
|
2836
|
+
"exists": True,
|
|
2837
|
+
"mode": "160000",
|
|
2838
|
+
"git_oid": index_entry[1].decode("ascii", errors="replace"),
|
|
2839
|
+
"sha256": hashlib.sha256(index_entry[1]).hexdigest(),
|
|
2840
|
+
}
|
|
2841
|
+
)
|
|
2842
|
+
continue
|
|
2843
|
+
exists = candidate.exists() or candidate.is_symlink()
|
|
2844
|
+
if not exists:
|
|
2845
|
+
entries.append(
|
|
2846
|
+
{
|
|
2847
|
+
"path": relative_name,
|
|
2848
|
+
"exists": False,
|
|
2849
|
+
"mode": None,
|
|
2850
|
+
"sha256": None,
|
|
2851
|
+
}
|
|
2852
|
+
)
|
|
2853
|
+
continue
|
|
2854
|
+
try:
|
|
2855
|
+
content = (
|
|
2856
|
+
os.fsencode(os.readlink(candidate))
|
|
2857
|
+
if candidate.is_symlink()
|
|
2858
|
+
else candidate.read_bytes()
|
|
2859
|
+
)
|
|
2860
|
+
except OSError as exc:
|
|
2861
|
+
raise StateError(f"Cannot read verification snapshot file: {relative_name}") from exc
|
|
2862
|
+
mode = worktree_git_mode(candidate).decode("ascii", errors="replace")
|
|
2863
|
+
entry = {
|
|
2864
|
+
"path": relative_name,
|
|
2865
|
+
"exists": True,
|
|
2866
|
+
"mode": mode,
|
|
2867
|
+
"sha256": hashlib.sha256(content).hexdigest(),
|
|
2868
|
+
}
|
|
2869
|
+
if index_entry is not None and raw_path not in modified_paths:
|
|
2870
|
+
entry["git_oid"] = index_entry[1].decode("ascii", errors="replace")
|
|
2871
|
+
else:
|
|
2872
|
+
# 仅无法从 Git object 还原的工作区内容进入被忽略的临时快照。
|
|
2873
|
+
entry["content_b64"] = base64.b64encode(content).decode("ascii")
|
|
2874
|
+
entries.append(entry)
|
|
2875
|
+
return entries
|
|
2876
|
+
|
|
2877
|
+
|
|
2878
|
+
def acceptance_filesystem_repositories(
|
|
2879
|
+
root: Path,
|
|
2880
|
+
plan: dict,
|
|
2881
|
+
git_scopes: list[tuple[Path, list[Path]]],
|
|
2882
|
+
) -> list[dict]:
|
|
2883
|
+
files_by_root: dict[Path, set[Path]] = {}
|
|
2884
|
+
for unit in plan.get("units", []):
|
|
2885
|
+
if not isinstance(unit, dict):
|
|
2886
|
+
continue
|
|
2887
|
+
for file_name in unit.get("files", []):
|
|
2888
|
+
if not is_non_empty_string(file_name):
|
|
2889
|
+
continue
|
|
2890
|
+
raw_path = Path(str(file_name))
|
|
2891
|
+
base = root.resolve()
|
|
2892
|
+
candidate = raw_path if raw_path.is_absolute() else base / raw_path
|
|
2893
|
+
resolved = candidate.resolve()
|
|
2894
|
+
if not raw_path.is_absolute() and not is_path_within(resolved, base):
|
|
2895
|
+
raise StateError(f"Execution plan file escapes project: {file_name}")
|
|
2896
|
+
if any(
|
|
2897
|
+
is_path_within(resolved, scope)
|
|
2898
|
+
for _repository, scopes in git_scopes
|
|
2899
|
+
for scope in scopes
|
|
2900
|
+
):
|
|
2901
|
+
continue
|
|
2902
|
+
snapshot_root = resolved.parent if raw_path.is_absolute() else base
|
|
2903
|
+
files_by_root.setdefault(snapshot_root, set()).add(resolved)
|
|
2904
|
+
|
|
2905
|
+
repositories: list[dict] = []
|
|
2906
|
+
for snapshot_root, files in sorted(
|
|
2907
|
+
files_by_root.items(), key=lambda item: item[0].as_posix()
|
|
2908
|
+
):
|
|
2909
|
+
entries = []
|
|
2910
|
+
for candidate in sorted(files, key=lambda item: item.as_posix()):
|
|
2911
|
+
relative_name = candidate.relative_to(snapshot_root).as_posix()
|
|
2912
|
+
exists = candidate.exists() or candidate.is_symlink()
|
|
2913
|
+
if not exists:
|
|
2914
|
+
entries.append(
|
|
2915
|
+
{
|
|
2916
|
+
"path": relative_name,
|
|
2917
|
+
"exists": False,
|
|
2918
|
+
"mode": None,
|
|
2919
|
+
"sha256": None,
|
|
2920
|
+
}
|
|
2921
|
+
)
|
|
2922
|
+
continue
|
|
2923
|
+
try:
|
|
2924
|
+
content = (
|
|
2925
|
+
os.fsencode(os.readlink(candidate))
|
|
2926
|
+
if candidate.is_symlink()
|
|
2927
|
+
else candidate.read_bytes()
|
|
2928
|
+
)
|
|
2929
|
+
except OSError as exc:
|
|
2930
|
+
raise StateError(
|
|
2931
|
+
f"Cannot read verification snapshot file: {candidate}"
|
|
2932
|
+
) from exc
|
|
2933
|
+
entries.append(
|
|
2934
|
+
{
|
|
2935
|
+
"path": relative_name,
|
|
2936
|
+
"exists": True,
|
|
2937
|
+
"mode": worktree_git_mode(candidate).decode("ascii", errors="replace"),
|
|
2938
|
+
"sha256": hashlib.sha256(content).hexdigest(),
|
|
2939
|
+
"content_b64": base64.b64encode(content).decode("ascii"),
|
|
2940
|
+
}
|
|
2941
|
+
)
|
|
2942
|
+
repositories.append(
|
|
2943
|
+
{
|
|
2944
|
+
"root": str(snapshot_root),
|
|
2945
|
+
"display": display_path(root, snapshot_root),
|
|
2946
|
+
"scopes": [],
|
|
2947
|
+
"entries": entries,
|
|
2948
|
+
}
|
|
2949
|
+
)
|
|
2950
|
+
return repositories
|
|
2951
|
+
|
|
2952
|
+
|
|
2953
|
+
def build_acceptance_snapshot(root: Path, task_id: str, task: dict) -> dict:
|
|
2954
|
+
plan = latest_execution_plan(root, task_id)
|
|
2955
|
+
if plan is None:
|
|
2956
|
+
raise StateError("Cannot capture verification snapshot without a valid plan.")
|
|
2957
|
+
fingerprints = evidence_fingerprints(root, task_id)
|
|
2958
|
+
repository_scopes = task_repository_scopes(root, task, plan)
|
|
2959
|
+
repositories = []
|
|
2960
|
+
for repository, scopes in repository_scopes:
|
|
2961
|
+
repositories.append(
|
|
2962
|
+
{
|
|
2963
|
+
"root": str(repository.resolve()),
|
|
2964
|
+
"display": display_path(root, repository.resolve()),
|
|
2965
|
+
"scopes": [
|
|
2966
|
+
scope.relative_to(repository.resolve()).as_posix() for scope in scopes
|
|
2967
|
+
],
|
|
2968
|
+
"entries": acceptance_repository_entries(repository.resolve(), scopes),
|
|
2969
|
+
}
|
|
2970
|
+
)
|
|
2971
|
+
repositories.extend(acceptance_filesystem_repositories(root, plan, repository_scopes))
|
|
2972
|
+
return {
|
|
2973
|
+
"schema": ACCEPTANCE_SNAPSHOT_SCHEMA,
|
|
2974
|
+
**fingerprints,
|
|
2975
|
+
"contract_fingerprint": verification_contract_fingerprint(root, task_id, task),
|
|
2976
|
+
"repositories": repositories,
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
|
|
2980
|
+
def load_acceptance_snapshot(root: Path, task: dict) -> dict:
|
|
2981
|
+
checkpoint = task.get("verification_checkpoint")
|
|
2982
|
+
if not isinstance(checkpoint, dict):
|
|
2983
|
+
raise StateError("VERIFICATION has no frozen acceptance checkpoint.")
|
|
2984
|
+
raw_path = checkpoint.get("snapshot_file")
|
|
2985
|
+
if not is_non_empty_string(raw_path):
|
|
2986
|
+
raise StateError("Verification checkpoint has no snapshot file.")
|
|
2987
|
+
candidate = (root / str(raw_path)).resolve()
|
|
2988
|
+
sessions_root = (root / ".easy-coding" / "sessions").resolve()
|
|
2989
|
+
if not is_path_within(candidate, sessions_root):
|
|
2990
|
+
raise StateError("Verification checkpoint snapshot escapes .easy-coding/sessions.")
|
|
2991
|
+
snapshot = load_json(candidate)
|
|
2992
|
+
if not isinstance(snapshot, dict) or snapshot.get("schema") != ACCEPTANCE_SNAPSHOT_SCHEMA:
|
|
2993
|
+
raise StateError("Verification checkpoint snapshot is missing or invalid.")
|
|
2994
|
+
if canonical_json_sha256(snapshot) != checkpoint.get("snapshot_sha256"):
|
|
2995
|
+
raise StateError("Verification checkpoint snapshot fingerprint changed.")
|
|
2996
|
+
if (
|
|
2997
|
+
snapshot.get("implementation_fingerprint")
|
|
2998
|
+
!= checkpoint.get("implementation_fingerprint")
|
|
2999
|
+
or snapshot.get("config_fingerprint") != checkpoint.get("config_fingerprint")
|
|
3000
|
+
or snapshot.get("contract_fingerprint") != checkpoint.get("contract_fingerprint")
|
|
3001
|
+
):
|
|
3002
|
+
raise StateError("Verification checkpoint metadata does not match its snapshot.")
|
|
3003
|
+
return snapshot
|
|
3004
|
+
|
|
3005
|
+
|
|
3006
|
+
def snapshot_entry_content(repository: Path, entry: dict | None) -> bytes | None:
|
|
3007
|
+
if not isinstance(entry, dict) or entry.get("exists") is not True:
|
|
3008
|
+
return None
|
|
3009
|
+
encoded = entry.get("content_b64")
|
|
3010
|
+
if isinstance(encoded, str):
|
|
3011
|
+
try:
|
|
3012
|
+
return base64.b64decode(encoded, validate=True)
|
|
3013
|
+
except ValueError as exc:
|
|
3014
|
+
raise StateError("Verification checkpoint contains invalid file content.") from exc
|
|
3015
|
+
object_id = entry.get("git_oid")
|
|
3016
|
+
if not is_non_empty_string(object_id):
|
|
3017
|
+
return None
|
|
3018
|
+
if entry.get("mode") == "160000":
|
|
3019
|
+
return str(object_id).encode("ascii", errors="replace")
|
|
3020
|
+
result = run_git(repository, "cat-file", "blob", str(object_id))
|
|
3021
|
+
if result is None or result.returncode != 0:
|
|
3022
|
+
raise StateError(f"Cannot restore verification checkpoint Git object: {object_id}")
|
|
3023
|
+
return result.stdout
|
|
3024
|
+
|
|
3025
|
+
|
|
3026
|
+
def acceptance_snapshot_entries(snapshot: dict) -> dict[tuple[str, str], tuple[Path, dict]]:
|
|
3027
|
+
entries: dict[tuple[str, str], tuple[Path, dict]] = {}
|
|
3028
|
+
for repository in snapshot.get("repositories", []):
|
|
3029
|
+
if not isinstance(repository, dict) or not is_non_empty_string(repository.get("root")):
|
|
3030
|
+
continue
|
|
3031
|
+
repository_root = Path(str(repository["root"]))
|
|
3032
|
+
for entry in repository.get("entries", []):
|
|
3033
|
+
if isinstance(entry, dict) and is_non_empty_string(entry.get("path")):
|
|
3034
|
+
entries[(str(repository_root), str(entry["path"]))] = (repository_root, entry)
|
|
3035
|
+
return entries
|
|
3036
|
+
|
|
3037
|
+
|
|
3038
|
+
def acceptance_change_patch(
|
|
3039
|
+
path_name: str,
|
|
3040
|
+
previous: bytes | None,
|
|
3041
|
+
current: bytes | None,
|
|
3042
|
+
) -> tuple[bool, str]:
|
|
3043
|
+
if (previous is not None and b"\0" in previous) or (current is not None and b"\0" in current):
|
|
3044
|
+
return True, ""
|
|
3045
|
+
try:
|
|
3046
|
+
previous_text = previous.decode("utf-8") if previous is not None else ""
|
|
3047
|
+
current_text = current.decode("utf-8") if current is not None else ""
|
|
3048
|
+
except UnicodeDecodeError:
|
|
3049
|
+
return True, ""
|
|
3050
|
+
patch = "".join(
|
|
3051
|
+
difflib.unified_diff(
|
|
3052
|
+
previous_text.splitlines(keepends=True),
|
|
3053
|
+
current_text.splitlines(keepends=True),
|
|
3054
|
+
fromfile=f"a/{path_name}" if previous is not None else "/dev/null",
|
|
3055
|
+
tofile=f"b/{path_name}" if current is not None else "/dev/null",
|
|
3056
|
+
)
|
|
3057
|
+
)
|
|
3058
|
+
return False, patch
|
|
3059
|
+
|
|
3060
|
+
|
|
3061
|
+
def inspect_acceptance_drift(root: Path, task_id: str, task: dict) -> dict:
|
|
3062
|
+
checkpoint = task.get("verification_checkpoint")
|
|
3063
|
+
baseline = load_acceptance_snapshot(root, task)
|
|
3064
|
+
current = build_acceptance_snapshot(root, task_id, task)
|
|
3065
|
+
baseline_entries = acceptance_snapshot_entries(baseline)
|
|
3066
|
+
current_entries = acceptance_snapshot_entries(current)
|
|
3067
|
+
changes: list[dict] = []
|
|
3068
|
+
digest_changes: list[dict] = []
|
|
3069
|
+
nested_repository_changed = False
|
|
3070
|
+
for key in sorted(set(baseline_entries) | set(current_entries)):
|
|
3071
|
+
previous_repository, previous_entry = baseline_entries.get(key, (Path(key[0]), None))
|
|
3072
|
+
current_repository, current_entry = current_entries.get(key, (Path(key[0]), None))
|
|
3073
|
+
if (
|
|
3074
|
+
isinstance(previous_entry, dict)
|
|
3075
|
+
and isinstance(current_entry, dict)
|
|
3076
|
+
and previous_entry.get("exists") == current_entry.get("exists")
|
|
3077
|
+
and previous_entry.get("mode") == current_entry.get("mode")
|
|
3078
|
+
and previous_entry.get("sha256") == current_entry.get("sha256")
|
|
3079
|
+
):
|
|
3080
|
+
continue
|
|
3081
|
+
repository = current_repository if isinstance(current_entry, dict) else previous_repository
|
|
3082
|
+
previous_content = snapshot_entry_content(previous_repository, previous_entry)
|
|
3083
|
+
current_content = snapshot_entry_content(current_repository, current_entry)
|
|
3084
|
+
binary, patch = acceptance_change_patch(key[1], previous_content, current_content)
|
|
3085
|
+
change_type = (
|
|
3086
|
+
"added"
|
|
3087
|
+
if previous_content is None and current_content is not None
|
|
3088
|
+
else "deleted"
|
|
3089
|
+
if previous_content is not None and current_content is None
|
|
3090
|
+
else "modified"
|
|
3091
|
+
)
|
|
3092
|
+
label = f"{display_path(root, repository)}:{key[1]}"
|
|
3093
|
+
detail = {
|
|
3094
|
+
"file": label,
|
|
3095
|
+
"repository": display_path(root, repository),
|
|
3096
|
+
"path": key[1],
|
|
3097
|
+
"change_type": change_type,
|
|
3098
|
+
"old_mode": previous_entry.get("mode") if isinstance(previous_entry, dict) else None,
|
|
3099
|
+
"new_mode": current_entry.get("mode") if isinstance(current_entry, dict) else None,
|
|
3100
|
+
"old_sha256": previous_entry.get("sha256")
|
|
3101
|
+
if isinstance(previous_entry, dict)
|
|
3102
|
+
else None,
|
|
3103
|
+
"new_sha256": current_entry.get("sha256")
|
|
3104
|
+
if isinstance(current_entry, dict)
|
|
3105
|
+
else None,
|
|
3106
|
+
"binary": binary,
|
|
3107
|
+
"patch": patch,
|
|
3108
|
+
}
|
|
3109
|
+
if detail["old_mode"] == "160000" or detail["new_mode"] == "160000":
|
|
3110
|
+
nested_repository_changed = True
|
|
3111
|
+
changes.append(detail)
|
|
3112
|
+
digest_changes.append(
|
|
3113
|
+
{key_name: value for key_name, value in detail.items() if key_name != "patch"}
|
|
3114
|
+
)
|
|
3115
|
+
current_implementation = str(current["implementation_fingerprint"])
|
|
3116
|
+
baseline_implementation = str(checkpoint["implementation_fingerprint"])
|
|
3117
|
+
config_changed = current.get("config_fingerprint") != checkpoint.get("config_fingerprint")
|
|
3118
|
+
contract_changed = current.get("contract_fingerprint") != checkpoint.get(
|
|
3119
|
+
"contract_fingerprint"
|
|
3120
|
+
)
|
|
3121
|
+
metadata_changed = bool(
|
|
3122
|
+
contract_changed
|
|
3123
|
+
or nested_repository_changed
|
|
3124
|
+
or (current_implementation != baseline_implementation and not changes)
|
|
3125
|
+
)
|
|
3126
|
+
metadata_reasons = [
|
|
3127
|
+
reason
|
|
3128
|
+
for condition, reason in (
|
|
3129
|
+
(contract_changed, "verification-contract-changed"),
|
|
3130
|
+
(nested_repository_changed, "nested-repository-changed"),
|
|
3131
|
+
(
|
|
3132
|
+
current_implementation != baseline_implementation
|
|
3133
|
+
and not changes
|
|
3134
|
+
and not contract_changed,
|
|
3135
|
+
"unclassified-implementation-drift",
|
|
3136
|
+
),
|
|
3137
|
+
)
|
|
3138
|
+
if condition
|
|
3139
|
+
]
|
|
3140
|
+
digest_payload = {
|
|
3141
|
+
"from": baseline_implementation,
|
|
3142
|
+
"to": current_implementation,
|
|
3143
|
+
"config_changed": config_changed,
|
|
3144
|
+
"metadata_changed": metadata_changed,
|
|
3145
|
+
"changes": digest_changes,
|
|
3146
|
+
}
|
|
3147
|
+
return {
|
|
3148
|
+
"status": "drift" if changes or config_changed or metadata_changed else "clean",
|
|
3149
|
+
"from_implementation_fingerprint": baseline_implementation,
|
|
3150
|
+
"implementation_fingerprint": current_implementation,
|
|
3151
|
+
"config_fingerprint": str(current["config_fingerprint"]),
|
|
3152
|
+
"config_changed": config_changed,
|
|
3153
|
+
"metadata_changed": metadata_changed,
|
|
3154
|
+
"metadata_reasons": metadata_reasons,
|
|
3155
|
+
"diff_sha256": canonical_json_sha256(digest_payload),
|
|
3156
|
+
"changed_files": [str(change["file"]) for change in changes],
|
|
3157
|
+
"changes": changes,
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
|
|
3161
|
+
def cleanup_verification_checkpoint(root: Path, task_id: str, task: dict) -> None:
|
|
3162
|
+
checkpoint = task.pop("verification_checkpoint", None)
|
|
3163
|
+
if not isinstance(checkpoint, dict):
|
|
3164
|
+
return
|
|
3165
|
+
raw_path = checkpoint.get("snapshot_file")
|
|
3166
|
+
if not is_non_empty_string(raw_path):
|
|
3167
|
+
return
|
|
3168
|
+
candidate = (root / str(raw_path)).resolve()
|
|
3169
|
+
sessions_root = (root / ".easy-coding" / "sessions").resolve()
|
|
3170
|
+
if not is_path_within(candidate, sessions_root):
|
|
3171
|
+
return
|
|
3172
|
+
try:
|
|
3173
|
+
candidate.unlink()
|
|
3174
|
+
except FileNotFoundError:
|
|
3175
|
+
pass
|
|
3176
|
+
try:
|
|
3177
|
+
candidate.parent.rmdir()
|
|
3178
|
+
except OSError:
|
|
3179
|
+
pass
|
|
3180
|
+
|
|
3181
|
+
|
|
3182
|
+
def record_verification_checkpoint(
|
|
3183
|
+
root: Path,
|
|
3184
|
+
agent: str,
|
|
3185
|
+
task_id: str | None = None,
|
|
3186
|
+
session_file: str | Path | None = None,
|
|
3187
|
+
) -> dict:
|
|
3188
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
3189
|
+
if task.get("status") != "VERIFICATION":
|
|
3190
|
+
raise StateError("Verification checkpoint can only be recorded during VERIFICATION.")
|
|
3191
|
+
if isinstance(task.get("verification_checkpoint"), dict):
|
|
3192
|
+
load_acceptance_snapshot(root, task)
|
|
3193
|
+
result = snapshot_state(root, session_file, session)
|
|
3194
|
+
result["action"] = "verification-checkpoint"
|
|
3195
|
+
result["verification_checkpoint"] = task["verification_checkpoint"]
|
|
3196
|
+
result["checkpoint_unchanged"] = True
|
|
3197
|
+
return result
|
|
3198
|
+
validate_verification_readiness(root, resolved_task_id, task)
|
|
3199
|
+
snapshot = build_acceptance_snapshot(root, resolved_task_id, task)
|
|
3200
|
+
path = acceptance_snapshot_path(root, resolved_task_id)
|
|
3201
|
+
write_json(path, snapshot)
|
|
3202
|
+
task["verification_checkpoint"] = {
|
|
3203
|
+
"schema": ACCEPTANCE_SNAPSHOT_SCHEMA,
|
|
3204
|
+
"implementation_fingerprint": snapshot["implementation_fingerprint"],
|
|
3205
|
+
"config_fingerprint": snapshot["config_fingerprint"],
|
|
3206
|
+
"contract_fingerprint": snapshot["contract_fingerprint"],
|
|
3207
|
+
"snapshot_file": display_path(root, path),
|
|
3208
|
+
"snapshot_sha256": canonical_json_sha256(snapshot),
|
|
3209
|
+
"recorded_at": now_iso(),
|
|
3210
|
+
"recorded_by": agent,
|
|
3211
|
+
}
|
|
3212
|
+
task["last_agent"] = agent
|
|
3213
|
+
write_task(root, resolved_task_id, task)
|
|
3214
|
+
result = snapshot_state(root, session_file, session)
|
|
3215
|
+
result["action"] = "verification-checkpoint"
|
|
3216
|
+
result["verification_checkpoint"] = task["verification_checkpoint"]
|
|
3217
|
+
return result
|
|
3218
|
+
|
|
3219
|
+
|
|
3220
|
+
def latest_acceptance_record(root: Path, task_id: str, task: dict) -> dict | None:
|
|
3221
|
+
latest_implement = max(
|
|
3222
|
+
(
|
|
3223
|
+
str(entry.get("entered_at"))
|
|
3224
|
+
for entry in task.get("stage_history", [])
|
|
3225
|
+
if isinstance(entry, dict)
|
|
3226
|
+
and entry.get("stage") == "IMPLEMENT"
|
|
3227
|
+
and is_non_empty_string(entry.get("entered_at"))
|
|
3228
|
+
),
|
|
3229
|
+
default="",
|
|
3230
|
+
)
|
|
3231
|
+
latest: dict | None = None
|
|
3232
|
+
for record in execution_records(root, task_id):
|
|
3233
|
+
if record.get("type") != "acceptance" or not is_non_empty_string(
|
|
3234
|
+
record.get("timestamp")
|
|
3235
|
+
):
|
|
3236
|
+
continue
|
|
3237
|
+
if latest_implement and str(record["timestamp"]) < latest_implement:
|
|
3238
|
+
continue
|
|
3239
|
+
latest = record
|
|
3240
|
+
return latest
|
|
3241
|
+
|
|
3242
|
+
|
|
3243
|
+
def ensure_verification_checkpoint(
|
|
3244
|
+
root: Path,
|
|
3245
|
+
task_id: str,
|
|
3246
|
+
task: dict,
|
|
3247
|
+
agent: str,
|
|
3248
|
+
session_file: str | Path | None,
|
|
3249
|
+
) -> dict:
|
|
3250
|
+
if isinstance(task.get("verification_checkpoint"), dict):
|
|
3251
|
+
load_acceptance_snapshot(root, task)
|
|
3252
|
+
return task
|
|
3253
|
+
record_verification_checkpoint(root, agent, task_id, session_file)
|
|
3254
|
+
refreshed = load_task(root, task_id)
|
|
3255
|
+
if not isinstance(refreshed, dict):
|
|
3256
|
+
raise StateError(f"Task not found after verification checkpoint: {task_id}")
|
|
3257
|
+
return refreshed
|
|
3258
|
+
|
|
3259
|
+
|
|
3260
|
+
def inspect_transition_drift(
|
|
3261
|
+
root: Path,
|
|
3262
|
+
agent: str,
|
|
3263
|
+
task_id: str | None = None,
|
|
3264
|
+
session_file: str | Path | None = None,
|
|
3265
|
+
) -> dict:
|
|
3266
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
3267
|
+
if task.get("status") != "VERIFICATION":
|
|
3268
|
+
raise StateError("Transition drift can only be inspected during VERIFICATION.")
|
|
3269
|
+
task = ensure_verification_checkpoint(root, resolved_task_id, task, agent, session_file)
|
|
3270
|
+
result = snapshot_state(root, session_file, session)
|
|
3271
|
+
result["acceptance_drift"] = inspect_acceptance_drift(root, resolved_task_id, task)
|
|
3272
|
+
result["action"] = "inspect-transition-drift"
|
|
3273
|
+
return result
|
|
3274
|
+
|
|
3275
|
+
|
|
3276
|
+
def append_transition_acceptance(
|
|
3277
|
+
root: Path,
|
|
3278
|
+
task_id: str,
|
|
3279
|
+
task: dict,
|
|
3280
|
+
agent: str,
|
|
3281
|
+
approval_mode: str,
|
|
3282
|
+
authorization: str,
|
|
3283
|
+
expected_diff_sha256: str | None = None,
|
|
3284
|
+
verification_policy: str | None = None,
|
|
3285
|
+
summary: str | None = None,
|
|
3286
|
+
) -> dict:
|
|
3287
|
+
drift = inspect_acceptance_drift(root, task_id, task)
|
|
3288
|
+
if drift["config_changed"]:
|
|
3289
|
+
raise StateError(
|
|
3290
|
+
"Behavior config changed after verification; rerun verification before MEMORY."
|
|
3291
|
+
)
|
|
3292
|
+
if drift["metadata_changed"]:
|
|
3293
|
+
raise StateError(
|
|
3294
|
+
"Execution plan, workflow, Canonical design, or nested repository state changed "
|
|
3295
|
+
"after verification; return to ANALYSIS or IMPLEMENT instead of accepting it as a code diff."
|
|
3296
|
+
)
|
|
3297
|
+
changed_files = list(drift["changed_files"])
|
|
3298
|
+
if changed_files:
|
|
3299
|
+
if expected_diff_sha256 != drift["diff_sha256"]:
|
|
3300
|
+
raise StateError(
|
|
3301
|
+
"Verified code changed after the acceptance checkpoint. Inspect the exact drift "
|
|
3302
|
+
"and confirm its current diff_sha256 before entering MEMORY."
|
|
3303
|
+
)
|
|
3304
|
+
if verification_policy not in ACCEPTANCE_VERIFICATION_POLICIES:
|
|
3305
|
+
raise StateError(
|
|
3306
|
+
"Accepted code drift requires verification policy carry-forward, targeted, or waived."
|
|
3307
|
+
)
|
|
3308
|
+
review_policy = "user-accepted-without-rereview"
|
|
3309
|
+
else:
|
|
3310
|
+
verification_policy = "current"
|
|
3311
|
+
review_policy = "current"
|
|
3312
|
+
required_targeted_source_tasks = (
|
|
3313
|
+
targeted_source_tasks_for_changes(root, task_id, task, drift["changes"])
|
|
3314
|
+
if verification_policy == "targeted"
|
|
3315
|
+
else []
|
|
3316
|
+
)
|
|
3317
|
+
normalized_summary = (
|
|
3318
|
+
summary.strip()
|
|
3319
|
+
if isinstance(summary, str) and summary.strip()
|
|
3320
|
+
else "User accepted the verified implementation"
|
|
3321
|
+
if authorization == "explicit-user"
|
|
3322
|
+
else f"Approval mode {approval_mode} authorized the verified implementation"
|
|
3323
|
+
)
|
|
3324
|
+
record = {
|
|
3325
|
+
"type": "acceptance",
|
|
3326
|
+
"from_implementation_fingerprint": drift["from_implementation_fingerprint"],
|
|
3327
|
+
"implementation_fingerprint": drift["implementation_fingerprint"],
|
|
3328
|
+
"config_fingerprint": drift["config_fingerprint"],
|
|
3329
|
+
"diff_sha256": drift["diff_sha256"],
|
|
3330
|
+
"changed_files": changed_files,
|
|
3331
|
+
"authorization": authorization,
|
|
3332
|
+
"approval_mode": approval_mode,
|
|
3333
|
+
"review_policy": review_policy,
|
|
3334
|
+
"verification_policy": verification_policy,
|
|
3335
|
+
"required_targeted_source_tasks": required_targeted_source_tasks,
|
|
3336
|
+
"summary": normalized_summary,
|
|
3337
|
+
"recorded_by": agent,
|
|
3338
|
+
"timestamp": now_iso(),
|
|
3339
|
+
}
|
|
3340
|
+
existing = latest_acceptance_record(root, task_id, task)
|
|
3341
|
+
identity_fields = (
|
|
3342
|
+
"from_implementation_fingerprint",
|
|
3343
|
+
"implementation_fingerprint",
|
|
3344
|
+
"config_fingerprint",
|
|
3345
|
+
"diff_sha256",
|
|
3346
|
+
"authorization",
|
|
3347
|
+
"approval_mode",
|
|
3348
|
+
"review_policy",
|
|
3349
|
+
"verification_policy",
|
|
3350
|
+
"required_targeted_source_tasks",
|
|
3351
|
+
"summary",
|
|
3352
|
+
)
|
|
3353
|
+
if not (
|
|
3354
|
+
isinstance(existing, dict)
|
|
3355
|
+
and existing.get("changed_files") == changed_files
|
|
3356
|
+
and all(existing.get(field) == record.get(field) for field in identity_fields)
|
|
3357
|
+
):
|
|
3358
|
+
append_execution_record(root, task_id, record)
|
|
3359
|
+
return record
|
|
3360
|
+
|
|
3361
|
+
|
|
3362
|
+
def targeted_source_tasks_for_changes(
|
|
3363
|
+
root: Path,
|
|
3364
|
+
task_id: str,
|
|
3365
|
+
task: dict,
|
|
3366
|
+
changes: list[dict],
|
|
3367
|
+
) -> list[str]:
|
|
3368
|
+
if not isinstance(task.get("spec_source"), dict):
|
|
3369
|
+
return []
|
|
3370
|
+
plan = latest_execution_plan(root, task_id)
|
|
3371
|
+
repo_paths = task.get("repo_paths")
|
|
3372
|
+
if plan is None or not isinstance(repo_paths, dict):
|
|
3373
|
+
raise StateError("Canonical targeted verification requires a valid repository plan.")
|
|
3374
|
+
|
|
3375
|
+
units_by_repository: dict[str, list[dict]] = {}
|
|
3376
|
+
for unit in plan.get("units", []):
|
|
3377
|
+
if not isinstance(unit, dict) or not is_non_empty_string(unit.get("repo_id")):
|
|
3378
|
+
continue
|
|
3379
|
+
raw_repository = repo_paths.get(str(unit["repo_id"]))
|
|
3380
|
+
if not is_non_empty_string(raw_repository):
|
|
3381
|
+
continue
|
|
3382
|
+
candidate = Path(str(raw_repository))
|
|
3383
|
+
repository = (candidate if candidate.is_absolute() else root / candidate).resolve()
|
|
3384
|
+
units_by_repository.setdefault(display_path(root, repository), []).append(unit)
|
|
3385
|
+
|
|
3386
|
+
impacted: set[str] = set()
|
|
3387
|
+
for change in changes:
|
|
3388
|
+
if not isinstance(change, dict):
|
|
3389
|
+
continue
|
|
3390
|
+
repository_units = units_by_repository.get(str(change.get("repository") or ""), [])
|
|
3391
|
+
if not repository_units:
|
|
3392
|
+
continue
|
|
3393
|
+
changed_path = str(change.get("path") or "")
|
|
3394
|
+
matched_units = [
|
|
3395
|
+
unit
|
|
3396
|
+
for unit in repository_units
|
|
3397
|
+
if any(
|
|
3398
|
+
changed_path == str(file_name)
|
|
3399
|
+
or changed_path.startswith(str(file_name).rstrip("/") + "/")
|
|
3400
|
+
for file_name in unit.get("files", [])
|
|
3401
|
+
if is_non_empty_string(file_name)
|
|
3402
|
+
)
|
|
3403
|
+
]
|
|
3404
|
+
scoped_units = matched_units or repository_units
|
|
3405
|
+
impacted.update(
|
|
3406
|
+
str(unit["source_task_id"])
|
|
3407
|
+
for unit in scoped_units
|
|
3408
|
+
if is_non_empty_string(unit.get("source_task_id"))
|
|
3409
|
+
)
|
|
3410
|
+
if not impacted:
|
|
3411
|
+
raise StateError(
|
|
3412
|
+
"Canonical executable drift could not be mapped to a selected source task."
|
|
3413
|
+
)
|
|
3414
|
+
return sorted(impacted)
|
|
3415
|
+
|
|
3416
|
+
|
|
2754
3417
|
def command_option_value(command: str, option: str) -> str | None:
|
|
2755
3418
|
try:
|
|
2756
3419
|
tokens = shlex.split(command)
|
|
@@ -2782,6 +3445,68 @@ def coverage_command_matches_frozen_contract(
|
|
|
2782
3445
|
)
|
|
2783
3446
|
|
|
2784
3447
|
|
|
3448
|
+
def current_acceptance_record(
|
|
3449
|
+
root: Path,
|
|
3450
|
+
task_id: str,
|
|
3451
|
+
task: dict,
|
|
3452
|
+
implementation_fingerprint_value: str,
|
|
3453
|
+
config_fingerprint_value: str,
|
|
3454
|
+
) -> dict | None:
|
|
3455
|
+
record = latest_acceptance_record(root, task_id, task)
|
|
3456
|
+
if not isinstance(record, dict):
|
|
3457
|
+
return None
|
|
3458
|
+
if (
|
|
3459
|
+
record.get("implementation_fingerprint") != implementation_fingerprint_value
|
|
3460
|
+
or record.get("config_fingerprint") != config_fingerprint_value
|
|
3461
|
+
or not is_non_empty_string(record.get("from_implementation_fingerprint"))
|
|
3462
|
+
or record.get("review_policy")
|
|
3463
|
+
not in {"current", "user-accepted-without-rereview"}
|
|
3464
|
+
or record.get("verification_policy")
|
|
3465
|
+
not in {"current", *ACCEPTANCE_VERIFICATION_POLICIES}
|
|
3466
|
+
or not is_string_list(record.get("required_targeted_source_tasks"))
|
|
3467
|
+
):
|
|
3468
|
+
return None
|
|
3469
|
+
return record
|
|
3470
|
+
|
|
3471
|
+
|
|
3472
|
+
def accepted_review_fingerprints(
|
|
3473
|
+
root: Path, task_id: str, task: dict, current_fingerprint: str
|
|
3474
|
+
) -> set[str]:
|
|
3475
|
+
accepted = {current_fingerprint}
|
|
3476
|
+
record = current_acceptance_record(
|
|
3477
|
+
root,
|
|
3478
|
+
task_id,
|
|
3479
|
+
task,
|
|
3480
|
+
current_fingerprint,
|
|
3481
|
+
behavior_config_fingerprint(root, task),
|
|
3482
|
+
)
|
|
3483
|
+
if record and record.get("review_policy") == "user-accepted-without-rereview":
|
|
3484
|
+
accepted.add(str(record["from_implementation_fingerprint"]))
|
|
3485
|
+
return accepted
|
|
3486
|
+
|
|
3487
|
+
|
|
3488
|
+
def accepted_verification_fingerprints(
|
|
3489
|
+
root: Path,
|
|
3490
|
+
task_id: str,
|
|
3491
|
+
task: dict,
|
|
3492
|
+
current_implementation: str,
|
|
3493
|
+
current_config: str,
|
|
3494
|
+
) -> tuple[set[str], dict | None]:
|
|
3495
|
+
record = current_acceptance_record(
|
|
3496
|
+
root,
|
|
3497
|
+
task_id,
|
|
3498
|
+
task,
|
|
3499
|
+
current_implementation,
|
|
3500
|
+
current_config,
|
|
3501
|
+
)
|
|
3502
|
+
if not record or record.get("verification_policy") == "current":
|
|
3503
|
+
return {current_implementation}, record
|
|
3504
|
+
previous = str(record["from_implementation_fingerprint"])
|
|
3505
|
+
if record.get("verification_policy") == "targeted":
|
|
3506
|
+
return {previous, current_implementation}, record
|
|
3507
|
+
return {previous}, record
|
|
3508
|
+
|
|
3509
|
+
|
|
2785
3510
|
def validate_spec_implementation_results(root: Path, task_id: str, task: dict) -> None:
|
|
2786
3511
|
if not isinstance(task.get("spec_source"), dict):
|
|
2787
3512
|
return
|
|
@@ -2858,11 +3583,12 @@ def validate_review_readiness(root: Path, task_id: str, task: dict) -> None:
|
|
|
2858
3583
|
if task.get("workflow_mode_legacy") is True and not is_spec_task:
|
|
2859
3584
|
return
|
|
2860
3585
|
expected = implementation_fingerprint(root, task_id)
|
|
3586
|
+
accepted_fingerprints = accepted_review_fingerprints(root, task_id, task, expected)
|
|
2861
3587
|
latest_by_dimension: dict[str, dict] = {}
|
|
2862
3588
|
for record in execution_records(root, task_id):
|
|
2863
3589
|
if (
|
|
2864
3590
|
record.get("type") == "review"
|
|
2865
|
-
and record.get("implementation_fingerprint")
|
|
3591
|
+
and record.get("implementation_fingerprint") in accepted_fingerprints
|
|
2866
3592
|
and is_non_empty_string(record.get("dimension"))
|
|
2867
3593
|
):
|
|
2868
3594
|
dimension = str(record["dimension"])
|
|
@@ -2977,6 +3703,13 @@ def validate_review_readiness(root: Path, task_id: str, task: dict) -> None:
|
|
|
2977
3703
|
|
|
2978
3704
|
def validate_verification_readiness(root: Path, task_id: str, task: dict) -> None:
|
|
2979
3705
|
fingerprints = evidence_fingerprints(root, task_id)
|
|
3706
|
+
accepted_fingerprints, acceptance = accepted_verification_fingerprints(
|
|
3707
|
+
root,
|
|
3708
|
+
task_id,
|
|
3709
|
+
task,
|
|
3710
|
+
fingerprints["implementation_fingerprint"],
|
|
3711
|
+
fingerprints["config_fingerprint"],
|
|
3712
|
+
)
|
|
2980
3713
|
is_spec_task = isinstance(task.get("spec_source"), dict)
|
|
2981
3714
|
if (
|
|
2982
3715
|
(task.get("workflow_mode_legacy") is not True or is_spec_task)
|
|
@@ -2988,8 +3721,7 @@ def validate_verification_readiness(root: Path, task_id: str, task: dict) -> Non
|
|
|
2988
3721
|
for record in execution_records(root, task_id):
|
|
2989
3722
|
if (
|
|
2990
3723
|
record.get("type") == "verify"
|
|
2991
|
-
and record.get("implementation_fingerprint")
|
|
2992
|
-
== fingerprints["implementation_fingerprint"]
|
|
3724
|
+
and record.get("implementation_fingerprint") in accepted_fingerprints
|
|
2993
3725
|
and record.get("config_fingerprint") == fingerprints["config_fingerprint"]
|
|
2994
3726
|
and is_non_empty_string(record.get("check"))
|
|
2995
3727
|
):
|
|
@@ -3067,6 +3799,32 @@ def validate_verification_readiness(root: Path, task_id: str, task: dict) -> Non
|
|
|
3067
3799
|
raise StateError(
|
|
3068
3800
|
"VERIFICATION cannot advance to MEMORY while current verification evidence contains failures."
|
|
3069
3801
|
)
|
|
3802
|
+
if acceptance and acceptance.get("verification_policy") == "targeted":
|
|
3803
|
+
current_records = [
|
|
3804
|
+
record
|
|
3805
|
+
for record in applicable_records
|
|
3806
|
+
if record.get("implementation_fingerprint")
|
|
3807
|
+
== fingerprints["implementation_fingerprint"]
|
|
3808
|
+
]
|
|
3809
|
+
if not current_records or any(record.get("passed") is not True for record in current_records):
|
|
3810
|
+
raise StateError(
|
|
3811
|
+
"Accepted executable drift requires at least one passed targeted verification "
|
|
3812
|
+
"record for the current implementation fingerprint."
|
|
3813
|
+
)
|
|
3814
|
+
if is_spec_task:
|
|
3815
|
+
required_source_tasks = set(acceptance["required_targeted_source_tasks"])
|
|
3816
|
+
current_source_tasks = {
|
|
3817
|
+
str(record.get("source_task_id"))
|
|
3818
|
+
for record in current_records
|
|
3819
|
+
if is_non_empty_string(record.get("source_task_id"))
|
|
3820
|
+
}
|
|
3821
|
+
missing_source_tasks = sorted(required_source_tasks - current_source_tasks)
|
|
3822
|
+
if missing_source_tasks:
|
|
3823
|
+
raise StateError(
|
|
3824
|
+
"Accepted Canonical executable drift requires a passed current-fingerprint "
|
|
3825
|
+
"targeted verification record for affected source tasks: "
|
|
3826
|
+
+ ", ".join(missing_source_tasks)
|
|
3827
|
+
)
|
|
3070
3828
|
if str(task.get("type") or "").strip().lower() == TDD_INIT_TASK_TYPE:
|
|
3071
3829
|
readiness = tdd_readiness(root)
|
|
3072
3830
|
if readiness["status"] != "ready":
|
|
@@ -4223,6 +4981,11 @@ def build_machine_breadcrumbs(
|
|
|
4223
4981
|
lines.append(
|
|
4224
4982
|
"[easy-coding:lite-review-bypass-required:IMPLEMENT->REVIEW]"
|
|
4225
4983
|
)
|
|
4984
|
+
elif pending.get("confirmation_override") == "evidence-drift":
|
|
4985
|
+
lines.append(
|
|
4986
|
+
"[easy-coding:acceptance-drift-confirmation-required]"
|
|
4987
|
+
)
|
|
4988
|
+
lines.append("[easy-coding:transition-confirmation-required]")
|
|
4226
4989
|
elif is_automatic_transition(
|
|
4227
4990
|
source,
|
|
4228
4991
|
target,
|
|
@@ -5687,6 +6450,7 @@ def sync_spec_design_state(
|
|
|
5687
6450
|
)
|
|
5688
6451
|
progress.pop("pending_action", None)
|
|
5689
6452
|
if task.get("status") not in {"INIT", "ANALYSIS"}:
|
|
6453
|
+
cleanup_verification_checkpoint(root, resolved_task_id, task)
|
|
5690
6454
|
task["status"] = "ANALYSIS"
|
|
5691
6455
|
append_stage_history(task, "ANALYSIS", agent)
|
|
5692
6456
|
task.pop("pending_transition", None)
|
|
@@ -5851,6 +6615,134 @@ def require_shared_task_statuses(root: Path, task: dict, allowed: set[str]) -> N
|
|
|
5851
6615
|
)
|
|
5852
6616
|
|
|
5853
6617
|
|
|
6618
|
+
def effective_verification_records(root: Path, task_id: str, task: dict) -> list[dict]:
|
|
6619
|
+
fingerprints = evidence_fingerprints(root, task_id)
|
|
6620
|
+
accepted_fingerprints, _ = accepted_verification_fingerprints(
|
|
6621
|
+
root,
|
|
6622
|
+
task_id,
|
|
6623
|
+
task,
|
|
6624
|
+
fingerprints["implementation_fingerprint"],
|
|
6625
|
+
fingerprints["config_fingerprint"],
|
|
6626
|
+
)
|
|
6627
|
+
latest: dict[tuple[str, str, str], dict] = {}
|
|
6628
|
+
for record in execution_records(root, task_id):
|
|
6629
|
+
if (
|
|
6630
|
+
record.get("type") != "verify"
|
|
6631
|
+
or record.get("implementation_fingerprint") not in accepted_fingerprints
|
|
6632
|
+
or record.get("config_fingerprint") != fingerprints["config_fingerprint"]
|
|
6633
|
+
or record.get("applicable") is False
|
|
6634
|
+
):
|
|
6635
|
+
continue
|
|
6636
|
+
key = (
|
|
6637
|
+
str(record.get("source_task_id") or ""),
|
|
6638
|
+
str(record.get("repo_id") or ""),
|
|
6639
|
+
str(record.get("command") or record.get("check") or ""),
|
|
6640
|
+
)
|
|
6641
|
+
latest[key] = record
|
|
6642
|
+
return list(latest.values())
|
|
6643
|
+
|
|
6644
|
+
|
|
6645
|
+
def acceptance_spec_evidence(acceptance: dict) -> dict:
|
|
6646
|
+
digest = str(acceptance.get("diff_sha256") or "")
|
|
6647
|
+
reference = (
|
|
6648
|
+
"execution.jsonl#acceptance="
|
|
6649
|
+
+ digest
|
|
6650
|
+
+ ";authorization="
|
|
6651
|
+
+ str(acceptance.get("authorization") or "")
|
|
6652
|
+
+ ";approval_mode="
|
|
6653
|
+
+ str(acceptance.get("approval_mode") or "")
|
|
6654
|
+
+ ";review_policy="
|
|
6655
|
+
+ str(acceptance.get("review_policy") or "")
|
|
6656
|
+
+ ";verification_policy="
|
|
6657
|
+
+ str(acceptance.get("verification_policy") or "")
|
|
6658
|
+
+ ";targeted_source_tasks="
|
|
6659
|
+
+ ",".join(str(value) for value in acceptance.get("required_targeted_source_tasks", []))
|
|
6660
|
+
)
|
|
6661
|
+
return {
|
|
6662
|
+
"kind": "acceptance",
|
|
6663
|
+
"status": "recorded",
|
|
6664
|
+
"ref": reference,
|
|
6665
|
+
"sha256": digest,
|
|
6666
|
+
}
|
|
6667
|
+
|
|
6668
|
+
|
|
6669
|
+
def writeback_verified_tasks(
|
|
6670
|
+
root: Path,
|
|
6671
|
+
harness_task_id: str,
|
|
6672
|
+
task: dict,
|
|
6673
|
+
agent: str,
|
|
6674
|
+
session_file: str | Path | None = None,
|
|
6675
|
+
) -> None:
|
|
6676
|
+
inspection, selection = inspect_task_spec(root, task)
|
|
6677
|
+
snapshots = _selected_execution_snapshots(inspection, task)
|
|
6678
|
+
acceptance = latest_acceptance_record(root, harness_task_id, task)
|
|
6679
|
+
if not isinstance(acceptance, dict):
|
|
6680
|
+
raise StateError("Canonical verification writeback requires an acceptance record.")
|
|
6681
|
+
verification_records = effective_verification_records(root, harness_task_id, task)
|
|
6682
|
+
selected_tasks = {
|
|
6683
|
+
str(item.get("task_id")): item
|
|
6684
|
+
for item in selection.get("selected_tasks", [])
|
|
6685
|
+
if isinstance(item, dict)
|
|
6686
|
+
}
|
|
6687
|
+
tests_by_task: dict[str, list[dict]] = {}
|
|
6688
|
+
for test in selection.get("selected_tests", []):
|
|
6689
|
+
if isinstance(test, dict):
|
|
6690
|
+
tests_by_task.setdefault(str(test.get("task_id")), []).append(test)
|
|
6691
|
+
for source_task_id in task.get("selected_spec_tasks") or []:
|
|
6692
|
+
source_task_id = str(source_task_id)
|
|
6693
|
+
status_value = snapshots.get(source_task_id, {}).get("status")
|
|
6694
|
+
if status_value in {"verified", "completed"}:
|
|
6695
|
+
continue
|
|
6696
|
+
if status_value != "implemented":
|
|
6697
|
+
raise StateError(
|
|
6698
|
+
f"Canonical task {source_task_id} must remain implemented until MEMORY entry is applied."
|
|
6699
|
+
)
|
|
6700
|
+
repo_id = str(selected_tasks.get(source_task_id, {}).get("repo_id") or "")
|
|
6701
|
+
evidence = []
|
|
6702
|
+
for test in tests_by_task.get(source_task_id, []):
|
|
6703
|
+
command = str(test.get("command") or "")
|
|
6704
|
+
matching = next(
|
|
6705
|
+
(
|
|
6706
|
+
record
|
|
6707
|
+
for record in verification_records
|
|
6708
|
+
if record.get("passed") is True
|
|
6709
|
+
and str(record.get("source_task_id") or "") == source_task_id
|
|
6710
|
+
and str(record.get("repo_id") or "") == repo_id
|
|
6711
|
+
and str(record.get("command") or "") == command
|
|
6712
|
+
),
|
|
6713
|
+
None,
|
|
6714
|
+
)
|
|
6715
|
+
if matching is None:
|
|
6716
|
+
raise StateError(
|
|
6717
|
+
f"Canonical Test {test.get('test_id')} has no accepted verification command: {command}"
|
|
6718
|
+
)
|
|
6719
|
+
evidence.append(
|
|
6720
|
+
{
|
|
6721
|
+
"kind": "test",
|
|
6722
|
+
"status": "passed",
|
|
6723
|
+
"ref": f"execution.jsonl#verify;command={command}",
|
|
6724
|
+
"test_id": str(test.get("test_id")),
|
|
6725
|
+
}
|
|
6726
|
+
)
|
|
6727
|
+
evidence.append(acceptance_spec_evidence(acceptance))
|
|
6728
|
+
acceptance_key = str(acceptance.get("diff_sha256") or "")[:16]
|
|
6729
|
+
key = (
|
|
6730
|
+
f"{harness_task_id}:{source_task_id}:verified-after-acceptance:"
|
|
6731
|
+
f"{task['spec_source']['revision']}:{acceptance_key}"
|
|
6732
|
+
)
|
|
6733
|
+
writeback_spec_task(
|
|
6734
|
+
root,
|
|
6735
|
+
source_task_id,
|
|
6736
|
+
"verified",
|
|
6737
|
+
"Harness verification was accepted when the MEMORY boundary was applied",
|
|
6738
|
+
evidence,
|
|
6739
|
+
key,
|
|
6740
|
+
agent,
|
|
6741
|
+
harness_task_id,
|
|
6742
|
+
session_file,
|
|
6743
|
+
)
|
|
6744
|
+
|
|
6745
|
+
|
|
5854
6746
|
def writeback_completed_tasks(
|
|
5855
6747
|
root: Path,
|
|
5856
6748
|
harness_task_id: str,
|
|
@@ -5859,6 +6751,10 @@ def writeback_completed_tasks(
|
|
|
5859
6751
|
) -> None:
|
|
5860
6752
|
inspection, _ = inspect_task_spec(root, task)
|
|
5861
6753
|
snapshots = _selected_execution_snapshots(inspection, task)
|
|
6754
|
+
acceptance = latest_acceptance_record(root, harness_task_id, task)
|
|
6755
|
+
acceptance_evidence = (
|
|
6756
|
+
[acceptance_spec_evidence(acceptance)] if isinstance(acceptance, dict) else []
|
|
6757
|
+
)
|
|
5862
6758
|
for source_task_id in task.get("selected_spec_tasks") or []:
|
|
5863
6759
|
status_value = snapshots.get(str(source_task_id), {}).get("status")
|
|
5864
6760
|
if status_value == "completed":
|
|
@@ -5867,13 +6763,21 @@ def writeback_completed_tasks(
|
|
|
5867
6763
|
raise StateError(
|
|
5868
6764
|
f"Canonical task {source_task_id} must be verified before Harness COMPLETE."
|
|
5869
6765
|
)
|
|
5870
|
-
|
|
6766
|
+
acceptance_key = (
|
|
6767
|
+
str(acceptance.get("diff_sha256") or "")[:16]
|
|
6768
|
+
if isinstance(acceptance, dict)
|
|
6769
|
+
else "legacy"
|
|
6770
|
+
)
|
|
6771
|
+
key = (
|
|
6772
|
+
f"{harness_task_id}:{source_task_id}:complete:"
|
|
6773
|
+
f"{task['spec_source']['revision']}:{acceptance_key}"
|
|
6774
|
+
)
|
|
5871
6775
|
action = {
|
|
5872
6776
|
"kind": "task",
|
|
5873
6777
|
"source_task_id": source_task_id,
|
|
5874
6778
|
"status": "completed",
|
|
5875
6779
|
"summary": "Harness MEMORY completed and the Canonical task is complete",
|
|
5876
|
-
"evidence":
|
|
6780
|
+
"evidence": acceptance_evidence,
|
|
5877
6781
|
"idempotency_key": key,
|
|
5878
6782
|
"agent": agent,
|
|
5879
6783
|
}
|
|
@@ -5892,6 +6796,7 @@ def writeback_completed_tasks(
|
|
|
5892
6796
|
spec_writeback_agent(agent),
|
|
5893
6797
|
design_digest,
|
|
5894
6798
|
execution_revision,
|
|
6799
|
+
evidence=acceptance_evidence,
|
|
5895
6800
|
run_id=harness_task_id,
|
|
5896
6801
|
idempotency_key=key,
|
|
5897
6802
|
),
|
|
@@ -6351,8 +7256,22 @@ def request_transition(
|
|
|
6351
7256
|
)
|
|
6352
7257
|
if previous == "REVIEW" and stage == "VERIFICATION":
|
|
6353
7258
|
validate_review_readiness(root, resolved_task_id, task)
|
|
7259
|
+
acceptance_drift: dict | None = None
|
|
6354
7260
|
if previous == "VERIFICATION" and stage == "MEMORY":
|
|
6355
|
-
|
|
7261
|
+
task = ensure_verification_checkpoint(
|
|
7262
|
+
root, resolved_task_id, task, agent, session_file
|
|
7263
|
+
)
|
|
7264
|
+
acceptance_drift = inspect_acceptance_drift(root, resolved_task_id, task)
|
|
7265
|
+
if acceptance_drift["config_changed"]:
|
|
7266
|
+
raise StateError(
|
|
7267
|
+
"Behavior config changed after verification; rerun verification before MEMORY."
|
|
7268
|
+
)
|
|
7269
|
+
if acceptance_drift["metadata_changed"]:
|
|
7270
|
+
raise StateError(
|
|
7271
|
+
"Non-code verification metadata changed; return to ANALYSIS or IMPLEMENT."
|
|
7272
|
+
)
|
|
7273
|
+
if acceptance_drift["status"] == "clean":
|
|
7274
|
+
validate_verification_readiness(root, resolved_task_id, task)
|
|
6356
7275
|
existing = task.get("pending_transition")
|
|
6357
7276
|
if isinstance(existing, dict):
|
|
6358
7277
|
if existing.get("from") != previous or existing.get("to") != stage:
|
|
@@ -6372,6 +7291,8 @@ def request_transition(
|
|
|
6372
7291
|
|
|
6373
7292
|
snapshot = snapshot_state(root, session_file, session)
|
|
6374
7293
|
snapshot["action"] = "request-transition"
|
|
7294
|
+
if acceptance_drift is not None:
|
|
7295
|
+
snapshot["acceptance_drift"] = acceptance_drift
|
|
6375
7296
|
return snapshot
|
|
6376
7297
|
|
|
6377
7298
|
|
|
@@ -6412,6 +7333,10 @@ def apply_transition(
|
|
|
6412
7333
|
if previous == "VERIFICATION" and stage == "MEMORY":
|
|
6413
7334
|
validate_verification_readiness(root, resolved_task_id, task)
|
|
6414
7335
|
if isinstance(task.get("spec_source"), dict):
|
|
7336
|
+
writeback_verified_tasks(
|
|
7337
|
+
root, resolved_task_id, task, agent, session_file
|
|
7338
|
+
)
|
|
7339
|
+
task = load_task(root, resolved_task_id) or task
|
|
6415
7340
|
require_shared_task_statuses(root, task, {"verified", "completed"})
|
|
6416
7341
|
if previous == "MEMORY" and stage == "COMPLETE":
|
|
6417
7342
|
progress = task.get("memory_progress")
|
|
@@ -6433,6 +7358,8 @@ def apply_transition(
|
|
|
6433
7358
|
task.pop("workflow_mode_legacy_direct_edge", None)
|
|
6434
7359
|
if stage in {"ANALYSIS", "IMPLEMENT", "MEMORY", "COMPLETE", "CLOSED"}:
|
|
6435
7360
|
task.pop("workflow_mode_legacy_review_bypass_fingerprint", None)
|
|
7361
|
+
if stage in {"ANALYSIS", "IMPLEMENT", "MEMORY", "COMPLETE", "CLOSED"}:
|
|
7362
|
+
cleanup_verification_checkpoint(root, resolved_task_id, task)
|
|
6436
7363
|
task.pop("pending_transition", None)
|
|
6437
7364
|
if stage == "MEMORY" and previous != stage:
|
|
6438
7365
|
task["memory_progress"] = {}
|
|
@@ -6457,7 +7384,7 @@ def auto_transition(
|
|
|
6457
7384
|
task_id: str | None = None,
|
|
6458
7385
|
session_file: str | Path | None = None,
|
|
6459
7386
|
) -> dict:
|
|
6460
|
-
session,
|
|
7387
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
6461
7388
|
previous = str(task.get("status") or "idle")
|
|
6462
7389
|
task_type = str(task.get("type") or "")
|
|
6463
7390
|
approval_mode = resolve_approval_mode(root, session)[2]
|
|
@@ -6474,6 +7401,43 @@ def auto_transition(
|
|
|
6474
7401
|
"A different transition is already pending. Cancel it before automatic transition."
|
|
6475
7402
|
)
|
|
6476
7403
|
|
|
7404
|
+
if previous == "VERIFICATION" and stage == "MEMORY":
|
|
7405
|
+
task = ensure_verification_checkpoint(
|
|
7406
|
+
root, resolved_task_id, task, agent, session_file
|
|
7407
|
+
)
|
|
7408
|
+
drift = inspect_acceptance_drift(root, resolved_task_id, task)
|
|
7409
|
+
if drift["config_changed"]:
|
|
7410
|
+
raise StateError(
|
|
7411
|
+
"Behavior config changed after verification; rerun verification before MEMORY."
|
|
7412
|
+
)
|
|
7413
|
+
if drift["metadata_changed"]:
|
|
7414
|
+
raise StateError(
|
|
7415
|
+
"Non-code verification metadata changed; return to ANALYSIS or IMPLEMENT."
|
|
7416
|
+
)
|
|
7417
|
+
if drift["changed_files"]:
|
|
7418
|
+
task["pending_transition"] = {
|
|
7419
|
+
"from": previous,
|
|
7420
|
+
"to": stage,
|
|
7421
|
+
"requested_at": now_iso(),
|
|
7422
|
+
"requested_by": agent,
|
|
7423
|
+
"reason": "verification checkpoint drift requires exact user acceptance",
|
|
7424
|
+
"confirmation_override": "evidence-drift",
|
|
7425
|
+
}
|
|
7426
|
+
task["last_agent"] = agent
|
|
7427
|
+
write_task(root, resolved_task_id, task)
|
|
7428
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
7429
|
+
snapshot["action"] = "acceptance-drift"
|
|
7430
|
+
snapshot["acceptance_drift"] = drift
|
|
7431
|
+
return snapshot
|
|
7432
|
+
append_transition_acceptance(
|
|
7433
|
+
root,
|
|
7434
|
+
resolved_task_id,
|
|
7435
|
+
task,
|
|
7436
|
+
agent,
|
|
7437
|
+
approval_mode,
|
|
7438
|
+
"approval-policy",
|
|
7439
|
+
)
|
|
7440
|
+
|
|
6477
7441
|
snapshot = apply_transition(root, stage, agent, task_id, session_file)
|
|
6478
7442
|
snapshot["action"] = "auto-transition"
|
|
6479
7443
|
snapshot["automatic_transition"] = {"from": previous, "to": stage}
|
|
@@ -6486,8 +7450,11 @@ def confirm_transition(
|
|
|
6486
7450
|
stage: str | None = None,
|
|
6487
7451
|
task_id: str | None = None,
|
|
6488
7452
|
session_file: str | Path | None = None,
|
|
7453
|
+
expected_diff_sha256: str | None = None,
|
|
7454
|
+
verification_policy: str | None = None,
|
|
7455
|
+
decision_summary: str | None = None,
|
|
6489
7456
|
) -> dict:
|
|
6490
|
-
session,
|
|
7457
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
6491
7458
|
pending = task.get("pending_transition")
|
|
6492
7459
|
if not isinstance(pending, dict):
|
|
6493
7460
|
raise StateError("No transition is pending user confirmation.")
|
|
@@ -6502,12 +7469,29 @@ def confirm_transition(
|
|
|
6502
7469
|
)
|
|
6503
7470
|
if stage and stage != target:
|
|
6504
7471
|
raise StateError(f"Pending transition targets {target}, not {stage}.")
|
|
6505
|
-
|
|
7472
|
+
drift_override = pending.get("confirmation_override") == "evidence-drift"
|
|
7473
|
+
if is_automatic_transition(source, target, task_type, approval_mode) and not drift_override:
|
|
6506
7474
|
raise StateError(
|
|
6507
7475
|
f"Transition {source} -> {target} is automatic in {approval_mode} mode; "
|
|
6508
7476
|
"use auto-transition instead."
|
|
6509
7477
|
)
|
|
6510
7478
|
|
|
7479
|
+
if source == "VERIFICATION" and target == "MEMORY":
|
|
7480
|
+
task = ensure_verification_checkpoint(
|
|
7481
|
+
root, resolved_task_id, task, agent, session_file
|
|
7482
|
+
)
|
|
7483
|
+
append_transition_acceptance(
|
|
7484
|
+
root,
|
|
7485
|
+
resolved_task_id,
|
|
7486
|
+
task,
|
|
7487
|
+
agent,
|
|
7488
|
+
approval_mode,
|
|
7489
|
+
"explicit-user",
|
|
7490
|
+
expected_diff_sha256,
|
|
7491
|
+
verification_policy,
|
|
7492
|
+
decision_summary,
|
|
7493
|
+
)
|
|
7494
|
+
|
|
6511
7495
|
snapshot = apply_transition(root, target, agent, task_id, session_file)
|
|
6512
7496
|
snapshot["action"] = "confirm-transition"
|
|
6513
7497
|
snapshot["confirmed_transition"] = {"from": source, "to": target}
|
|
@@ -6548,6 +7532,46 @@ def memory_short_complete(
|
|
|
6548
7532
|
memory_file.strip(),
|
|
6549
7533
|
require_current_id=True,
|
|
6550
7534
|
)
|
|
7535
|
+
acceptance = latest_acceptance_record(root, resolved_task_id, task)
|
|
7536
|
+
if isinstance(acceptance, dict) and acceptance.get("changed_files"):
|
|
7537
|
+
try:
|
|
7538
|
+
memory_text = resolved_memory_path.read_text(encoding="utf-8")
|
|
7539
|
+
except (OSError, UnicodeError) as exc:
|
|
7540
|
+
raise StateError(f"Cannot read short-memory file: {resolved_memory_path}") from exc
|
|
7541
|
+
required_decision_fields = {
|
|
7542
|
+
"diff_sha256": str(acceptance.get("diff_sha256") or ""),
|
|
7543
|
+
"authorization": str(acceptance.get("authorization") or ""),
|
|
7544
|
+
"approval_mode": str(acceptance.get("approval_mode") or ""),
|
|
7545
|
+
"review_policy": str(acceptance.get("review_policy") or ""),
|
|
7546
|
+
"verification_policy": str(acceptance.get("verification_policy") or ""),
|
|
7547
|
+
"summary": str(acceptance.get("summary") or ""),
|
|
7548
|
+
}
|
|
7549
|
+
missing_decision_fields = [
|
|
7550
|
+
field_name
|
|
7551
|
+
for field_name, value in required_decision_fields.items()
|
|
7552
|
+
if not value or value not in memory_text
|
|
7553
|
+
]
|
|
7554
|
+
missing_changed_files = [
|
|
7555
|
+
str(file_name)
|
|
7556
|
+
for file_name in acceptance.get("changed_files", [])
|
|
7557
|
+
if not is_non_empty_string(file_name) or str(file_name) not in memory_text
|
|
7558
|
+
]
|
|
7559
|
+
missing_targeted_tasks = [
|
|
7560
|
+
str(source_task_id)
|
|
7561
|
+
for source_task_id in acceptance.get("required_targeted_source_tasks", [])
|
|
7562
|
+
if not is_non_empty_string(source_task_id)
|
|
7563
|
+
or str(source_task_id) not in memory_text
|
|
7564
|
+
]
|
|
7565
|
+
if missing_decision_fields or missing_changed_files or missing_targeted_tasks:
|
|
7566
|
+
missing_labels = [
|
|
7567
|
+
*missing_decision_fields,
|
|
7568
|
+
*(f"changed_file:{file_name}" for file_name in missing_changed_files),
|
|
7569
|
+
*(f"targeted_source_task:{task_name}" for task_name in missing_targeted_tasks),
|
|
7570
|
+
]
|
|
7571
|
+
raise StateError(
|
|
7572
|
+
"Short memory must record the complete accepted post-verification decision; "
|
|
7573
|
+
"missing: " + ", ".join(missing_labels)
|
|
7574
|
+
)
|
|
6551
7575
|
progress = task.get("memory_progress")
|
|
6552
7576
|
if not isinstance(progress, dict):
|
|
6553
7577
|
progress = {}
|
|
@@ -6676,6 +7700,7 @@ def close_current_task(
|
|
|
6676
7700
|
if isinstance(task.get("spec_source"), dict) and task.get("status") not in TERMINAL_STATUSES:
|
|
6677
7701
|
cancel_shared_tasks(root, str(task_id), task, reason, agent)
|
|
6678
7702
|
if task.get("status") != "CLOSED":
|
|
7703
|
+
cleanup_verification_checkpoint(root, str(task_id), task)
|
|
6679
7704
|
task["status"] = "CLOSED"
|
|
6680
7705
|
append_stage_history(task, "CLOSED", agent)
|
|
6681
7706
|
task.pop("pending_transition", None)
|
|
@@ -6969,6 +7994,18 @@ def main() -> int:
|
|
|
6969
7994
|
fingerprints_parser.add_argument("--agent", required=True)
|
|
6970
7995
|
fingerprints_parser.add_argument("--task-id")
|
|
6971
7996
|
|
|
7997
|
+
verification_checkpoint_parser = subcommands.add_parser(
|
|
7998
|
+
"verification-checkpoint", parents=[common]
|
|
7999
|
+
)
|
|
8000
|
+
verification_checkpoint_parser.add_argument("--agent", required=True)
|
|
8001
|
+
verification_checkpoint_parser.add_argument("--task-id")
|
|
8002
|
+
|
|
8003
|
+
inspect_transition_drift_parser = subcommands.add_parser(
|
|
8004
|
+
"inspect-transition-drift", parents=[common]
|
|
8005
|
+
)
|
|
8006
|
+
inspect_transition_drift_parser.add_argument("--agent", required=True)
|
|
8007
|
+
inspect_transition_drift_parser.add_argument("--task-id")
|
|
8008
|
+
|
|
6972
8009
|
disable_harness_parser = subcommands.add_parser("disable-harness", parents=[common])
|
|
6973
8010
|
disable_harness_parser.add_argument("--agent", required=True)
|
|
6974
8011
|
|
|
@@ -6994,6 +8031,11 @@ def main() -> int:
|
|
|
6994
8031
|
confirm_transition_parser.add_argument("--stage")
|
|
6995
8032
|
confirm_transition_parser.add_argument("--agent", required=True)
|
|
6996
8033
|
confirm_transition_parser.add_argument("--task-id")
|
|
8034
|
+
confirm_transition_parser.add_argument("--diff-sha256")
|
|
8035
|
+
confirm_transition_parser.add_argument(
|
|
8036
|
+
"--verification-policy", choices=sorted(ACCEPTANCE_VERIFICATION_POLICIES)
|
|
8037
|
+
)
|
|
8038
|
+
confirm_transition_parser.add_argument("--decision-summary")
|
|
6997
8039
|
|
|
6998
8040
|
auto_transition_parser = subcommands.add_parser("auto-transition", parents=[common])
|
|
6999
8041
|
auto_transition_parser.add_argument("--stage", required=True)
|
|
@@ -7005,6 +8047,11 @@ def main() -> int:
|
|
|
7005
8047
|
transition.add_argument("--stage")
|
|
7006
8048
|
transition.add_argument("--agent", required=True)
|
|
7007
8049
|
transition.add_argument("--task-id")
|
|
8050
|
+
transition.add_argument("--diff-sha256")
|
|
8051
|
+
transition.add_argument(
|
|
8052
|
+
"--verification-policy", choices=sorted(ACCEPTANCE_VERIFICATION_POLICIES)
|
|
8053
|
+
)
|
|
8054
|
+
transition.add_argument("--decision-summary")
|
|
7008
8055
|
|
|
7009
8056
|
cancel_transition_parser = subcommands.add_parser("cancel-transition", parents=[common])
|
|
7010
8057
|
cancel_transition_parser.add_argument("--agent", required=True)
|
|
@@ -7413,6 +8460,28 @@ def main() -> int:
|
|
|
7413
8460
|
session_file,
|
|
7414
8461
|
)
|
|
7415
8462
|
)
|
|
8463
|
+
elif command == "verification-checkpoint":
|
|
8464
|
+
emit(
|
|
8465
|
+
attach_status_context(
|
|
8466
|
+
root,
|
|
8467
|
+
record_verification_checkpoint(
|
|
8468
|
+
root, agent, args.task_id, session_file
|
|
8469
|
+
),
|
|
8470
|
+
agent,
|
|
8471
|
+
session_file,
|
|
8472
|
+
)
|
|
8473
|
+
)
|
|
8474
|
+
elif command == "inspect-transition-drift":
|
|
8475
|
+
emit(
|
|
8476
|
+
attach_status_context(
|
|
8477
|
+
root,
|
|
8478
|
+
inspect_transition_drift(
|
|
8479
|
+
root, agent, args.task_id, session_file
|
|
8480
|
+
),
|
|
8481
|
+
agent,
|
|
8482
|
+
session_file,
|
|
8483
|
+
)
|
|
8484
|
+
)
|
|
7416
8485
|
elif command == "disable-harness":
|
|
7417
8486
|
emit(
|
|
7418
8487
|
attach_status_context(
|
|
@@ -7469,7 +8538,16 @@ def main() -> int:
|
|
|
7469
8538
|
emit(
|
|
7470
8539
|
attach_status_context(
|
|
7471
8540
|
root,
|
|
7472
|
-
confirm_transition(
|
|
8541
|
+
confirm_transition(
|
|
8542
|
+
root,
|
|
8543
|
+
agent,
|
|
8544
|
+
args.stage,
|
|
8545
|
+
args.task_id,
|
|
8546
|
+
session_file,
|
|
8547
|
+
args.diff_sha256,
|
|
8548
|
+
args.verification_policy,
|
|
8549
|
+
args.decision_summary,
|
|
8550
|
+
),
|
|
7473
8551
|
agent,
|
|
7474
8552
|
session_file,
|
|
7475
8553
|
)
|