easy-coding-harness 0.10.0-beta.3 → 0.10.0-beta.5
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 +42 -0
- package/README.md +13 -5
- package/package.json +1 -1
- package/templates/claude/agents/ec-implementer.md +1 -0
- package/templates/codex/agents/ec-implementer.toml +1 -0
- package/templates/common/bundled-skills/ec-init/SKILL.md +3 -1
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +15 -7
- package/templates/common/skills/ec-analysis/SKILL.md +50 -9
- package/templates/common/skills/ec-git/SKILL.md +7 -1
- package/templates/common/skills/ec-implementing/SKILL.md +34 -1
- package/templates/common/skills/ec-memory/SKILL.md +65 -3
- package/templates/common/skills/ec-reviewing/SKILL.md +6 -0
- package/templates/common/skills/ec-task-close/SKILL.md +4 -0
- package/templates/common/skills/ec-task-management/SKILL.md +7 -1
- package/templates/common/skills/ec-verification/SKILL.md +11 -1
- package/templates/common/skills/ec-workflow/SKILL.md +16 -2
- package/templates/main-constraint/AGENTS.md.tpl +15 -4
- package/templates/main-constraint/CLAUDE.md.tpl +15 -4
- package/templates/qoder/agents/ec-implementer.md +1 -0
- package/templates/runtime/templates/dev-spec-skeleton.md +8 -1
- package/templates/shared-hooks/easy_coding_state.py +2120 -109
- package/templates/shared-hooks/easy_dev_spec.py +87 -12
- package/templates/shared-hooks/easy_dev_spec_execution.py +1014 -0
- package/templates/shared-hooks/easy_dev_spec_protocol.py +1426 -18
|
@@ -7,6 +7,7 @@ import re
|
|
|
7
7
|
import secrets
|
|
8
8
|
import shlex
|
|
9
9
|
import subprocess
|
|
10
|
+
import tempfile
|
|
10
11
|
import time
|
|
11
12
|
import uuid
|
|
12
13
|
from datetime import datetime, timezone
|
|
@@ -20,6 +21,17 @@ from easy_dev_spec import (
|
|
|
20
21
|
select_consumption_scopes,
|
|
21
22
|
select_tasks,
|
|
22
23
|
)
|
|
24
|
+
from easy_dev_spec_execution import (
|
|
25
|
+
ExecutionConflictError,
|
|
26
|
+
ExecutionStateError,
|
|
27
|
+
initialize_execution,
|
|
28
|
+
record_dependency_status,
|
|
29
|
+
record_step_status,
|
|
30
|
+
record_task_status,
|
|
31
|
+
show_execution,
|
|
32
|
+
sync_design,
|
|
33
|
+
)
|
|
34
|
+
from easy_dev_spec_protocol import split_execution_region
|
|
23
35
|
|
|
24
36
|
|
|
25
37
|
TERMINAL_STATUSES = {"COMPLETE", "CLOSED"}
|
|
@@ -38,6 +50,7 @@ MANDATORY_DEV_SPEC_HEADERS: list[str] = [
|
|
|
38
50
|
"### 需求解析",
|
|
39
51
|
"### 现状",
|
|
40
52
|
"### 冲突摘要",
|
|
53
|
+
"### 决策闭环",
|
|
41
54
|
"### 影响面分析",
|
|
42
55
|
"### 改动范围",
|
|
43
56
|
"### 修改方案",
|
|
@@ -105,6 +118,12 @@ LEGACY_STAGE_MAP = {
|
|
|
105
118
|
|
|
106
119
|
DEFAULT_SHORT_TERM_MAX = 10
|
|
107
120
|
DEFAULT_SHORT_TERM_KEEP = 5
|
|
121
|
+
# 架构认知正文的项目相对路径,用于冻结与复核 ABSTRACT 内容指纹。
|
|
122
|
+
ARCHITECTURE_ABSTRACT_PATH = Path(".easy-coding/ABSTRACT.md")
|
|
123
|
+
# 架构认知变更日志的项目相对路径,用于验证 backfill/update 留下审计记录。
|
|
124
|
+
ARCHITECTURE_CHANGELOG_PATH = Path(".easy-coding/CHANGELOG.md")
|
|
125
|
+
# MEMORY 架构评估唯一允许的动作集合;状态 API 和 CLI 参数共享该契约。
|
|
126
|
+
ARCHITECTURE_ACTIONS = {"no-op", "backfill", "update"}
|
|
108
127
|
SESSION_STALE_THRESHOLD_HOURS = 30 * 24
|
|
109
128
|
SESSION_COMPONENT_PATTERN = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
110
129
|
SESSION_AGENT_NAMESPACES = {"claude-code", "codex", "qoder", "unknown"}
|
|
@@ -117,6 +136,19 @@ SHORT_MEMORY_UUID_V7_PATTERN = re.compile(
|
|
|
117
136
|
)
|
|
118
137
|
LEGACY_SHORT_MEMORY_ID_PATTERN = re.compile(r"^SM-\d{8}-\d+$")
|
|
119
138
|
DEV_SPEC_PLACEHOLDER_PATTERN = re.compile(r"\[\[EC_TODO:[^\]\n]+\]\]")
|
|
139
|
+
DECISION_STATUS_PATTERN = re.compile(
|
|
140
|
+
r"\s*decision_status\s*:\s*([a-z][a-z0-9_-]*)\s*", re.IGNORECASE
|
|
141
|
+
)
|
|
142
|
+
DECISION_CONCLUSIONS_PATTERN = re.compile(
|
|
143
|
+
r"\s*(?:[-+*]\s+)?(?:\*\*)?已解决问题与结论(?:\*\*)?\s*[::]\s*(.+?)\s*"
|
|
144
|
+
)
|
|
145
|
+
DECISION_EVIDENCE_PATTERN = re.compile(
|
|
146
|
+
r"\s*(?:[-+*]\s+)?(?:\*\*)?确认依据(?:\*\*)?\s*[::]\s*(.+?)\s*"
|
|
147
|
+
)
|
|
148
|
+
UNRESOLVED_DECISION_VALUE_PATTERN = re.compile(
|
|
149
|
+
r"(?:待确认|待决策|未确认|未决|todo|tbd|unknown|open|pending|unresolved)[。.!!]?",
|
|
150
|
+
re.IGNORECASE,
|
|
151
|
+
)
|
|
120
152
|
MARKDOWN_HEADING_PATTERN = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
|
|
121
153
|
TABLE_HEADER_CELLS = {
|
|
122
154
|
"改动文件",
|
|
@@ -798,6 +830,76 @@ def validate_recorded_short_memory(
|
|
|
798
830
|
validate_short_memory_file(root, task_id, memory_file, expected_sha256)
|
|
799
831
|
|
|
800
832
|
|
|
833
|
+
def architecture_asset_baseline(root: Path, relative_path: Path) -> dict:
|
|
834
|
+
path = root / relative_path
|
|
835
|
+
if not path.exists():
|
|
836
|
+
return {
|
|
837
|
+
"path": str(relative_path),
|
|
838
|
+
"exists": False,
|
|
839
|
+
"non_empty": False,
|
|
840
|
+
"sha256": None,
|
|
841
|
+
}
|
|
842
|
+
if not path.is_file():
|
|
843
|
+
raise StateError(f"Architecture asset is not a file: {relative_path}")
|
|
844
|
+
try:
|
|
845
|
+
content = path.read_text(encoding="utf-8")
|
|
846
|
+
except (OSError, UnicodeError) as error:
|
|
847
|
+
raise StateError(f"Cannot read architecture asset as UTF-8: {relative_path}") from error
|
|
848
|
+
return {
|
|
849
|
+
"path": str(relative_path),
|
|
850
|
+
"exists": True,
|
|
851
|
+
"non_empty": bool(content.strip()),
|
|
852
|
+
"sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def read_project_mode(root: Path) -> str | None:
|
|
857
|
+
project_profile = root / ".easy-coding" / "project.yaml"
|
|
858
|
+
if not project_profile.is_file():
|
|
859
|
+
return None
|
|
860
|
+
try:
|
|
861
|
+
content = project_profile.read_text(encoding="utf-8")
|
|
862
|
+
except (OSError, UnicodeError) as error:
|
|
863
|
+
raise StateError("Cannot read .easy-coding/project.yaml as UTF-8.") from error
|
|
864
|
+
for raw_line in content.splitlines():
|
|
865
|
+
match = re.fullmatch(
|
|
866
|
+
r"\s*mode\s*:\s*(['\"]?)(startup|iterative)\1\s*(?:#.*)?", raw_line
|
|
867
|
+
)
|
|
868
|
+
if match:
|
|
869
|
+
return match.group(2)
|
|
870
|
+
return None
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def build_architecture_assessment_instruction(root: Path, memory_action: str) -> dict:
|
|
874
|
+
abstract = architecture_asset_baseline(root, ARCHITECTURE_ABSTRACT_PATH)
|
|
875
|
+
changelog = architecture_asset_baseline(root, ARCHITECTURE_CHANGELOG_PATH)
|
|
876
|
+
if not abstract["non_empty"] and read_project_mode(root) == "startup":
|
|
877
|
+
required = True
|
|
878
|
+
trigger = "missing-abstract"
|
|
879
|
+
allowed_actions = ["backfill"]
|
|
880
|
+
elif not abstract["non_empty"]:
|
|
881
|
+
raise StateError(
|
|
882
|
+
"ABSTRACT.md is missing or empty outside the startup backfill exception; "
|
|
883
|
+
"run ec-init supplementary initialization before completing MEMORY."
|
|
884
|
+
)
|
|
885
|
+
elif memory_action == "distill":
|
|
886
|
+
required = True
|
|
887
|
+
trigger = "distillation"
|
|
888
|
+
allowed_actions = ["no-op", "update"]
|
|
889
|
+
else:
|
|
890
|
+
required = False
|
|
891
|
+
trigger = "none"
|
|
892
|
+
allowed_actions = []
|
|
893
|
+
instruction = {
|
|
894
|
+
"required": required,
|
|
895
|
+
"trigger": trigger,
|
|
896
|
+
"allowed_actions": allowed_actions,
|
|
897
|
+
"abstract": abstract,
|
|
898
|
+
"changelog": changelog,
|
|
899
|
+
}
|
|
900
|
+
return instruction
|
|
901
|
+
|
|
902
|
+
|
|
801
903
|
def build_memory_instruction(
|
|
802
904
|
root: Path,
|
|
803
905
|
checkpoint_file: str | None = None,
|
|
@@ -818,7 +920,7 @@ def build_memory_instruction(
|
|
|
818
920
|
checkpoint_disposition = "kept"
|
|
819
921
|
else:
|
|
820
922
|
raise StateError("Recorded short-memory checkpoint is absent from the frozen memory set.")
|
|
821
|
-
|
|
923
|
+
instruction = {
|
|
822
924
|
"short_count": short_count,
|
|
823
925
|
"short_term_max": config["short_term_max"],
|
|
824
926
|
"short_term_keep": config["short_term_keep"],
|
|
@@ -828,6 +930,216 @@ def build_memory_instruction(
|
|
|
828
930
|
"kept_files": kept_files,
|
|
829
931
|
"checkpoint_disposition": checkpoint_disposition,
|
|
830
932
|
}
|
|
933
|
+
if not legacy_checkpoint:
|
|
934
|
+
instruction["architecture_assessment"] = build_architecture_assessment_instruction(
|
|
935
|
+
root, action
|
|
936
|
+
)
|
|
937
|
+
return instruction
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def require_architecture_instruction(instruction: dict) -> dict | None:
|
|
941
|
+
assessment_instruction = instruction.get("architecture_assessment")
|
|
942
|
+
if assessment_instruction is None:
|
|
943
|
+
# 0.10.0-beta.5 之前已冻结的指令继续按旧契约完成,避免升级中断在途任务。
|
|
944
|
+
return None
|
|
945
|
+
if not isinstance(assessment_instruction, dict):
|
|
946
|
+
raise StateError("Memory instruction has an invalid architecture assessment contract.")
|
|
947
|
+
return assessment_instruction
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def validate_architecture_asset_changed(
|
|
951
|
+
baseline: dict,
|
|
952
|
+
current: dict,
|
|
953
|
+
label: str,
|
|
954
|
+
) -> None:
|
|
955
|
+
if not current.get("exists") or not current.get("non_empty") or not current.get("sha256"):
|
|
956
|
+
raise StateError(f"Architecture {label} must exist and be non-empty after this action.")
|
|
957
|
+
if baseline.get("sha256") == current.get("sha256"):
|
|
958
|
+
raise StateError(f"Architecture {label} did not change after this action.")
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def validate_architecture_assets_unchanged(root: Path, instruction: dict) -> None:
|
|
962
|
+
for key, relative_path in (
|
|
963
|
+
("abstract", ARCHITECTURE_ABSTRACT_PATH),
|
|
964
|
+
("changelog", ARCHITECTURE_CHANGELOG_PATH),
|
|
965
|
+
):
|
|
966
|
+
baseline = instruction.get(key)
|
|
967
|
+
if not isinstance(baseline, dict):
|
|
968
|
+
raise StateError(f"Architecture assessment is missing the {key} baseline.")
|
|
969
|
+
if architecture_asset_baseline(root, relative_path) != baseline:
|
|
970
|
+
raise StateError(f"Architecture asset changed during a no-op assessment: {relative_path}")
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def validate_architecture_action_result(
|
|
974
|
+
root: Path,
|
|
975
|
+
instruction: dict,
|
|
976
|
+
action: str,
|
|
977
|
+
) -> tuple[dict, dict]:
|
|
978
|
+
abstract_before = instruction.get("abstract")
|
|
979
|
+
changelog_before = instruction.get("changelog")
|
|
980
|
+
if not isinstance(abstract_before, dict) or not isinstance(changelog_before, dict):
|
|
981
|
+
raise StateError("Architecture assessment is missing frozen asset baselines.")
|
|
982
|
+
abstract = architecture_asset_baseline(root, ARCHITECTURE_ABSTRACT_PATH)
|
|
983
|
+
changelog = architecture_asset_baseline(root, ARCHITECTURE_CHANGELOG_PATH)
|
|
984
|
+
if action == "no-op":
|
|
985
|
+
validate_architecture_assets_unchanged(root, instruction)
|
|
986
|
+
elif action == "backfill":
|
|
987
|
+
if abstract_before.get("non_empty") is True:
|
|
988
|
+
raise StateError(
|
|
989
|
+
"Architecture backfill is allowed only when ABSTRACT.md was missing or empty."
|
|
990
|
+
)
|
|
991
|
+
validate_architecture_asset_changed(abstract_before, abstract, "ABSTRACT.md")
|
|
992
|
+
validate_architecture_asset_changed(changelog_before, changelog, "CHANGELOG.md")
|
|
993
|
+
elif action == "update":
|
|
994
|
+
if abstract_before.get("non_empty") is not True:
|
|
995
|
+
raise StateError("Architecture update requires an existing ABSTRACT.md baseline.")
|
|
996
|
+
validate_architecture_asset_changed(abstract_before, abstract, "ABSTRACT.md")
|
|
997
|
+
validate_architecture_asset_changed(changelog_before, changelog, "CHANGELOG.md")
|
|
998
|
+
else:
|
|
999
|
+
raise StateError(f"Unknown architecture assessment action: {action}")
|
|
1000
|
+
return abstract, changelog
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def allowed_architecture_evidence(progress: dict, instruction: dict) -> set[str]:
|
|
1004
|
+
allowed_evidence = set(instruction.get("candidate_files") or [])
|
|
1005
|
+
if not allowed_evidence:
|
|
1006
|
+
checkpoint_file = progress.get("short_memory_file")
|
|
1007
|
+
if isinstance(checkpoint_file, str):
|
|
1008
|
+
allowed_evidence.add(checkpoint_file)
|
|
1009
|
+
return allowed_evidence
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def record_architecture_assessment(
|
|
1013
|
+
root: Path,
|
|
1014
|
+
action: str,
|
|
1015
|
+
reason: str,
|
|
1016
|
+
evidence: list[str],
|
|
1017
|
+
affected_sections: list[str],
|
|
1018
|
+
agent: str,
|
|
1019
|
+
task_id: str | None = None,
|
|
1020
|
+
session_file: str | Path | None = None,
|
|
1021
|
+
) -> dict:
|
|
1022
|
+
if action not in ARCHITECTURE_ACTIONS:
|
|
1023
|
+
raise StateError(f"Unknown architecture assessment action: {action}")
|
|
1024
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
1025
|
+
if task.get("status") != "MEMORY":
|
|
1026
|
+
raise StateError("Architecture assessment is only available during MEMORY.")
|
|
1027
|
+
progress = task.get("memory_progress")
|
|
1028
|
+
if not isinstance(progress, dict) or progress.get("short_memory_written") is not True:
|
|
1029
|
+
raise StateError("Short memory must be recorded before architecture assessment.")
|
|
1030
|
+
instruction = progress.get("instruction")
|
|
1031
|
+
if not isinstance(instruction, dict):
|
|
1032
|
+
raise StateError("Request the authoritative memory instruction before architecture assessment.")
|
|
1033
|
+
validate_recorded_short_memory(root, resolved_task_id, progress)
|
|
1034
|
+
for candidate_file in instruction.get("candidate_files") or []:
|
|
1035
|
+
if not resolve_short_memory_path(root, candidate_file).is_file():
|
|
1036
|
+
raise StateError(
|
|
1037
|
+
"Keep every frozen distillation candidate until architecture assessment succeeds: "
|
|
1038
|
+
f"{candidate_file}"
|
|
1039
|
+
)
|
|
1040
|
+
assessment_instruction = require_architecture_instruction(instruction)
|
|
1041
|
+
if assessment_instruction is None:
|
|
1042
|
+
raise StateError("Legacy memory instructions do not require an architecture assessment.")
|
|
1043
|
+
if assessment_instruction.get("required") is not True:
|
|
1044
|
+
raise StateError("Architecture assessment is not required for this memory instruction.")
|
|
1045
|
+
allowed_actions = assessment_instruction.get("allowed_actions")
|
|
1046
|
+
if not isinstance(allowed_actions, list) or action not in allowed_actions:
|
|
1047
|
+
raise StateError(
|
|
1048
|
+
f"Architecture action {action} is not allowed for trigger "
|
|
1049
|
+
f"{assessment_instruction.get('trigger')}."
|
|
1050
|
+
)
|
|
1051
|
+
normalized_reason = reason.strip()
|
|
1052
|
+
normalized_evidence = list(dict.fromkeys(item.strip() for item in evidence if item.strip()))
|
|
1053
|
+
normalized_sections = list(
|
|
1054
|
+
dict.fromkeys(item.strip() for item in affected_sections if item.strip())
|
|
1055
|
+
)
|
|
1056
|
+
if not normalized_reason:
|
|
1057
|
+
raise StateError("Architecture assessment requires a non-empty reason.")
|
|
1058
|
+
if not normalized_evidence:
|
|
1059
|
+
raise StateError("Architecture assessment requires at least one frozen memory evidence file.")
|
|
1060
|
+
allowed_evidence = allowed_architecture_evidence(progress, instruction)
|
|
1061
|
+
invalid_evidence = [item for item in normalized_evidence if item not in allowed_evidence]
|
|
1062
|
+
if invalid_evidence:
|
|
1063
|
+
raise StateError(
|
|
1064
|
+
"Architecture assessment evidence must come from the frozen memory set: "
|
|
1065
|
+
+ ", ".join(invalid_evidence)
|
|
1066
|
+
)
|
|
1067
|
+
if action in {"backfill", "update"} and not normalized_sections:
|
|
1068
|
+
raise StateError("Architecture backfill/update requires at least one affected section.")
|
|
1069
|
+
if action == "no-op" and normalized_sections:
|
|
1070
|
+
raise StateError("Architecture no-op must not declare affected sections.")
|
|
1071
|
+
|
|
1072
|
+
abstract, changelog = validate_architecture_action_result(
|
|
1073
|
+
root, assessment_instruction, action
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
assessment = {
|
|
1077
|
+
"action": action,
|
|
1078
|
+
"trigger": assessment_instruction.get("trigger"),
|
|
1079
|
+
"reason": normalized_reason,
|
|
1080
|
+
"evidence": normalized_evidence,
|
|
1081
|
+
"affected_sections": normalized_sections,
|
|
1082
|
+
"abstract_sha256": abstract.get("sha256"),
|
|
1083
|
+
"changelog_sha256": changelog.get("sha256"),
|
|
1084
|
+
"recorded_at": now_iso(),
|
|
1085
|
+
"recorded_by": agent,
|
|
1086
|
+
}
|
|
1087
|
+
progress["architecture_assessment"] = assessment
|
|
1088
|
+
progress["updated_at"] = now_iso()
|
|
1089
|
+
task["memory_progress"] = progress
|
|
1090
|
+
task["last_agent"] = agent
|
|
1091
|
+
write_task(root, resolved_task_id, task)
|
|
1092
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
1093
|
+
snapshot["memory"] = instruction
|
|
1094
|
+
snapshot["architecture_assessment"] = assessment
|
|
1095
|
+
snapshot["action"] = "memory-architecture-assessment"
|
|
1096
|
+
return snapshot
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def validate_recorded_architecture_assessment(root: Path, progress: dict, instruction: dict) -> None:
|
|
1100
|
+
assessment_instruction = require_architecture_instruction(instruction)
|
|
1101
|
+
if assessment_instruction is None:
|
|
1102
|
+
return
|
|
1103
|
+
if assessment_instruction.get("required") is not True:
|
|
1104
|
+
validate_architecture_assets_unchanged(root, assessment_instruction)
|
|
1105
|
+
if progress.get("architecture_assessment") is not None:
|
|
1106
|
+
raise StateError("Unexpected architecture assessment for a no-op memory instruction.")
|
|
1107
|
+
return
|
|
1108
|
+
assessment = progress.get("architecture_assessment")
|
|
1109
|
+
if not isinstance(assessment, dict):
|
|
1110
|
+
raise StateError("Complete the required architecture assessment before MEMORY completion.")
|
|
1111
|
+
action = assessment.get("action")
|
|
1112
|
+
allowed_actions = assessment_instruction.get("allowed_actions")
|
|
1113
|
+
if not isinstance(allowed_actions, list) or action not in allowed_actions:
|
|
1114
|
+
raise StateError("Recorded architecture assessment has an invalid action.")
|
|
1115
|
+
if assessment.get("trigger") != assessment_instruction.get("trigger"):
|
|
1116
|
+
raise StateError("Recorded architecture assessment trigger does not match its instruction.")
|
|
1117
|
+
reason = assessment.get("reason")
|
|
1118
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
1119
|
+
raise StateError("Recorded architecture assessment is missing its reason.")
|
|
1120
|
+
evidence = assessment.get("evidence")
|
|
1121
|
+
if not isinstance(evidence, list) or not evidence or not all(
|
|
1122
|
+
isinstance(item, str) for item in evidence
|
|
1123
|
+
):
|
|
1124
|
+
raise StateError("Recorded architecture assessment has invalid evidence.")
|
|
1125
|
+
if any(item not in allowed_architecture_evidence(progress, instruction) for item in evidence):
|
|
1126
|
+
raise StateError("Recorded architecture assessment evidence is outside the frozen set.")
|
|
1127
|
+
affected_sections = assessment.get("affected_sections")
|
|
1128
|
+
if not isinstance(affected_sections, list) or not all(
|
|
1129
|
+
isinstance(item, str) and item.strip() for item in affected_sections
|
|
1130
|
+
):
|
|
1131
|
+
raise StateError("Recorded architecture assessment has invalid affected sections.")
|
|
1132
|
+
if action == "no-op" and affected_sections:
|
|
1133
|
+
raise StateError("Recorded architecture no-op must not declare affected sections.")
|
|
1134
|
+
if action in {"backfill", "update"} and not affected_sections:
|
|
1135
|
+
raise StateError("Recorded architecture backfill/update requires affected sections.")
|
|
1136
|
+
abstract, changelog = validate_architecture_action_result(
|
|
1137
|
+
root, assessment_instruction, action
|
|
1138
|
+
)
|
|
1139
|
+
if assessment.get("abstract_sha256") != abstract.get("sha256"):
|
|
1140
|
+
raise StateError("ABSTRACT.md changed after the architecture assessment was recorded.")
|
|
1141
|
+
if assessment.get("changelog_sha256") != changelog.get("sha256"):
|
|
1142
|
+
raise StateError("Architecture CHANGELOG.md changed after the assessment was recorded.")
|
|
831
1143
|
|
|
832
1144
|
|
|
833
1145
|
def validate_distillation_file_sets(root: Path, instruction: dict) -> None:
|
|
@@ -909,7 +1221,28 @@ def normalize_legacy_task(task: dict) -> bool:
|
|
|
909
1221
|
|
|
910
1222
|
def write_json(path: Path, data: dict) -> None:
|
|
911
1223
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
912
|
-
|
|
1224
|
+
descriptor, temporary_name = tempfile.mkstemp(
|
|
1225
|
+
prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
|
|
1226
|
+
)
|
|
1227
|
+
temporary_path = Path(temporary_name)
|
|
1228
|
+
try:
|
|
1229
|
+
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
|
|
1230
|
+
handle.write(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
|
1231
|
+
handle.flush()
|
|
1232
|
+
os.fsync(handle.fileno())
|
|
1233
|
+
os.replace(temporary_path, path)
|
|
1234
|
+
try:
|
|
1235
|
+
directory_descriptor = os.open(path.parent, os.O_RDONLY)
|
|
1236
|
+
try:
|
|
1237
|
+
os.fsync(directory_descriptor)
|
|
1238
|
+
finally:
|
|
1239
|
+
os.close(directory_descriptor)
|
|
1240
|
+
except OSError:
|
|
1241
|
+
# Some platforms do not allow opening directories; file replacement is still atomic.
|
|
1242
|
+
pass
|
|
1243
|
+
finally:
|
|
1244
|
+
if temporary_path.exists():
|
|
1245
|
+
temporary_path.unlink()
|
|
913
1246
|
|
|
914
1247
|
|
|
915
1248
|
def acquire_legacy_state_lock(root: Path) -> Path | None:
|
|
@@ -1181,6 +1514,8 @@ def append_execution_record(root: Path, task_id: str, record: dict) -> None:
|
|
|
1181
1514
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1182
1515
|
with path.open("a", encoding="utf-8") as handle:
|
|
1183
1516
|
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
1517
|
+
handle.flush()
|
|
1518
|
+
os.fsync(handle.fileno())
|
|
1184
1519
|
|
|
1185
1520
|
|
|
1186
1521
|
def is_non_empty_string(value: object) -> bool:
|
|
@@ -1327,25 +1662,58 @@ def stored_spec_path(root: Path, task: dict) -> Path:
|
|
|
1327
1662
|
source = task.get("spec_source")
|
|
1328
1663
|
if not isinstance(source, dict) or not is_non_empty_string(source.get("path")):
|
|
1329
1664
|
raise StateError("Spec-backed task is missing spec_source.path.")
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1665
|
+
path_mode = source.get("path_mode")
|
|
1666
|
+
raw_path = Path(str(source["path"])).expanduser()
|
|
1667
|
+
if path_mode is None:
|
|
1668
|
+
path_mode = "absolute" if raw_path.is_absolute() else "project-relative"
|
|
1669
|
+
if path_mode not in {"project-relative", "absolute"}:
|
|
1670
|
+
raise StateError("Spec-backed task has an invalid spec_source.path_mode.")
|
|
1671
|
+
if path_mode == "absolute" and not raw_path.is_absolute():
|
|
1672
|
+
raise StateError("Absolute Spec binding must store an absolute path.")
|
|
1673
|
+
if path_mode == "project-relative" and raw_path.is_absolute():
|
|
1674
|
+
raise StateError("Project-relative Spec binding must not store an absolute path.")
|
|
1675
|
+
resolved = (raw_path if path_mode == "absolute" else root / raw_path).resolve()
|
|
1676
|
+
if path_mode == "project-relative":
|
|
1677
|
+
try:
|
|
1678
|
+
resolved.relative_to(root.resolve())
|
|
1679
|
+
except ValueError as exc:
|
|
1680
|
+
raise StateError("Project-relative Spec source escapes the project root.") from exc
|
|
1681
|
+
if not resolved.is_file():
|
|
1682
|
+
raise StateError(
|
|
1683
|
+
"Canonical Spec source is unavailable; run rebind-spec-source with an explicit path."
|
|
1684
|
+
)
|
|
1337
1685
|
return resolved
|
|
1338
1686
|
|
|
1339
1687
|
|
|
1688
|
+
def legacy_source_digest_matches(
|
|
1689
|
+
spec_path: Path, legacy_sha256: object, current_source_sha256: object
|
|
1690
|
+
) -> bool:
|
|
1691
|
+
if not is_non_empty_string(legacy_sha256):
|
|
1692
|
+
return False
|
|
1693
|
+
if legacy_sha256 == current_source_sha256:
|
|
1694
|
+
return True
|
|
1695
|
+
try:
|
|
1696
|
+
design_text, execution = split_execution_region(
|
|
1697
|
+
spec_path.read_text(encoding="utf-8")
|
|
1698
|
+
)
|
|
1699
|
+
except (OSError, UnicodeError, ValueError):
|
|
1700
|
+
return False
|
|
1701
|
+
if execution is None:
|
|
1702
|
+
return False
|
|
1703
|
+
design_document_sha256 = hashlib.sha256(design_text.encode("utf-8")).hexdigest()
|
|
1704
|
+
return legacy_sha256 == design_document_sha256
|
|
1705
|
+
|
|
1706
|
+
|
|
1340
1707
|
def inspect_task_spec(root: Path, task: dict) -> tuple[dict, dict]:
|
|
1341
1708
|
source = task.get("spec_source")
|
|
1342
1709
|
selected = task.get("selected_spec_tasks")
|
|
1343
1710
|
repo_paths = task.get("repo_paths")
|
|
1344
1711
|
if not isinstance(source, dict) or not is_string_list(selected, allow_empty=False):
|
|
1345
1712
|
raise StateError("Spec-backed task source and selected task metadata are incomplete.")
|
|
1713
|
+
spec_path = stored_spec_path(root, task)
|
|
1346
1714
|
try:
|
|
1347
1715
|
inspection = inspect_spec(
|
|
1348
|
-
|
|
1716
|
+
spec_path,
|
|
1349
1717
|
root,
|
|
1350
1718
|
repo_paths if isinstance(repo_paths, dict) else {},
|
|
1351
1719
|
selected,
|
|
@@ -1361,6 +1729,10 @@ def inspect_task_spec(root: Path, task: dict) -> tuple[dict, dict]:
|
|
|
1361
1729
|
selection = select_tasks(inspection, selected, satisfied)
|
|
1362
1730
|
except EasyDevSpecError as exc:
|
|
1363
1731
|
raise StateError(f"Canonical Spec validation failed: {exc}") from exc
|
|
1732
|
+
if not isinstance(inspection.get("execution"), dict):
|
|
1733
|
+
raise StateError(
|
|
1734
|
+
"Canonical Spec shared execution is not initialized; run initialize-spec-execution."
|
|
1735
|
+
)
|
|
1364
1736
|
stored_dependencies = task.get("spec_dependency_evidence")
|
|
1365
1737
|
if not isinstance(stored_dependencies, list):
|
|
1366
1738
|
raise StateError("Spec-backed task dependency metadata is incomplete.")
|
|
@@ -1373,30 +1745,44 @@ def inspect_task_spec(root: Path, task: dict) -> tuple[dict, dict]:
|
|
|
1373
1745
|
for record in stored_dependencies
|
|
1374
1746
|
if isinstance(record, dict)
|
|
1375
1747
|
}
|
|
1376
|
-
if (
|
|
1377
|
-
len(stored_by_edge) != len(stored_dependencies)
|
|
1378
|
-
or set(stored_by_edge) != set(expected_by_edge)
|
|
1379
|
-
):
|
|
1748
|
+
if len(stored_by_edge) != len(stored_dependencies) or set(stored_by_edge) != set(expected_by_edge):
|
|
1380
1749
|
raise StateError("Canonical Spec dependency metadata no longer matches source selection.")
|
|
1750
|
+
refreshed_dependencies: list[dict] = []
|
|
1381
1751
|
for edge, expected in expected_by_edge.items():
|
|
1382
1752
|
stored = stored_by_edge[edge]
|
|
1383
|
-
for field in ("dependency_type", "required_evidence"
|
|
1753
|
+
for field in ("dependency_type", "required_evidence"):
|
|
1384
1754
|
if stored.get(field) != expected.get(field):
|
|
1385
|
-
raise StateError(
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1755
|
+
raise StateError("Canonical Spec dependency metadata no longer matches source selection.")
|
|
1756
|
+
refreshed = dict(stored)
|
|
1757
|
+
refreshed["status"] = expected.get("status")
|
|
1758
|
+
refreshed["shared_status"] = expected.get("shared_status")
|
|
1759
|
+
if expected.get("evidence"):
|
|
1760
|
+
refreshed["evidence"] = expected.get("evidence")
|
|
1761
|
+
refreshed_dependencies.append(refreshed)
|
|
1392
1762
|
if source.get("schema") != inspection.get("schema"):
|
|
1393
1763
|
raise StateError("Canonical Spec schema no longer matches task.json.")
|
|
1394
1764
|
if source.get("spec_id") != inspection.get("spec_id"):
|
|
1395
1765
|
raise StateError("Canonical Spec ID no longer matches task.json.")
|
|
1396
1766
|
if source.get("revision") != inspection.get("revision"):
|
|
1397
|
-
raise StateError("Canonical Spec revision
|
|
1398
|
-
|
|
1399
|
-
|
|
1767
|
+
raise StateError("Canonical Spec design revision changed; return the task to ANALYSIS.")
|
|
1768
|
+
stored_design_sha256 = source.get("design_sha256")
|
|
1769
|
+
if stored_design_sha256 is None:
|
|
1770
|
+
if not legacy_source_digest_matches(
|
|
1771
|
+
spec_path, source.get("sha256"), inspection.get("source_sha256")
|
|
1772
|
+
):
|
|
1773
|
+
raise StateError(
|
|
1774
|
+
"Legacy Canonical Spec digest changed before migration; rebind or recreate the task."
|
|
1775
|
+
)
|
|
1776
|
+
stored_design_sha256 = inspection.get("design_sha256")
|
|
1777
|
+
if stored_design_sha256 != inspection.get("design_sha256"):
|
|
1778
|
+
raise StateError("Canonical Spec static design changed; return the task to ANALYSIS.")
|
|
1779
|
+
stored_execution_revision = source.get("execution_revision")
|
|
1780
|
+
current_execution_revision = inspection.get("execution_revision")
|
|
1781
|
+
if isinstance(stored_execution_revision, int) and isinstance(current_execution_revision, int):
|
|
1782
|
+
if current_execution_revision < stored_execution_revision:
|
|
1783
|
+
raise StateError(
|
|
1784
|
+
"Canonical Spec execution revision moved backwards; restore the latest shared Spec."
|
|
1785
|
+
)
|
|
1400
1786
|
selected_repo_ids = set(selection["selected_repo_ids"])
|
|
1401
1787
|
stored_bindings = task.get("spec_repositories")
|
|
1402
1788
|
if not isinstance(stored_bindings, list):
|
|
@@ -1424,6 +1810,18 @@ def inspect_task_spec(root: Path, task: dict) -> tuple[dict, dict]:
|
|
|
1424
1810
|
for field in ("repo_id", "name", "path", "baseline_commit"):
|
|
1425
1811
|
if stored.get(field) != current.get(field):
|
|
1426
1812
|
raise StateError("Canonical Spec repository bindings no longer match task.json.")
|
|
1813
|
+
source.update(
|
|
1814
|
+
{
|
|
1815
|
+
"path_mode": source.get("path_mode")
|
|
1816
|
+
or ("absolute" if Path(str(source.get("path"))).is_absolute() else "project-relative"),
|
|
1817
|
+
"design_sha256": inspection.get("design_sha256"),
|
|
1818
|
+
"document_sha256": inspection.get("document_sha256"),
|
|
1819
|
+
"execution_revision": inspection.get("execution_revision"),
|
|
1820
|
+
}
|
|
1821
|
+
)
|
|
1822
|
+
source.pop("sha256", None)
|
|
1823
|
+
task["spec_source"] = source
|
|
1824
|
+
task["spec_dependency_evidence"] = refreshed_dependencies
|
|
1427
1825
|
return inspection, selection
|
|
1428
1826
|
|
|
1429
1827
|
|
|
@@ -1682,6 +2080,8 @@ def has_valid_execution_plan(root: Path, task_id: str) -> bool:
|
|
|
1682
2080
|
return False
|
|
1683
2081
|
if isinstance(record, dict) and record.get("type") == "plan":
|
|
1684
2082
|
latest_plan = record
|
|
2083
|
+
elif isinstance(record, dict) and record.get("type") == "spec-design-sync":
|
|
2084
|
+
latest_plan = None
|
|
1685
2085
|
except OSError:
|
|
1686
2086
|
return False
|
|
1687
2087
|
task = load_task(root, task_id)
|
|
@@ -1721,6 +2121,8 @@ def latest_execution_plan(root: Path, task_id: str) -> dict | None:
|
|
|
1721
2121
|
for record in execution_records(root, task_id):
|
|
1722
2122
|
if record.get("type") == "plan":
|
|
1723
2123
|
latest = record
|
|
2124
|
+
elif record.get("type") == "spec-design-sync":
|
|
2125
|
+
latest = None
|
|
1724
2126
|
if latest is None or not is_valid_execution_plan(latest, allow_empty_files=True):
|
|
1725
2127
|
return None
|
|
1726
2128
|
return latest
|
|
@@ -2176,10 +2578,16 @@ def implementation_fingerprint(root: Path, task_id: str) -> str:
|
|
|
2176
2578
|
digest.update(b"\0")
|
|
2177
2579
|
if task and isinstance(task.get("spec_source"), dict):
|
|
2178
2580
|
digest.update(b"canonical-spec\0")
|
|
2581
|
+
source = task.get("spec_source") or {}
|
|
2179
2582
|
digest.update(
|
|
2180
2583
|
json.dumps(
|
|
2181
2584
|
{
|
|
2182
|
-
"source":
|
|
2585
|
+
"source": {
|
|
2586
|
+
"schema": source.get("schema"),
|
|
2587
|
+
"spec_id": source.get("spec_id"),
|
|
2588
|
+
"revision": source.get("revision"),
|
|
2589
|
+
"design_sha256": source.get("design_sha256"),
|
|
2590
|
+
},
|
|
2183
2591
|
"selected_tasks": task.get("selected_spec_tasks"),
|
|
2184
2592
|
},
|
|
2185
2593
|
ensure_ascii=False,
|
|
@@ -2910,19 +3318,35 @@ def validate_read_only_completion(root: Path, task_id: str) -> None:
|
|
|
2910
3318
|
)
|
|
2911
3319
|
|
|
2912
3320
|
|
|
3321
|
+
def markdown_fence_token(line: str) -> tuple[str, int, str] | None:
|
|
3322
|
+
stripped = line.lstrip()
|
|
3323
|
+
if not stripped or stripped[0] not in {"`", "~"}:
|
|
3324
|
+
return None
|
|
3325
|
+
marker = stripped[0]
|
|
3326
|
+
run_length = len(stripped) - len(stripped.lstrip(marker))
|
|
3327
|
+
if run_length < 3:
|
|
3328
|
+
return None
|
|
3329
|
+
return marker, run_length, stripped[run_length:]
|
|
3330
|
+
|
|
3331
|
+
|
|
2913
3332
|
def markdown_headings(content: str) -> list[tuple[int, int, str]]:
|
|
2914
3333
|
headings: list[tuple[int, int, str]] = []
|
|
2915
|
-
fence_marker: str | None = None
|
|
3334
|
+
fence_marker: tuple[str, int] | None = None
|
|
2916
3335
|
for index, line in enumerate(content.splitlines()):
|
|
2917
|
-
|
|
2918
|
-
if
|
|
2919
|
-
marker =
|
|
2920
|
-
if
|
|
2921
|
-
|
|
2922
|
-
|
|
3336
|
+
fence = markdown_fence_token(line)
|
|
3337
|
+
if fence_marker is not None:
|
|
3338
|
+
marker, run_length, remainder = fence or ("", 0, "")
|
|
3339
|
+
if (
|
|
3340
|
+
marker == fence_marker[0]
|
|
3341
|
+
and run_length >= fence_marker[1]
|
|
3342
|
+
and not remainder.strip()
|
|
3343
|
+
):
|
|
2923
3344
|
fence_marker = None
|
|
2924
3345
|
continue
|
|
2925
|
-
if
|
|
3346
|
+
if fence is not None:
|
|
3347
|
+
marker, run_length, remainder = fence
|
|
3348
|
+
if marker != "`" or "`" not in remainder:
|
|
3349
|
+
fence_marker = (marker, run_length)
|
|
2926
3350
|
continue
|
|
2927
3351
|
match = MARKDOWN_HEADING_PATTERN.match(line.strip())
|
|
2928
3352
|
if match:
|
|
@@ -2930,6 +3354,55 @@ def markdown_headings(content: str) -> list[tuple[int, int, str]]:
|
|
|
2930
3354
|
return headings
|
|
2931
3355
|
|
|
2932
3356
|
|
|
3357
|
+
def markdown_section_body(content: str, title: str, level: int = 3) -> str | None:
|
|
3358
|
+
lines = content.splitlines()
|
|
3359
|
+
headings = markdown_headings(content)
|
|
3360
|
+
heading_index = next(
|
|
3361
|
+
(
|
|
3362
|
+
index
|
|
3363
|
+
for index, (_, heading_level, heading_title) in enumerate(headings)
|
|
3364
|
+
if heading_level == level and heading_title == title
|
|
3365
|
+
),
|
|
3366
|
+
None,
|
|
3367
|
+
)
|
|
3368
|
+
if heading_index is None:
|
|
3369
|
+
return None
|
|
3370
|
+
line_index, heading_level, _ = headings[heading_index]
|
|
3371
|
+
next_line_index = len(lines)
|
|
3372
|
+
for candidate_line, candidate_level, _ in headings[heading_index + 1 :]:
|
|
3373
|
+
if candidate_level <= heading_level:
|
|
3374
|
+
next_line_index = candidate_line
|
|
3375
|
+
break
|
|
3376
|
+
return "\n".join(lines[line_index + 1 : next_line_index])
|
|
3377
|
+
|
|
3378
|
+
|
|
3379
|
+
def markdown_standalone_field_values(content: str, pattern: re.Pattern[str]) -> list[str]:
|
|
3380
|
+
values: list[str] = []
|
|
3381
|
+
fence_marker: tuple[str, int] | None = None
|
|
3382
|
+
for line in content.splitlines():
|
|
3383
|
+
fence = markdown_fence_token(line)
|
|
3384
|
+
if fence_marker is not None:
|
|
3385
|
+
marker, run_length, remainder = fence or ("", 0, "")
|
|
3386
|
+
if (
|
|
3387
|
+
marker == fence_marker[0]
|
|
3388
|
+
and run_length >= fence_marker[1]
|
|
3389
|
+
and not remainder.strip()
|
|
3390
|
+
):
|
|
3391
|
+
fence_marker = None
|
|
3392
|
+
continue
|
|
3393
|
+
if fence is not None:
|
|
3394
|
+
marker, run_length, remainder = fence
|
|
3395
|
+
if marker != "`" or "`" not in remainder:
|
|
3396
|
+
fence_marker = (marker, run_length)
|
|
3397
|
+
continue
|
|
3398
|
+
if line.startswith(("\t", " ")):
|
|
3399
|
+
continue
|
|
3400
|
+
match = pattern.fullmatch(line)
|
|
3401
|
+
if match:
|
|
3402
|
+
values.append(match.group(1).strip())
|
|
3403
|
+
return values
|
|
3404
|
+
|
|
3405
|
+
|
|
2933
3406
|
def has_meaningful_markdown_body(content: str) -> bool:
|
|
2934
3407
|
for line in content.splitlines():
|
|
2935
3408
|
stripped = line.strip()
|
|
@@ -3046,6 +3519,71 @@ def validate_analysis_readiness(
|
|
|
3046
3519
|
if "[阶段:ANALYSIS]" in dev_spec_content or "### 待用户决策" in dev_spec_content:
|
|
3047
3520
|
reasons.append("dev-spec.md contains forbidden analysis-only sections")
|
|
3048
3521
|
|
|
3522
|
+
decision_headings = [
|
|
3523
|
+
heading
|
|
3524
|
+
for heading in markdown_headings(dev_spec_content)
|
|
3525
|
+
if heading[1] == 3 and heading[2] == "决策闭环"
|
|
3526
|
+
]
|
|
3527
|
+
if len(decision_headings) != 1:
|
|
3528
|
+
reasons.append(
|
|
3529
|
+
"dev-spec.md must contain exactly one `### 决策闭环` section; "
|
|
3530
|
+
f"found {len(decision_headings)}"
|
|
3531
|
+
)
|
|
3532
|
+
decision_section = markdown_section_body(dev_spec_content, "决策闭环") or ""
|
|
3533
|
+
all_decision_statuses = [
|
|
3534
|
+
value.lower()
|
|
3535
|
+
for value in markdown_standalone_field_values(
|
|
3536
|
+
dev_spec_content, DECISION_STATUS_PATTERN
|
|
3537
|
+
)
|
|
3538
|
+
]
|
|
3539
|
+
section_decision_statuses = [
|
|
3540
|
+
value.lower()
|
|
3541
|
+
for value in markdown_standalone_field_values(
|
|
3542
|
+
decision_section, DECISION_STATUS_PATTERN
|
|
3543
|
+
)
|
|
3544
|
+
]
|
|
3545
|
+
if not all_decision_statuses:
|
|
3546
|
+
reasons.append(
|
|
3547
|
+
"dev-spec.md is missing the decision closure marker `decision_status: closed`; "
|
|
3548
|
+
"resume ec-analysis, resolve material questions, and record the conclusions first"
|
|
3549
|
+
)
|
|
3550
|
+
elif len(all_decision_statuses) != 1:
|
|
3551
|
+
reasons.append(
|
|
3552
|
+
"dev-spec.md must contain exactly one decision_status marker; "
|
|
3553
|
+
f"found {len(all_decision_statuses)}"
|
|
3554
|
+
)
|
|
3555
|
+
elif len(section_decision_statuses) != 1:
|
|
3556
|
+
reasons.append(
|
|
3557
|
+
"dev-spec.md decision_status marker must be inside the `### 决策闭环` section"
|
|
3558
|
+
)
|
|
3559
|
+
elif section_decision_statuses[0] != "closed":
|
|
3560
|
+
reasons.append(
|
|
3561
|
+
"dev-spec.md has unresolved material decisions: "
|
|
3562
|
+
f"decision_status is {section_decision_statuses[0]!r}, expected 'closed'"
|
|
3563
|
+
)
|
|
3564
|
+
decision_conclusions = markdown_standalone_field_values(
|
|
3565
|
+
decision_section, DECISION_CONCLUSIONS_PATTERN
|
|
3566
|
+
)
|
|
3567
|
+
decision_evidence = markdown_standalone_field_values(
|
|
3568
|
+
decision_section, DECISION_EVIDENCE_PATTERN
|
|
3569
|
+
)
|
|
3570
|
+
for field_name, values in (
|
|
3571
|
+
("已解决问题与结论", decision_conclusions),
|
|
3572
|
+
("确认依据", decision_evidence),
|
|
3573
|
+
):
|
|
3574
|
+
if len(values) != 1:
|
|
3575
|
+
reasons.append(
|
|
3576
|
+
"dev-spec.md decision closure must contain exactly one non-empty "
|
|
3577
|
+
f"`{field_name}` field; found {len(values)}"
|
|
3578
|
+
)
|
|
3579
|
+
elif UNRESOLVED_DECISION_VALUE_PATTERN.fullmatch(
|
|
3580
|
+
re.sub(r"[`*_]", "", values[0]).strip()
|
|
3581
|
+
):
|
|
3582
|
+
reasons.append(
|
|
3583
|
+
"dev-spec.md has unresolved decision closure evidence: "
|
|
3584
|
+
f"`{field_name}` is {values[0]!r}"
|
|
3585
|
+
)
|
|
3586
|
+
|
|
3049
3587
|
if not skeleton.exists():
|
|
3050
3588
|
reasons.append("dev-spec skeleton template is missing")
|
|
3051
3589
|
else:
|
|
@@ -3365,6 +3903,7 @@ def spec_task_summary(task: dict | None) -> dict | None:
|
|
|
3365
3903
|
"selected_spec_tasks": task.get("selected_spec_tasks", []),
|
|
3366
3904
|
"repositories": task.get("spec_repositories", []),
|
|
3367
3905
|
"pending_dependencies": pending_dependencies,
|
|
3906
|
+
"writeback": task.get("spec_writeback_progress"),
|
|
3368
3907
|
}
|
|
3369
3908
|
|
|
3370
3909
|
|
|
@@ -4078,15 +4617,6 @@ def create_task(
|
|
|
4078
4617
|
return {"task_id": task_id, "task": task}
|
|
4079
4618
|
|
|
4080
4619
|
|
|
4081
|
-
def ensure_path_inside_root(root: Path, path: Path, label: str) -> Path:
|
|
4082
|
-
resolved = path.resolve()
|
|
4083
|
-
try:
|
|
4084
|
-
resolved.relative_to(root.resolve())
|
|
4085
|
-
except ValueError as exc:
|
|
4086
|
-
raise StateError(f"{label} must be inside the Easy Coding project root.") from exc
|
|
4087
|
-
return resolved
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
4620
|
def create_task_from_spec(
|
|
4091
4621
|
root: Path,
|
|
4092
4622
|
spec_path: str,
|
|
@@ -4100,12 +4630,14 @@ def create_task_from_spec(
|
|
|
4100
4630
|
set_current: bool = True,
|
|
4101
4631
|
session_file: str | Path | None = None,
|
|
4102
4632
|
) -> dict:
|
|
4103
|
-
raw_spec_path = Path(spec_path)
|
|
4104
|
-
resolved_spec_path =
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4633
|
+
raw_spec_path = Path(spec_path).expanduser()
|
|
4634
|
+
resolved_spec_path = (
|
|
4635
|
+
raw_spec_path.resolve()
|
|
4636
|
+
if raw_spec_path.is_absolute()
|
|
4637
|
+
else (root / raw_spec_path).resolve()
|
|
4108
4638
|
)
|
|
4639
|
+
if not resolved_spec_path.is_file():
|
|
4640
|
+
raise StateError("Canonical Spec path must be an explicitly selected UTF-8 file.")
|
|
4109
4641
|
try:
|
|
4110
4642
|
inspection = inspect_spec(
|
|
4111
4643
|
resolved_spec_path,
|
|
@@ -4129,7 +4661,16 @@ def create_task_from_spec(
|
|
|
4129
4661
|
str(binding["repo_id"]): str(binding["path"])
|
|
4130
4662
|
for binding in bindings
|
|
4131
4663
|
}
|
|
4132
|
-
|
|
4664
|
+
if not isinstance(inspection.get("execution"), dict):
|
|
4665
|
+
raise StateError(
|
|
4666
|
+
"Canonical Spec shared execution is not initialized; run initialize-spec-execution first."
|
|
4667
|
+
)
|
|
4668
|
+
try:
|
|
4669
|
+
source_path = resolved_spec_path.relative_to(root.resolve()).as_posix()
|
|
4670
|
+
path_mode = "project-relative"
|
|
4671
|
+
except ValueError:
|
|
4672
|
+
source_path = str(resolved_spec_path)
|
|
4673
|
+
path_mode = "absolute"
|
|
4133
4674
|
fields = {
|
|
4134
4675
|
"repos": list(selection["selected_repo_ids"]),
|
|
4135
4676
|
"repo_paths": stored_repo_paths,
|
|
@@ -4138,11 +4679,19 @@ def create_task_from_spec(
|
|
|
4138
4679
|
"spec_id": inspection["spec_id"],
|
|
4139
4680
|
"revision": inspection["revision"],
|
|
4140
4681
|
"path": source_path,
|
|
4141
|
-
"
|
|
4682
|
+
"path_mode": path_mode,
|
|
4683
|
+
"design_sha256": inspection["design_sha256"],
|
|
4684
|
+
"document_sha256": inspection["document_sha256"],
|
|
4685
|
+
"execution_revision": inspection["execution_revision"],
|
|
4142
4686
|
},
|
|
4143
4687
|
"selected_spec_tasks": selection["selected_task_ids"],
|
|
4144
4688
|
"spec_repositories": bindings,
|
|
4145
4689
|
"spec_dependency_evidence": selection["dependency_records"],
|
|
4690
|
+
"spec_writeback_progress": {
|
|
4691
|
+
"last_execution_revision": inspection["execution_revision"],
|
|
4692
|
+
"status": "ok",
|
|
4693
|
+
"updated_at": now_iso(),
|
|
4694
|
+
},
|
|
4146
4695
|
}
|
|
4147
4696
|
return create_task(
|
|
4148
4697
|
root,
|
|
@@ -4156,69 +4705,1312 @@ def create_task_from_spec(
|
|
|
4156
4705
|
)
|
|
4157
4706
|
|
|
4158
4707
|
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4708
|
+
SPEC_WRITEBACK_APP = "easy-coding"
|
|
4709
|
+
|
|
4710
|
+
|
|
4711
|
+
def spec_writeback_agent(agent: str) -> str:
|
|
4712
|
+
raw_agent = str(agent).strip()
|
|
4713
|
+
if raw_agent.endswith(" with Easy Coding"):
|
|
4714
|
+
return raw_agent
|
|
4715
|
+
normalized = normalize_agent_identity(raw_agent)
|
|
4716
|
+
display_name = {
|
|
4717
|
+
"claude-code": "Claude Code",
|
|
4718
|
+
"codex": "Codex",
|
|
4719
|
+
"qoder": "Qoder",
|
|
4720
|
+
"unknown": "Unknown Agent",
|
|
4721
|
+
}.get(normalized, normalized)
|
|
4722
|
+
return f"{display_name} with Easy Coding"
|
|
4723
|
+
|
|
4724
|
+
|
|
4725
|
+
def initialize_spec_execution_state(root: Path, spec_path: str) -> dict:
|
|
4726
|
+
raw_path = Path(spec_path).expanduser()
|
|
4727
|
+
resolved = raw_path.resolve() if raw_path.is_absolute() else (root / raw_path).resolve()
|
|
4728
|
+
if not resolved.is_file():
|
|
4729
|
+
raise StateError("Canonical Spec path must identify an explicit UTF-8 file.")
|
|
4730
|
+
try:
|
|
4731
|
+
execution = initialize_execution(resolved)
|
|
4732
|
+
details = show_execution(resolved)
|
|
4733
|
+
except (ExecutionStateError, ExecutionConflictError) as exc:
|
|
4734
|
+
raise StateError(f"Cannot initialize Canonical Spec execution: {exc}") from exc
|
|
4735
|
+
return {
|
|
4736
|
+
"action": "initialize-spec-execution",
|
|
4737
|
+
"spec": str(resolved),
|
|
4738
|
+
"design_sha256": details["design_sha256"],
|
|
4739
|
+
"document_sha256": details["document_sha256"],
|
|
4740
|
+
"execution_revision": execution["execution_revision"],
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
|
|
4744
|
+
def _spec_event(execution: dict, idempotency_key: str) -> dict:
|
|
4177
4745
|
matches = [
|
|
4178
|
-
|
|
4179
|
-
for
|
|
4180
|
-
if isinstance(
|
|
4181
|
-
and record.get("task_id") == dependency_task_id
|
|
4182
|
-
and (source_task_id is None or record.get("source_task_id") == source_task_id)
|
|
4746
|
+
event
|
|
4747
|
+
for event in execution.get("events", [])
|
|
4748
|
+
if isinstance(event, dict) and event.get("idempotency_key") == idempotency_key
|
|
4183
4749
|
]
|
|
4184
|
-
if
|
|
4185
|
-
raise StateError("
|
|
4186
|
-
|
|
4187
|
-
raise StateError(
|
|
4188
|
-
"Canonical Spec dependency is ambiguous; pass --source-task to identify the edge."
|
|
4189
|
-
)
|
|
4190
|
-
record = matches[0]
|
|
4191
|
-
if record.get("dependency_type") == "contract":
|
|
4192
|
-
raise StateError("Contract dependencies are satisfied by the frozen READY Spec.")
|
|
4193
|
-
record["status"] = "satisfied"
|
|
4194
|
-
record["evidence"] = evidence.strip()
|
|
4195
|
-
record["satisfied_at"] = now_iso()
|
|
4196
|
-
record["satisfied_by"] = agent
|
|
4197
|
-
task["last_agent"] = agent
|
|
4198
|
-
write_task(root, resolved_task_id, task)
|
|
4199
|
-
snapshot = snapshot_state(root, session_file, session)
|
|
4200
|
-
snapshot["action"] = "satisfy-spec-dependency"
|
|
4201
|
-
return snapshot
|
|
4750
|
+
if len(matches) != 1:
|
|
4751
|
+
raise StateError("Shared Spec writeback did not expose one matching idempotent event.")
|
|
4752
|
+
return matches[0]
|
|
4202
4753
|
|
|
4203
4754
|
|
|
4204
|
-
def
|
|
4205
|
-
|
|
4206
|
-
|
|
4755
|
+
def _writeback_progress(task: dict) -> dict:
|
|
4756
|
+
progress = task.get("spec_writeback_progress")
|
|
4757
|
+
if not isinstance(progress, dict):
|
|
4758
|
+
progress = {}
|
|
4759
|
+
task["spec_writeback_progress"] = progress
|
|
4760
|
+
return progress
|
|
4207
4761
|
|
|
4208
4762
|
|
|
4209
|
-
def
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4763
|
+
def _is_idempotency_key_conflict(exc: ExecutionConflictError) -> bool:
|
|
4764
|
+
return str(exc).startswith("幂等键已被不同事件使用")
|
|
4765
|
+
|
|
4766
|
+
|
|
4767
|
+
def _execute_spec_writeback(
|
|
4768
|
+
root: Path,
|
|
4769
|
+
harness_task_id: str,
|
|
4770
|
+
task: dict,
|
|
4771
|
+
action: dict,
|
|
4772
|
+
idempotency_key: str,
|
|
4773
|
+
invoke,
|
|
4774
|
+
) -> dict:
|
|
4775
|
+
inspection, _ = inspect_task_spec(root, task)
|
|
4776
|
+
source = task["spec_source"]
|
|
4777
|
+
progress = _writeback_progress(task)
|
|
4778
|
+
serialized_action = json.dumps(action, ensure_ascii=False, sort_keys=True)
|
|
4779
|
+
existing_pending = progress.get("pending_action")
|
|
4780
|
+
if isinstance(existing_pending, str) and existing_pending.strip():
|
|
4781
|
+
try:
|
|
4782
|
+
existing_action = json.loads(existing_pending)
|
|
4783
|
+
except json.JSONDecodeError as exc:
|
|
4784
|
+
raise StateError("Pending Canonical Spec writeback metadata is invalid JSON.") from exc
|
|
4785
|
+
if existing_action != action:
|
|
4786
|
+
raise StateError(
|
|
4787
|
+
"A different Canonical Spec writeback is pending; run "
|
|
4788
|
+
"reconcile-spec-execution before starting another action."
|
|
4789
|
+
)
|
|
4790
|
+
progress.update(
|
|
4791
|
+
{
|
|
4792
|
+
"last_execution_revision": source["execution_revision"],
|
|
4793
|
+
"pending_action": serialized_action,
|
|
4794
|
+
"status": "pending",
|
|
4795
|
+
"updated_at": now_iso(),
|
|
4796
|
+
}
|
|
4797
|
+
)
|
|
4798
|
+
write_task(root, harness_task_id, task)
|
|
4799
|
+
|
|
4800
|
+
def call_writer(current_inspection: dict) -> dict:
|
|
4801
|
+
return invoke(
|
|
4802
|
+
str(current_inspection["design_sha256"]),
|
|
4803
|
+
int(current_inspection["execution_revision"]),
|
|
4804
|
+
)
|
|
4805
|
+
|
|
4806
|
+
try:
|
|
4807
|
+
execution = call_writer(inspection)
|
|
4808
|
+
except ExecutionConflictError:
|
|
4809
|
+
try:
|
|
4810
|
+
refreshed = inspect_spec(
|
|
4811
|
+
stored_spec_path(root, task),
|
|
4812
|
+
root,
|
|
4813
|
+
task.get("repo_paths") if isinstance(task.get("repo_paths"), dict) else {},
|
|
4814
|
+
task.get("selected_spec_tasks") or [],
|
|
4815
|
+
)
|
|
4816
|
+
except EasyDevSpecError as exc:
|
|
4817
|
+
progress["status"] = "error"
|
|
4818
|
+
progress["updated_at"] = now_iso()
|
|
4819
|
+
progress.pop("pending_action", None)
|
|
4820
|
+
write_task(root, harness_task_id, task)
|
|
4821
|
+
raise StateError(f"Cannot refresh Canonical Spec after CAS conflict: {exc}") from exc
|
|
4822
|
+
if refreshed.get("design_sha256") != source.get("design_sha256"):
|
|
4823
|
+
progress["status"] = "error"
|
|
4824
|
+
progress["updated_at"] = now_iso()
|
|
4825
|
+
progress.pop("pending_action", None)
|
|
4826
|
+
write_task(root, harness_task_id, task)
|
|
4827
|
+
raise StateError("Canonical Spec design changed during writeback; return to ANALYSIS.")
|
|
4828
|
+
if int(refreshed.get("execution_revision", -1)) < int(source["execution_revision"]):
|
|
4829
|
+
progress["status"] = "conflict"
|
|
4830
|
+
progress["updated_at"] = now_iso()
|
|
4831
|
+
write_task(root, harness_task_id, task)
|
|
4832
|
+
raise StateError("Canonical Spec execution revision moved backwards during writeback.")
|
|
4833
|
+
try:
|
|
4834
|
+
execution = call_writer(refreshed)
|
|
4835
|
+
except (ExecutionStateError, ExecutionConflictError) as exc:
|
|
4836
|
+
terminal_conflict = isinstance(
|
|
4837
|
+
exc, ExecutionConflictError
|
|
4838
|
+
) and _is_idempotency_key_conflict(exc)
|
|
4839
|
+
progress["status"] = "error" if terminal_conflict else "conflict"
|
|
4840
|
+
progress["updated_at"] = now_iso()
|
|
4841
|
+
if terminal_conflict:
|
|
4842
|
+
progress.pop("pending_action", None)
|
|
4843
|
+
write_task(root, harness_task_id, task)
|
|
4844
|
+
raise StateError(f"Canonical Spec CAS retry failed: {exc}") from exc
|
|
4845
|
+
except ExecutionStateError as exc:
|
|
4846
|
+
progress["status"] = "error"
|
|
4847
|
+
progress["updated_at"] = now_iso()
|
|
4848
|
+
progress.pop("pending_action", None)
|
|
4849
|
+
write_task(root, harness_task_id, task)
|
|
4850
|
+
raise StateError(f"Canonical Spec writeback failed: {exc}") from exc
|
|
4851
|
+
|
|
4852
|
+
event = _spec_event(execution, idempotency_key)
|
|
4853
|
+
try:
|
|
4854
|
+
details = show_execution(stored_spec_path(root, task))
|
|
4855
|
+
except ExecutionStateError as exc:
|
|
4856
|
+
raise StateError(f"Canonical Spec writeback cannot be verified: {exc}") from exc
|
|
4857
|
+
source.update(
|
|
4858
|
+
{
|
|
4859
|
+
"revision": details["design_revision"],
|
|
4860
|
+
"design_sha256": details["design_sha256"],
|
|
4861
|
+
"document_sha256": details["document_sha256"],
|
|
4862
|
+
"execution_revision": execution["execution_revision"],
|
|
4863
|
+
}
|
|
4864
|
+
)
|
|
4865
|
+
inspect_task_spec(root, task)
|
|
4866
|
+
progress.update(
|
|
4867
|
+
{
|
|
4868
|
+
"last_execution_revision": execution["execution_revision"],
|
|
4869
|
+
"last_event_id": event["event_id"],
|
|
4870
|
+
"last_idempotency_key": idempotency_key,
|
|
4871
|
+
"status": "ok",
|
|
4872
|
+
"updated_at": now_iso(),
|
|
4873
|
+
}
|
|
4874
|
+
)
|
|
4875
|
+
progress.pop("pending_action", None)
|
|
4876
|
+
acknowledgment = {
|
|
4877
|
+
"type": "spec-writeback",
|
|
4878
|
+
"action": action,
|
|
4879
|
+
"event_id": event["event_id"],
|
|
4880
|
+
"execution_revision": execution["execution_revision"],
|
|
4881
|
+
"idempotency_key": idempotency_key,
|
|
4882
|
+
"timestamp": now_iso(),
|
|
4883
|
+
}
|
|
4884
|
+
already_acknowledged = any(
|
|
4885
|
+
record.get("type") == "spec-writeback"
|
|
4886
|
+
and record.get("idempotency_key") == idempotency_key
|
|
4887
|
+
for record in execution_records(root, harness_task_id)
|
|
4888
|
+
)
|
|
4889
|
+
if not already_acknowledged:
|
|
4890
|
+
append_execution_record(root, harness_task_id, acknowledgment)
|
|
4891
|
+
write_task(root, harness_task_id, task)
|
|
4892
|
+
return acknowledgment
|
|
4893
|
+
|
|
4894
|
+
|
|
4895
|
+
def writeback_spec_task(
|
|
4896
|
+
root: Path,
|
|
4897
|
+
source_task_id: str,
|
|
4898
|
+
status_value: str,
|
|
4899
|
+
summary: str,
|
|
4900
|
+
evidence: list[dict],
|
|
4901
|
+
idempotency_key: str,
|
|
4902
|
+
agent: str,
|
|
4903
|
+
task_id: str | None = None,
|
|
4904
|
+
session_file: str | Path | None = None,
|
|
4905
|
+
) -> dict:
|
|
4906
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
4907
|
+
if source_task_id not in set(task.get("selected_spec_tasks") or []):
|
|
4908
|
+
raise StateError("Canonical source task is outside the Harness task selection.")
|
|
4909
|
+
action = {
|
|
4910
|
+
"kind": "task",
|
|
4911
|
+
"source_task_id": source_task_id,
|
|
4912
|
+
"status": status_value,
|
|
4913
|
+
"summary": summary,
|
|
4914
|
+
"evidence": evidence,
|
|
4915
|
+
"idempotency_key": idempotency_key,
|
|
4916
|
+
"agent": agent,
|
|
4917
|
+
}
|
|
4918
|
+
acknowledgment = _execute_spec_writeback(
|
|
4919
|
+
root,
|
|
4920
|
+
resolved_task_id,
|
|
4921
|
+
task,
|
|
4922
|
+
action,
|
|
4923
|
+
idempotency_key,
|
|
4924
|
+
lambda design_digest, execution_revision: record_task_status(
|
|
4925
|
+
stored_spec_path(root, task),
|
|
4926
|
+
source_task_id,
|
|
4927
|
+
status_value,
|
|
4928
|
+
summary,
|
|
4929
|
+
SPEC_WRITEBACK_APP,
|
|
4930
|
+
spec_writeback_agent(agent),
|
|
4931
|
+
design_digest,
|
|
4932
|
+
execution_revision,
|
|
4933
|
+
evidence=evidence,
|
|
4934
|
+
run_id=resolved_task_id,
|
|
4935
|
+
idempotency_key=idempotency_key,
|
|
4936
|
+
),
|
|
4937
|
+
)
|
|
4938
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
4939
|
+
snapshot["spec_writeback"] = acknowledgment
|
|
4940
|
+
snapshot["action"] = "writeback-spec-task"
|
|
4941
|
+
return snapshot
|
|
4942
|
+
|
|
4943
|
+
|
|
4944
|
+
def writeback_spec_step(
|
|
4945
|
+
root: Path,
|
|
4946
|
+
source_task_id: str,
|
|
4947
|
+
step_id: str,
|
|
4948
|
+
status_value: str,
|
|
4949
|
+
summary: str,
|
|
4950
|
+
evidence: list[dict],
|
|
4951
|
+
idempotency_key: str,
|
|
4952
|
+
agent: str,
|
|
4953
|
+
task_id: str | None = None,
|
|
4954
|
+
session_file: str | Path | None = None,
|
|
4955
|
+
) -> dict:
|
|
4956
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
4957
|
+
if source_task_id not in set(task.get("selected_spec_tasks") or []):
|
|
4958
|
+
raise StateError("Canonical source task is outside the Harness task selection.")
|
|
4959
|
+
action = {
|
|
4960
|
+
"kind": "step",
|
|
4961
|
+
"source_task_id": source_task_id,
|
|
4962
|
+
"step_id": step_id,
|
|
4963
|
+
"status": status_value,
|
|
4964
|
+
"summary": summary,
|
|
4965
|
+
"evidence": evidence,
|
|
4966
|
+
"idempotency_key": idempotency_key,
|
|
4967
|
+
"agent": agent,
|
|
4968
|
+
}
|
|
4969
|
+
acknowledgment = _execute_spec_writeback(
|
|
4970
|
+
root,
|
|
4971
|
+
resolved_task_id,
|
|
4972
|
+
task,
|
|
4973
|
+
action,
|
|
4974
|
+
idempotency_key,
|
|
4975
|
+
lambda design_digest, execution_revision: record_step_status(
|
|
4976
|
+
stored_spec_path(root, task),
|
|
4977
|
+
source_task_id,
|
|
4978
|
+
step_id,
|
|
4979
|
+
status_value,
|
|
4980
|
+
summary,
|
|
4981
|
+
SPEC_WRITEBACK_APP,
|
|
4982
|
+
spec_writeback_agent(agent),
|
|
4983
|
+
design_digest,
|
|
4984
|
+
execution_revision,
|
|
4985
|
+
evidence=evidence,
|
|
4986
|
+
run_id=resolved_task_id,
|
|
4987
|
+
idempotency_key=idempotency_key,
|
|
4988
|
+
),
|
|
4989
|
+
)
|
|
4990
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
4991
|
+
snapshot["spec_writeback"] = acknowledgment
|
|
4992
|
+
snapshot["action"] = "writeback-spec-step"
|
|
4993
|
+
return snapshot
|
|
4994
|
+
|
|
4995
|
+
|
|
4996
|
+
def writeback_spec_dependency(
|
|
4997
|
+
root: Path,
|
|
4998
|
+
source_task_id: str,
|
|
4999
|
+
dependency_task_id: str,
|
|
5000
|
+
status_value: str,
|
|
5001
|
+
summary: str,
|
|
5002
|
+
evidence: list[dict],
|
|
5003
|
+
idempotency_key: str,
|
|
5004
|
+
agent: str,
|
|
5005
|
+
task_id: str | None = None,
|
|
5006
|
+
session_file: str | Path | None = None,
|
|
5007
|
+
) -> dict:
|
|
5008
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
5009
|
+
if source_task_id not in set(task.get("selected_spec_tasks") or []):
|
|
5010
|
+
raise StateError("Canonical source task is outside the Harness task selection.")
|
|
5011
|
+
action = {
|
|
5012
|
+
"kind": "dependency",
|
|
5013
|
+
"source_task_id": source_task_id,
|
|
5014
|
+
"dependency_task_id": dependency_task_id,
|
|
5015
|
+
"status": status_value,
|
|
5016
|
+
"summary": summary,
|
|
5017
|
+
"evidence": evidence,
|
|
5018
|
+
"idempotency_key": idempotency_key,
|
|
5019
|
+
"agent": agent,
|
|
5020
|
+
}
|
|
5021
|
+
acknowledgment = _execute_spec_writeback(
|
|
5022
|
+
root,
|
|
5023
|
+
resolved_task_id,
|
|
5024
|
+
task,
|
|
5025
|
+
action,
|
|
5026
|
+
idempotency_key,
|
|
5027
|
+
lambda design_digest, execution_revision: record_dependency_status(
|
|
5028
|
+
stored_spec_path(root, task),
|
|
5029
|
+
source_task_id,
|
|
5030
|
+
dependency_task_id,
|
|
5031
|
+
status_value,
|
|
5032
|
+
summary,
|
|
5033
|
+
SPEC_WRITEBACK_APP,
|
|
5034
|
+
spec_writeback_agent(agent),
|
|
5035
|
+
design_digest,
|
|
5036
|
+
execution_revision,
|
|
5037
|
+
evidence=evidence,
|
|
5038
|
+
run_id=resolved_task_id,
|
|
5039
|
+
idempotency_key=idempotency_key,
|
|
5040
|
+
),
|
|
5041
|
+
)
|
|
5042
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
5043
|
+
snapshot["spec_writeback"] = acknowledgment
|
|
5044
|
+
snapshot["action"] = "writeback-spec-dependency"
|
|
5045
|
+
return snapshot
|
|
5046
|
+
|
|
5047
|
+
|
|
5048
|
+
def rebind_spec_source(
|
|
5049
|
+
root: Path,
|
|
5050
|
+
spec_path: str,
|
|
5051
|
+
agent: str,
|
|
5052
|
+
task_id: str | None = None,
|
|
5053
|
+
session_file: str | Path | None = None,
|
|
5054
|
+
) -> dict:
|
|
5055
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
5056
|
+
source = task.get("spec_source")
|
|
5057
|
+
if not isinstance(source, dict):
|
|
5058
|
+
raise StateError("Current task is not backed by a Canonical Spec.")
|
|
5059
|
+
raw_path = Path(spec_path).expanduser()
|
|
5060
|
+
resolved = raw_path.resolve() if raw_path.is_absolute() else (root / raw_path).resolve()
|
|
5061
|
+
try:
|
|
5062
|
+
inspection = inspect_spec(
|
|
5063
|
+
resolved,
|
|
5064
|
+
root,
|
|
5065
|
+
task.get("repo_paths") if isinstance(task.get("repo_paths"), dict) else {},
|
|
5066
|
+
task.get("selected_spec_tasks") or [],
|
|
5067
|
+
)
|
|
5068
|
+
except EasyDevSpecError as exc:
|
|
5069
|
+
raise StateError(f"Cannot rebind Canonical Spec: {exc}") from exc
|
|
5070
|
+
for field in ("schema", "spec_id", "revision", "design_sha256"):
|
|
5071
|
+
expected = source.get(field)
|
|
5072
|
+
if field == "design_sha256" and expected is None and source.get("sha256") == inspection.get("source_sha256"):
|
|
5073
|
+
expected = inspection.get("design_sha256")
|
|
5074
|
+
if expected != inspection.get(field):
|
|
5075
|
+
raise StateError(f"Rebind rejected because Canonical Spec {field} does not match.")
|
|
5076
|
+
previous_execution_revision = source.get("execution_revision", 0)
|
|
5077
|
+
if int(inspection.get("execution_revision", -1)) < int(previous_execution_revision):
|
|
5078
|
+
raise StateError("Rebind rejected because Canonical execution revision moved backwards.")
|
|
5079
|
+
try:
|
|
5080
|
+
source_path = resolved.relative_to(root.resolve()).as_posix()
|
|
5081
|
+
path_mode = "project-relative"
|
|
5082
|
+
except ValueError:
|
|
5083
|
+
source_path = str(resolved)
|
|
5084
|
+
path_mode = "absolute"
|
|
5085
|
+
source.update({"path": source_path, "path_mode": path_mode})
|
|
5086
|
+
inspect_task_spec(root, task)
|
|
5087
|
+
task["last_agent"] = agent
|
|
5088
|
+
write_task(root, resolved_task_id, task)
|
|
5089
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
5090
|
+
snapshot["action"] = "rebind-spec-source"
|
|
5091
|
+
return snapshot
|
|
5092
|
+
|
|
5093
|
+
|
|
5094
|
+
def reconcile_local_result_evidence(
|
|
5095
|
+
root: Path,
|
|
5096
|
+
resolved_task_id: str,
|
|
5097
|
+
task: dict,
|
|
5098
|
+
agent: str,
|
|
5099
|
+
session_file: str | Path | None,
|
|
5100
|
+
) -> tuple[int, list[str]]:
|
|
5101
|
+
plan = latest_execution_plan(root, resolved_task_id)
|
|
5102
|
+
if not isinstance(plan, dict):
|
|
5103
|
+
return 0, []
|
|
5104
|
+
inspection, selection = inspect_task_spec(root, task)
|
|
5105
|
+
snapshots = _selected_execution_snapshots(inspection, task)
|
|
5106
|
+
units = {
|
|
5107
|
+
str(unit.get("id")): unit
|
|
5108
|
+
for unit in plan.get("units", [])
|
|
5109
|
+
if isinstance(unit, dict) and is_non_empty_string(unit.get("id"))
|
|
5110
|
+
}
|
|
5111
|
+
records = execution_records(root, resolved_task_id)
|
|
5112
|
+
last_plan_index = max(
|
|
5113
|
+
(index for index, record in enumerate(records) if record.get("type") == "plan"),
|
|
5114
|
+
default=-1,
|
|
5115
|
+
)
|
|
5116
|
+
lifecycle_by_unit: dict[str, list[tuple[int, dict]]] = {
|
|
5117
|
+
unit_id: [] for unit_id in units
|
|
5118
|
+
}
|
|
5119
|
+
for record_index, record in enumerate(records[last_plan_index + 1 :], last_plan_index + 1):
|
|
5120
|
+
unit_id = str(record.get("unit_id") or "")
|
|
5121
|
+
if record.get("type") in {"dispatch", "result"} and unit_id in lifecycle_by_unit:
|
|
5122
|
+
lifecycle_by_unit[unit_id].append((record_index, record))
|
|
5123
|
+
latest_results = {
|
|
5124
|
+
unit_id: lifecycle[-1]
|
|
5125
|
+
for unit_id, lifecycle in lifecycle_by_unit.items()
|
|
5126
|
+
if lifecycle and lifecycle[-1][1].get("type") == "result"
|
|
5127
|
+
}
|
|
5128
|
+
step_by_id = {
|
|
5129
|
+
str(step.get("step_id")): step
|
|
5130
|
+
for step in selection.get("selected_steps", [])
|
|
5131
|
+
if isinstance(step, dict)
|
|
5132
|
+
}
|
|
5133
|
+
test_by_id = {
|
|
5134
|
+
str(test.get("test_id")): test
|
|
5135
|
+
for test in selection.get("selected_tests", [])
|
|
5136
|
+
if isinstance(test, dict)
|
|
5137
|
+
}
|
|
5138
|
+
reconciled = 0
|
|
5139
|
+
unresolved: list[str] = []
|
|
5140
|
+
for unit_id, (result_index, result) in latest_results.items():
|
|
5141
|
+
unit = units.get(unit_id)
|
|
5142
|
+
if not unit:
|
|
5143
|
+
continue
|
|
5144
|
+
lifecycle = lifecycle_by_unit.get(unit_id, [])
|
|
5145
|
+
if len(lifecycle) < 2 or lifecycle[-2][1].get("type") != "dispatch":
|
|
5146
|
+
unresolved.append(f"{unit_id}:missing-matching-dispatch")
|
|
5147
|
+
continue
|
|
5148
|
+
dispatch_index, dispatch = lifecycle[-2]
|
|
5149
|
+
source_task_id = str(unit.get("source_task_id") or "")
|
|
5150
|
+
source_steps = [str(value) for value in unit.get("source_step_ids", [])]
|
|
5151
|
+
if source_task_id not in snapshots or not source_steps:
|
|
5152
|
+
continue
|
|
5153
|
+
if (
|
|
5154
|
+
dispatch.get("source_task_id") != source_task_id
|
|
5155
|
+
or dispatch.get("repo_id") != unit.get("repo_id")
|
|
5156
|
+
or result.get("source_task_id") != source_task_id
|
|
5157
|
+
or result.get("repo_id") != unit.get("repo_id")
|
|
5158
|
+
or not isinstance(result.get("changed_files"), list)
|
|
5159
|
+
or not set(result.get("changed_files", [])).issubset(set(unit.get("files", [])))
|
|
5160
|
+
or not is_non_empty_string(result.get("summary"))
|
|
5161
|
+
):
|
|
5162
|
+
unresolved.append(f"{unit_id}:source-ownership-mismatch")
|
|
5163
|
+
continue
|
|
5164
|
+
current_status = snapshots[source_task_id].get("status")
|
|
5165
|
+
if current_status != "in_progress":
|
|
5166
|
+
unresolved.append(
|
|
5167
|
+
f"{unit_id}:shared-task-status={current_status or 'missing'}"
|
|
5168
|
+
)
|
|
5169
|
+
continue
|
|
5170
|
+
attempt_id, attempt_completed_steps = _shared_attempt_projection(
|
|
5171
|
+
inspection, source_task_id
|
|
5172
|
+
)
|
|
5173
|
+
if not attempt_id:
|
|
5174
|
+
unresolved.append(f"{unit_id}:missing-in-progress-attempt")
|
|
5175
|
+
continue
|
|
5176
|
+
attempt_ack_index = max(
|
|
5177
|
+
(
|
|
5178
|
+
index
|
|
5179
|
+
for index, record in enumerate(records)
|
|
5180
|
+
if record.get("type") == "spec-writeback"
|
|
5181
|
+
and record.get("event_id") == attempt_id
|
|
5182
|
+
and isinstance(record.get("action"), dict)
|
|
5183
|
+
and record["action"].get("kind") == "task"
|
|
5184
|
+
and record["action"].get("source_task_id") == source_task_id
|
|
5185
|
+
and record["action"].get("status") == "in_progress"
|
|
5186
|
+
),
|
|
5187
|
+
default=-1,
|
|
5188
|
+
)
|
|
5189
|
+
if attempt_ack_index < 0:
|
|
5190
|
+
unresolved.append(f"{unit_id}:missing-in-progress-acknowledgment")
|
|
5191
|
+
continue
|
|
5192
|
+
if dispatch_index <= attempt_ack_index or result_index <= attempt_ack_index:
|
|
5193
|
+
unresolved.append(f"{unit_id}:no-result-for-current-attempt")
|
|
5194
|
+
continue
|
|
5195
|
+
result_status = result.get("status")
|
|
5196
|
+
successful = (
|
|
5197
|
+
result_status == "completed"
|
|
5198
|
+
and result.get("issues") == []
|
|
5199
|
+
and result.get("needs_attention") == []
|
|
5200
|
+
)
|
|
5201
|
+
failed = result_status == "failed"
|
|
5202
|
+
if not successful and not failed:
|
|
5203
|
+
unresolved.append(f"{unit_id}:invalid-result-status-or-issues")
|
|
5204
|
+
continue
|
|
5205
|
+
if failed:
|
|
5206
|
+
if len(source_steps) != 1:
|
|
5207
|
+
unresolved.append(f"{unit_id}:ambiguous-failed-source-step")
|
|
5208
|
+
continue
|
|
5209
|
+
step_id = source_steps[0]
|
|
5210
|
+
key = f"{resolved_task_id}:{unit_id}:{step_id}:{attempt_id}:result-failed"
|
|
5211
|
+
writeback_spec_step(
|
|
5212
|
+
root,
|
|
5213
|
+
source_task_id,
|
|
5214
|
+
step_id,
|
|
5215
|
+
"failed",
|
|
5216
|
+
str(result.get("summary") or f"Unit {unit_id} failed"),
|
|
5217
|
+
[
|
|
5218
|
+
{
|
|
5219
|
+
"kind": "result",
|
|
5220
|
+
"status": "failed",
|
|
5221
|
+
"ref": f"execution.jsonl#unit={unit_id}",
|
|
5222
|
+
}
|
|
5223
|
+
],
|
|
5224
|
+
key,
|
|
5225
|
+
agent,
|
|
5226
|
+
resolved_task_id,
|
|
5227
|
+
session_file,
|
|
5228
|
+
)
|
|
5229
|
+
reconciled += 1
|
|
5230
|
+
unresolved.extend(
|
|
5231
|
+
f"{unit_id}:{remaining_step}:blocked-after-unit-failure"
|
|
5232
|
+
for remaining_step in source_steps[1:]
|
|
5233
|
+
)
|
|
5234
|
+
task = load_task(root, resolved_task_id) or task
|
|
5235
|
+
inspection, selection = inspect_task_spec(root, task)
|
|
5236
|
+
snapshots = _selected_execution_snapshots(inspection, task)
|
|
5237
|
+
continue
|
|
5238
|
+
passed_commands = {
|
|
5239
|
+
str(check.get("command"))
|
|
5240
|
+
for check in result.get("checks", [])
|
|
5241
|
+
if isinstance(check, dict)
|
|
5242
|
+
and check.get("passed") is True
|
|
5243
|
+
and is_non_empty_string(check.get("command"))
|
|
5244
|
+
}
|
|
5245
|
+
missing_unit_commands = sorted(set(unit.get("test_commands", [])) - passed_commands)
|
|
5246
|
+
if missing_unit_commands:
|
|
5247
|
+
unresolved.append(
|
|
5248
|
+
f"{unit_id}:missing-passed-command=" + ",".join(missing_unit_commands)
|
|
5249
|
+
)
|
|
5250
|
+
continue
|
|
5251
|
+
pending_steps = list(dict.fromkeys(source_steps))
|
|
5252
|
+
while pending_steps:
|
|
5253
|
+
ready_step_id = next(
|
|
5254
|
+
(
|
|
5255
|
+
step_id
|
|
5256
|
+
for step_id in pending_steps
|
|
5257
|
+
if step_id in attempt_completed_steps
|
|
5258
|
+
or set((step_by_id.get(step_id) or {}).get("depends_on_step_ids", []))
|
|
5259
|
+
.issubset(attempt_completed_steps)
|
|
5260
|
+
),
|
|
5261
|
+
None,
|
|
5262
|
+
)
|
|
5263
|
+
if ready_step_id is None:
|
|
5264
|
+
unresolved.extend(
|
|
5265
|
+
f"{unit_id}:{step_id}:dependency-pending" for step_id in pending_steps
|
|
5266
|
+
)
|
|
5267
|
+
break
|
|
5268
|
+
step_id = ready_step_id
|
|
5269
|
+
pending_steps.remove(step_id)
|
|
5270
|
+
if step_id in attempt_completed_steps:
|
|
5271
|
+
continue
|
|
5272
|
+
step = step_by_id.get(step_id)
|
|
5273
|
+
if not step:
|
|
5274
|
+
unresolved.append(f"{unit_id}:{step_id}:missing-step")
|
|
5275
|
+
continue
|
|
5276
|
+
tests = [test_by_id.get(str(test_id)) for test_id in step.get("test_ids", [])]
|
|
5277
|
+
if any(not isinstance(test, dict) for test in tests):
|
|
5278
|
+
unresolved.append(f"{unit_id}:{step_id}:missing-test")
|
|
5279
|
+
continue
|
|
5280
|
+
missing_commands = [
|
|
5281
|
+
str(test.get("command"))
|
|
5282
|
+
for test in tests
|
|
5283
|
+
if str(test.get("command")) not in passed_commands
|
|
5284
|
+
]
|
|
5285
|
+
if missing_commands:
|
|
5286
|
+
unresolved.append(
|
|
5287
|
+
f"{unit_id}:{step_id}:missing-passed-command=" + ",".join(missing_commands)
|
|
5288
|
+
)
|
|
5289
|
+
continue
|
|
5290
|
+
evidence = [
|
|
5291
|
+
{
|
|
5292
|
+
"kind": "test",
|
|
5293
|
+
"status": "passed",
|
|
5294
|
+
"ref": f"execution.jsonl#unit={unit_id};command={test.get('command')}",
|
|
5295
|
+
"test_id": str(test.get("test_id")),
|
|
5296
|
+
}
|
|
5297
|
+
for test in tests
|
|
5298
|
+
]
|
|
5299
|
+
key = f"{resolved_task_id}:{unit_id}:{step_id}:{attempt_id}:result-completed"
|
|
5300
|
+
writeback_spec_step(
|
|
5301
|
+
root,
|
|
5302
|
+
source_task_id,
|
|
5303
|
+
step_id,
|
|
5304
|
+
"completed",
|
|
5305
|
+
str(result.get("summary") or f"Unit {unit_id} completed"),
|
|
5306
|
+
evidence,
|
|
5307
|
+
key,
|
|
5308
|
+
agent,
|
|
5309
|
+
resolved_task_id,
|
|
5310
|
+
session_file,
|
|
5311
|
+
)
|
|
5312
|
+
reconciled += 1
|
|
5313
|
+
task = load_task(root, resolved_task_id) or task
|
|
5314
|
+
inspection, selection = inspect_task_spec(root, task)
|
|
5315
|
+
snapshots = _selected_execution_snapshots(inspection, task)
|
|
5316
|
+
_, attempt_completed_steps = _shared_attempt_projection(
|
|
5317
|
+
inspection, source_task_id
|
|
5318
|
+
)
|
|
5319
|
+
task = load_task(root, resolved_task_id) or task
|
|
5320
|
+
inspection, _ = inspect_task_spec(root, task)
|
|
5321
|
+
snapshots = _selected_execution_snapshots(inspection, task)
|
|
5322
|
+
selected_tasks = {
|
|
5323
|
+
str(item.get("task_id")): item
|
|
5324
|
+
for item in selection.get("selected_tasks", [])
|
|
5325
|
+
if isinstance(item, dict)
|
|
5326
|
+
}
|
|
5327
|
+
for source_task_id, snapshot in snapshots.items():
|
|
5328
|
+
if snapshot.get("status") != "in_progress":
|
|
5329
|
+
continue
|
|
5330
|
+
expected_steps = set(selected_tasks.get(source_task_id, {}).get("step_ids", []))
|
|
5331
|
+
attempt_id, attempt_completed_steps = _shared_attempt_projection(
|
|
5332
|
+
inspection, source_task_id
|
|
5333
|
+
)
|
|
5334
|
+
if attempt_id and expected_steps and attempt_completed_steps == expected_steps:
|
|
5335
|
+
key = (
|
|
5336
|
+
f"{resolved_task_id}:{source_task_id}:{attempt_id}:"
|
|
5337
|
+
"implemented-from-results"
|
|
5338
|
+
)
|
|
5339
|
+
writeback_spec_task(
|
|
5340
|
+
root,
|
|
5341
|
+
source_task_id,
|
|
5342
|
+
"implemented",
|
|
5343
|
+
"All Canonical Steps have passed local implementation evidence",
|
|
5344
|
+
[],
|
|
5345
|
+
key,
|
|
5346
|
+
agent,
|
|
5347
|
+
resolved_task_id,
|
|
5348
|
+
session_file,
|
|
5349
|
+
)
|
|
5350
|
+
reconciled += 1
|
|
5351
|
+
return reconciled, unresolved
|
|
5352
|
+
|
|
5353
|
+
|
|
5354
|
+
def reconcile_spec_execution(
|
|
5355
|
+
root: Path,
|
|
5356
|
+
agent: str,
|
|
5357
|
+
task_id: str | None = None,
|
|
5358
|
+
session_file: str | Path | None = None,
|
|
5359
|
+
) -> dict:
|
|
5360
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
5361
|
+
progress = _writeback_progress(task)
|
|
5362
|
+
pending = progress.get("pending_action")
|
|
5363
|
+
if not isinstance(pending, str) or not pending.strip():
|
|
5364
|
+
reconciled, unresolved = reconcile_local_result_evidence(
|
|
5365
|
+
root,
|
|
5366
|
+
resolved_task_id,
|
|
5367
|
+
task,
|
|
5368
|
+
agent,
|
|
5369
|
+
session_file,
|
|
5370
|
+
)
|
|
5371
|
+
task = load_task(root, resolved_task_id) or task
|
|
5372
|
+
inspect_task_spec(root, task)
|
|
5373
|
+
progress.update(
|
|
5374
|
+
{
|
|
5375
|
+
"last_execution_revision": task["spec_source"]["execution_revision"],
|
|
5376
|
+
"status": "ok",
|
|
5377
|
+
"updated_at": now_iso(),
|
|
5378
|
+
}
|
|
5379
|
+
)
|
|
5380
|
+
write_task(root, resolved_task_id, task)
|
|
5381
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
5382
|
+
snapshot["action"] = "reconcile-spec-execution"
|
|
5383
|
+
snapshot["reconciled"] = reconciled > 0
|
|
5384
|
+
snapshot["reconciled_actions"] = reconciled
|
|
5385
|
+
snapshot["unresolved_local_evidence"] = unresolved
|
|
5386
|
+
return snapshot
|
|
5387
|
+
try:
|
|
5388
|
+
action = json.loads(pending)
|
|
5389
|
+
except json.JSONDecodeError as exc:
|
|
5390
|
+
raise StateError("Pending Canonical Spec writeback metadata is invalid JSON.") from exc
|
|
5391
|
+
kind = action.get("kind")
|
|
5392
|
+
if kind == "sync-design":
|
|
5393
|
+
affected_task_ids = action.get("affected_task_ids")
|
|
5394
|
+
if not is_string_list(affected_task_ids):
|
|
5395
|
+
raise StateError("Pending Canonical Spec design sync has invalid affected tasks.")
|
|
5396
|
+
result = sync_spec_design_state(
|
|
5397
|
+
root,
|
|
5398
|
+
affected_task_ids,
|
|
5399
|
+
str(action.get("summary") or "Reconciled Canonical Spec design sync"),
|
|
5400
|
+
str(action.get("idempotency_key") or ""),
|
|
5401
|
+
str(action.get("agent") or agent),
|
|
5402
|
+
resolved_task_id,
|
|
5403
|
+
session_file,
|
|
5404
|
+
)
|
|
5405
|
+
result["action"] = "reconcile-spec-execution"
|
|
5406
|
+
result["reconciled"] = True
|
|
5407
|
+
return result
|
|
5408
|
+
try:
|
|
5409
|
+
design_text, _ = split_execution_region(
|
|
5410
|
+
stored_spec_path(root, task).read_text(encoding="utf-8")
|
|
5411
|
+
)
|
|
5412
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
5413
|
+
raise StateError(f"Cannot inspect pending Canonical Spec writeback: {exc}") from exc
|
|
5414
|
+
current_design_sha256 = hashlib.sha256(design_text.encode("utf-8")).hexdigest()
|
|
5415
|
+
source = task.get("spec_source")
|
|
5416
|
+
if not isinstance(source, dict):
|
|
5417
|
+
raise StateError("Current task is not backed by a Canonical Spec.")
|
|
5418
|
+
if current_design_sha256 != source.get("design_sha256"):
|
|
5419
|
+
# 旧设计上的进度事件不能重放到新设计;清除单槽 pending,允许后续 sync-design。
|
|
5420
|
+
progress["status"] = "error"
|
|
5421
|
+
progress["updated_at"] = now_iso()
|
|
5422
|
+
progress.pop("pending_action", None)
|
|
5423
|
+
write_task(root, resolved_task_id, task)
|
|
5424
|
+
raise StateError(
|
|
5425
|
+
"Pending Canonical Spec writeback belongs to an obsolete design and was "
|
|
5426
|
+
"discarded; return to ANALYSIS and run sync-spec-design."
|
|
5427
|
+
)
|
|
5428
|
+
common = {
|
|
5429
|
+
"root": root,
|
|
5430
|
+
"summary": str(action.get("summary") or "Reconciled shared Spec writeback"),
|
|
5431
|
+
"evidence": action.get("evidence") if isinstance(action.get("evidence"), list) else [],
|
|
5432
|
+
"idempotency_key": str(action.get("idempotency_key") or ""),
|
|
5433
|
+
"agent": str(action.get("agent") or agent),
|
|
5434
|
+
"task_id": resolved_task_id,
|
|
5435
|
+
"session_file": session_file,
|
|
5436
|
+
}
|
|
5437
|
+
if not common["idempotency_key"]:
|
|
5438
|
+
raise StateError("Pending Canonical Spec writeback has no idempotency key.")
|
|
5439
|
+
if kind == "task":
|
|
5440
|
+
result = writeback_spec_task(
|
|
5441
|
+
source_task_id=str(action.get("source_task_id") or ""),
|
|
5442
|
+
status_value=str(action.get("status") or ""),
|
|
5443
|
+
**common,
|
|
5444
|
+
)
|
|
5445
|
+
elif kind == "step":
|
|
5446
|
+
result = writeback_spec_step(
|
|
5447
|
+
source_task_id=str(action.get("source_task_id") or ""),
|
|
5448
|
+
step_id=str(action.get("step_id") or ""),
|
|
5449
|
+
status_value=str(action.get("status") or ""),
|
|
5450
|
+
**common,
|
|
5451
|
+
)
|
|
5452
|
+
elif kind == "dependency":
|
|
5453
|
+
result = writeback_spec_dependency(
|
|
5454
|
+
source_task_id=str(action.get("source_task_id") or ""),
|
|
5455
|
+
dependency_task_id=str(action.get("dependency_task_id") or ""),
|
|
5456
|
+
status_value=str(action.get("status") or ""),
|
|
5457
|
+
**common,
|
|
5458
|
+
)
|
|
5459
|
+
else:
|
|
5460
|
+
raise StateError("Pending Canonical Spec writeback kind is unsupported.")
|
|
5461
|
+
result["action"] = "reconcile-spec-execution"
|
|
5462
|
+
result["reconciled"] = True
|
|
5463
|
+
return result
|
|
5464
|
+
|
|
5465
|
+
|
|
5466
|
+
def sync_spec_design_state(
|
|
5467
|
+
root: Path,
|
|
5468
|
+
affected_task_ids: list[str],
|
|
5469
|
+
summary: str,
|
|
5470
|
+
idempotency_key: str,
|
|
5471
|
+
agent: str,
|
|
5472
|
+
task_id: str | None = None,
|
|
5473
|
+
session_file: str | Path | None = None,
|
|
5474
|
+
) -> dict:
|
|
5475
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
5476
|
+
source = task.get("spec_source")
|
|
5477
|
+
if not isinstance(source, dict):
|
|
5478
|
+
raise StateError("Current task is not backed by a Canonical Spec.")
|
|
5479
|
+
spec_path = stored_spec_path(root, task)
|
|
5480
|
+
requested_task_ids = sorted(set(affected_task_ids))
|
|
5481
|
+
|
|
5482
|
+
def current_execution_envelope() -> dict:
|
|
5483
|
+
try:
|
|
5484
|
+
from easy_dev_spec_protocol import split_execution_region
|
|
5485
|
+
|
|
5486
|
+
_, execution = split_execution_region(spec_path.read_text(encoding="utf-8"))
|
|
5487
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
5488
|
+
raise StateError(f"Cannot inspect pre-sync Canonical execution state: {exc}") from exc
|
|
5489
|
+
if not isinstance(execution, dict):
|
|
5490
|
+
raise StateError("Canonical Spec shared execution is missing before sync-design.")
|
|
5491
|
+
if execution.get("design_sha256") != source.get("design_sha256"):
|
|
5492
|
+
matching_events = [
|
|
5493
|
+
event
|
|
5494
|
+
for event in execution.get("events", [])
|
|
5495
|
+
if isinstance(event, dict)
|
|
5496
|
+
and event.get("type") == "spec_revised"
|
|
5497
|
+
and event.get("idempotency_key") == idempotency_key
|
|
5498
|
+
and event.get("requested_task_ids") == requested_task_ids
|
|
5499
|
+
and event.get("run_id") == resolved_task_id
|
|
5500
|
+
]
|
|
5501
|
+
if len(matching_events) != 1:
|
|
5502
|
+
raise StateError(
|
|
5503
|
+
"Canonical Spec execution baseline no longer matches the bound design."
|
|
5504
|
+
)
|
|
5505
|
+
return execution
|
|
5506
|
+
|
|
5507
|
+
current_revision = int(current_execution_envelope().get("execution_revision", -1))
|
|
5508
|
+
progress = _writeback_progress(task)
|
|
5509
|
+
pending_action = {
|
|
5510
|
+
"kind": "sync-design",
|
|
5511
|
+
"affected_task_ids": requested_task_ids,
|
|
5512
|
+
"summary": summary,
|
|
5513
|
+
"idempotency_key": idempotency_key,
|
|
5514
|
+
"agent": agent,
|
|
5515
|
+
}
|
|
5516
|
+
serialized_pending_action = json.dumps(
|
|
5517
|
+
pending_action, ensure_ascii=False, sort_keys=True
|
|
5518
|
+
)
|
|
5519
|
+
existing_pending = progress.get("pending_action")
|
|
5520
|
+
if isinstance(existing_pending, str) and existing_pending.strip():
|
|
5521
|
+
try:
|
|
5522
|
+
existing_action = json.loads(existing_pending)
|
|
5523
|
+
except json.JSONDecodeError as exc:
|
|
5524
|
+
raise StateError("Pending Canonical Spec writeback metadata is invalid JSON.") from exc
|
|
5525
|
+
if existing_action != pending_action:
|
|
5526
|
+
raise StateError(
|
|
5527
|
+
"A different Canonical Spec writeback is pending; run "
|
|
5528
|
+
"reconcile-spec-execution before sync-design."
|
|
5529
|
+
)
|
|
5530
|
+
progress.update(
|
|
5531
|
+
{
|
|
5532
|
+
"last_execution_revision": current_revision,
|
|
5533
|
+
"pending_action": serialized_pending_action,
|
|
5534
|
+
"status": "pending",
|
|
5535
|
+
"updated_at": now_iso(),
|
|
5536
|
+
}
|
|
5537
|
+
)
|
|
5538
|
+
write_task(root, resolved_task_id, task)
|
|
5539
|
+
|
|
5540
|
+
def invoke_sync(execution_revision: int) -> dict:
|
|
5541
|
+
return sync_design(
|
|
5542
|
+
spec_path,
|
|
5543
|
+
requested_task_ids,
|
|
5544
|
+
summary,
|
|
5545
|
+
SPEC_WRITEBACK_APP,
|
|
5546
|
+
spec_writeback_agent(agent),
|
|
5547
|
+
str(source.get("design_sha256")),
|
|
5548
|
+
execution_revision,
|
|
5549
|
+
run_id=resolved_task_id,
|
|
5550
|
+
idempotency_key=idempotency_key,
|
|
5551
|
+
)
|
|
5552
|
+
|
|
5553
|
+
try:
|
|
5554
|
+
execution = invoke_sync(current_revision)
|
|
5555
|
+
except ExecutionConflictError:
|
|
5556
|
+
try:
|
|
5557
|
+
execution = invoke_sync(
|
|
5558
|
+
int(current_execution_envelope().get("execution_revision", -1))
|
|
5559
|
+
)
|
|
5560
|
+
except ExecutionStateError as exc:
|
|
5561
|
+
progress["status"] = "error"
|
|
5562
|
+
progress["updated_at"] = now_iso()
|
|
5563
|
+
progress.pop("pending_action", None)
|
|
5564
|
+
write_task(root, resolved_task_id, task)
|
|
5565
|
+
raise StateError(f"Cannot synchronize Canonical Spec design: {exc}") from exc
|
|
5566
|
+
except ExecutionConflictError as exc:
|
|
5567
|
+
terminal_conflict = _is_idempotency_key_conflict(exc)
|
|
5568
|
+
progress["status"] = "error" if terminal_conflict else "conflict"
|
|
5569
|
+
progress["updated_at"] = now_iso()
|
|
5570
|
+
if terminal_conflict:
|
|
5571
|
+
progress.pop("pending_action", None)
|
|
5572
|
+
write_task(root, resolved_task_id, task)
|
|
5573
|
+
raise StateError(f"Cannot synchronize Canonical Spec design after CAS retry: {exc}") from exc
|
|
5574
|
+
except ExecutionStateError as exc:
|
|
5575
|
+
progress["status"] = "error"
|
|
5576
|
+
progress["updated_at"] = now_iso()
|
|
5577
|
+
progress.pop("pending_action", None)
|
|
5578
|
+
write_task(root, resolved_task_id, task)
|
|
5579
|
+
raise StateError(f"Cannot synchronize Canonical Spec design: {exc}") from exc
|
|
5580
|
+
try:
|
|
5581
|
+
details = show_execution(spec_path)
|
|
5582
|
+
inspection = inspect_spec(
|
|
5583
|
+
spec_path,
|
|
5584
|
+
root,
|
|
5585
|
+
task.get("repo_paths") if isinstance(task.get("repo_paths"), dict) else {},
|
|
5586
|
+
task.get("selected_spec_tasks") or [],
|
|
5587
|
+
)
|
|
5588
|
+
except (ExecutionStateError, EasyDevSpecError) as exc:
|
|
5589
|
+
raise StateError(f"Cannot synchronize Canonical Spec design: {exc}") from exc
|
|
5590
|
+
if inspection.get("spec_id") != source.get("spec_id"):
|
|
5591
|
+
raise StateError("Synchronized Canonical Spec identity changed unexpectedly.")
|
|
5592
|
+
binding_was_synchronized = (
|
|
5593
|
+
source.get("revision") == inspection.get("revision")
|
|
5594
|
+
and source.get("design_sha256") == inspection.get("design_sha256")
|
|
5595
|
+
)
|
|
5596
|
+
source.update(
|
|
5597
|
+
{
|
|
5598
|
+
"revision": inspection["revision"],
|
|
5599
|
+
"design_sha256": inspection["design_sha256"],
|
|
5600
|
+
"document_sha256": inspection["document_sha256"],
|
|
5601
|
+
"execution_revision": execution["execution_revision"],
|
|
5602
|
+
}
|
|
5603
|
+
)
|
|
5604
|
+
event = _spec_event(execution, idempotency_key)
|
|
5605
|
+
if not binding_was_synchronized:
|
|
5606
|
+
reset_task_ids = set(event.get("task_ids", []))
|
|
5607
|
+
refreshed_dependencies: list[dict] = []
|
|
5608
|
+
for dependency in task.get("spec_dependency_evidence", []):
|
|
5609
|
+
if not isinstance(dependency, dict):
|
|
5610
|
+
continue
|
|
5611
|
+
refreshed = dict(dependency)
|
|
5612
|
+
if refreshed.get("source_task_id") in reset_task_ids:
|
|
5613
|
+
refreshed["status"] = "pending"
|
|
5614
|
+
refreshed["shared_status"] = "pending"
|
|
5615
|
+
for field in ("evidence", "satisfied_at", "satisfied_by"):
|
|
5616
|
+
refreshed.pop(field, None)
|
|
5617
|
+
refreshed_dependencies.append(refreshed)
|
|
5618
|
+
task["spec_dependency_evidence"] = refreshed_dependencies
|
|
5619
|
+
inspect_task_spec(root, task)
|
|
5620
|
+
progress.update(
|
|
5621
|
+
{
|
|
5622
|
+
"last_execution_revision": execution["execution_revision"],
|
|
5623
|
+
"last_event_id": event["event_id"],
|
|
5624
|
+
"last_idempotency_key": idempotency_key,
|
|
5625
|
+
"status": "ok",
|
|
5626
|
+
"updated_at": now_iso(),
|
|
5627
|
+
}
|
|
5628
|
+
)
|
|
5629
|
+
progress.pop("pending_action", None)
|
|
5630
|
+
if task.get("status") not in {"INIT", "ANALYSIS"}:
|
|
5631
|
+
task["status"] = "ANALYSIS"
|
|
5632
|
+
append_stage_history(task, "ANALYSIS", agent)
|
|
5633
|
+
task.pop("pending_transition", None)
|
|
5634
|
+
task["last_agent"] = agent
|
|
5635
|
+
already_acknowledged = any(
|
|
5636
|
+
record.get("type") == "spec-design-sync"
|
|
5637
|
+
and record.get("idempotency_key") == idempotency_key
|
|
5638
|
+
for record in execution_records(root, resolved_task_id)
|
|
5639
|
+
)
|
|
5640
|
+
if not already_acknowledged:
|
|
5641
|
+
append_execution_record(
|
|
5642
|
+
root,
|
|
5643
|
+
resolved_task_id,
|
|
5644
|
+
{
|
|
5645
|
+
"type": "spec-design-sync",
|
|
5646
|
+
"affected_task_ids": requested_task_ids,
|
|
5647
|
+
"event_id": event["event_id"],
|
|
5648
|
+
"design_sha256": details["design_sha256"],
|
|
5649
|
+
"execution_revision": execution["execution_revision"],
|
|
5650
|
+
"idempotency_key": idempotency_key,
|
|
5651
|
+
"timestamp": now_iso(),
|
|
5652
|
+
},
|
|
5653
|
+
)
|
|
5654
|
+
write_task(root, resolved_task_id, task)
|
|
5655
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
5656
|
+
snapshot["action"] = "sync-spec-design"
|
|
5657
|
+
return snapshot
|
|
5658
|
+
|
|
5659
|
+
|
|
5660
|
+
def _selected_execution_snapshots(inspection: dict, task: dict) -> dict[str, dict]:
|
|
5661
|
+
selected = set(task.get("selected_spec_tasks") or [])
|
|
5662
|
+
execution = inspection.get("execution")
|
|
5663
|
+
if not isinstance(execution, dict):
|
|
5664
|
+
raise StateError("Canonical Spec shared execution is unavailable.")
|
|
5665
|
+
return {
|
|
5666
|
+
str(snapshot.get("task_id")): snapshot
|
|
5667
|
+
for snapshot in execution.get("tasks", [])
|
|
5668
|
+
if isinstance(snapshot, dict) and snapshot.get("task_id") in selected
|
|
5669
|
+
}
|
|
5670
|
+
|
|
5671
|
+
|
|
5672
|
+
def _shared_attempt_projection(
|
|
5673
|
+
inspection: dict, source_task_id: str
|
|
5674
|
+
) -> tuple[str | None, set[str]]:
|
|
5675
|
+
execution = inspection.get("execution")
|
|
5676
|
+
if not isinstance(execution, dict):
|
|
5677
|
+
return None, set()
|
|
5678
|
+
events = [event for event in execution.get("events", []) if isinstance(event, dict)]
|
|
5679
|
+
start_index = next(
|
|
5680
|
+
(
|
|
5681
|
+
index
|
|
5682
|
+
for index in range(len(events) - 1, -1, -1)
|
|
5683
|
+
if events[index].get("type") == "task_status_changed"
|
|
5684
|
+
and events[index].get("task_id") == source_task_id
|
|
5685
|
+
and events[index].get("to_status") == "in_progress"
|
|
5686
|
+
),
|
|
5687
|
+
None,
|
|
5688
|
+
)
|
|
5689
|
+
if start_index is None:
|
|
5690
|
+
return None, set()
|
|
5691
|
+
start_event = events[start_index]
|
|
5692
|
+
completed_steps: set[str] = set()
|
|
5693
|
+
for event in events[start_index + 1 :]:
|
|
5694
|
+
if event.get("type") != "step_status_changed" or event.get("task_id") != source_task_id:
|
|
5695
|
+
continue
|
|
5696
|
+
step_id = str(event.get("step_id") or "")
|
|
5697
|
+
if not step_id:
|
|
5698
|
+
continue
|
|
5699
|
+
if event.get("step_status") == "completed":
|
|
5700
|
+
completed_steps.add(step_id)
|
|
5701
|
+
elif event.get("step_status") == "failed":
|
|
5702
|
+
completed_steps.discard(step_id)
|
|
5703
|
+
return str(start_event.get("event_id") or "") or None, completed_steps
|
|
5704
|
+
|
|
5705
|
+
|
|
5706
|
+
def _snapshot_dependencies_ready(snapshot: dict, all_snapshots: dict[str, dict]) -> bool:
|
|
5707
|
+
for dependency in snapshot.get("dependencies", []):
|
|
5708
|
+
if not isinstance(dependency, dict) or dependency.get("type") not in {"hard", "contract"}:
|
|
5709
|
+
continue
|
|
5710
|
+
if dependency.get("status") == "satisfied":
|
|
5711
|
+
continue
|
|
5712
|
+
if dependency.get("type") == "hard" and all_snapshots.get(
|
|
5713
|
+
str(dependency.get("task_id")), {}
|
|
5714
|
+
).get("status") == "completed":
|
|
5715
|
+
continue
|
|
5716
|
+
return False
|
|
5717
|
+
return True
|
|
5718
|
+
|
|
5719
|
+
|
|
5720
|
+
def writeback_ready_tasks_for_implement(
|
|
5721
|
+
root: Path,
|
|
5722
|
+
harness_task_id: str,
|
|
5723
|
+
task: dict,
|
|
5724
|
+
agent: str,
|
|
5725
|
+
restart_statuses: set[str] | None = None,
|
|
5726
|
+
) -> None:
|
|
5727
|
+
inspection, _ = inspect_task_spec(root, task)
|
|
5728
|
+
implement_attempt = 1 + sum(
|
|
5729
|
+
1
|
|
5730
|
+
for entry in task.get("stage_history", [])
|
|
5731
|
+
if isinstance(entry, dict) and entry.get("stage") == "IMPLEMENT"
|
|
5732
|
+
)
|
|
5733
|
+
all_snapshots = {
|
|
5734
|
+
str(snapshot.get("task_id")): snapshot
|
|
5735
|
+
for snapshot in inspection["execution"].get("tasks", [])
|
|
5736
|
+
if isinstance(snapshot, dict)
|
|
5737
|
+
}
|
|
5738
|
+
selected_snapshots = _selected_execution_snapshots(inspection, task)
|
|
5739
|
+
for source_task_id in task.get("selected_spec_tasks") or []:
|
|
5740
|
+
snapshot = selected_snapshots.get(str(source_task_id))
|
|
5741
|
+
if not snapshot or snapshot.get("status") == "in_progress":
|
|
5742
|
+
continue
|
|
5743
|
+
if restart_statuses is not None and snapshot.get("status") not in restart_statuses:
|
|
5744
|
+
continue
|
|
5745
|
+
if not _snapshot_dependencies_ready(snapshot, all_snapshots):
|
|
5746
|
+
continue
|
|
5747
|
+
key = (
|
|
5748
|
+
f"{harness_task_id}:{source_task_id}:enter-implement:"
|
|
5749
|
+
f"{task['spec_source']['revision']}:attempt-{implement_attempt}"
|
|
5750
|
+
)
|
|
5751
|
+
action = {
|
|
5752
|
+
"kind": "task",
|
|
5753
|
+
"source_task_id": source_task_id,
|
|
5754
|
+
"status": "in_progress",
|
|
5755
|
+
"summary": "Harness entered IMPLEMENT for a dependency-ready Canonical task",
|
|
5756
|
+
"evidence": [],
|
|
5757
|
+
"idempotency_key": key,
|
|
5758
|
+
"agent": agent,
|
|
5759
|
+
}
|
|
5760
|
+
_execute_spec_writeback(
|
|
5761
|
+
root,
|
|
5762
|
+
harness_task_id,
|
|
5763
|
+
task,
|
|
5764
|
+
action,
|
|
5765
|
+
key,
|
|
5766
|
+
lambda design_digest, execution_revision, source_task_id=source_task_id: record_task_status(
|
|
5767
|
+
stored_spec_path(root, task),
|
|
5768
|
+
str(source_task_id),
|
|
5769
|
+
"in_progress",
|
|
5770
|
+
"Harness entered IMPLEMENT for a dependency-ready Canonical task",
|
|
5771
|
+
SPEC_WRITEBACK_APP,
|
|
5772
|
+
spec_writeback_agent(agent),
|
|
5773
|
+
design_digest,
|
|
5774
|
+
execution_revision,
|
|
5775
|
+
run_id=harness_task_id,
|
|
5776
|
+
idempotency_key=key,
|
|
5777
|
+
),
|
|
5778
|
+
)
|
|
5779
|
+
|
|
5780
|
+
|
|
5781
|
+
def require_shared_task_statuses(root: Path, task: dict, allowed: set[str]) -> None:
|
|
5782
|
+
inspection, _ = inspect_task_spec(root, task)
|
|
5783
|
+
snapshots = _selected_execution_snapshots(inspection, task)
|
|
5784
|
+
invalid = [
|
|
5785
|
+
f"{task_id}:{snapshots.get(str(task_id), {}).get('status', 'missing')}"
|
|
5786
|
+
for task_id in task.get("selected_spec_tasks") or []
|
|
5787
|
+
if snapshots.get(str(task_id), {}).get("status") not in allowed
|
|
5788
|
+
]
|
|
5789
|
+
if invalid:
|
|
5790
|
+
raise StateError(
|
|
5791
|
+
"Canonical Spec writeback is incomplete for selected tasks: " + ", ".join(invalid)
|
|
5792
|
+
)
|
|
5793
|
+
|
|
5794
|
+
|
|
5795
|
+
def writeback_completed_tasks(
|
|
5796
|
+
root: Path,
|
|
5797
|
+
harness_task_id: str,
|
|
5798
|
+
task: dict,
|
|
5799
|
+
agent: str,
|
|
5800
|
+
) -> None:
|
|
5801
|
+
inspection, _ = inspect_task_spec(root, task)
|
|
5802
|
+
snapshots = _selected_execution_snapshots(inspection, task)
|
|
5803
|
+
for source_task_id in task.get("selected_spec_tasks") or []:
|
|
5804
|
+
status_value = snapshots.get(str(source_task_id), {}).get("status")
|
|
5805
|
+
if status_value == "completed":
|
|
5806
|
+
continue
|
|
5807
|
+
if status_value != "verified":
|
|
5808
|
+
raise StateError(
|
|
5809
|
+
f"Canonical task {source_task_id} must be verified before Harness COMPLETE."
|
|
5810
|
+
)
|
|
5811
|
+
key = f"{harness_task_id}:{source_task_id}:complete:{task['spec_source']['revision']}"
|
|
5812
|
+
action = {
|
|
5813
|
+
"kind": "task",
|
|
5814
|
+
"source_task_id": source_task_id,
|
|
5815
|
+
"status": "completed",
|
|
5816
|
+
"summary": "Harness MEMORY completed and the Canonical task is complete",
|
|
5817
|
+
"evidence": [],
|
|
5818
|
+
"idempotency_key": key,
|
|
5819
|
+
"agent": agent,
|
|
5820
|
+
}
|
|
5821
|
+
_execute_spec_writeback(
|
|
5822
|
+
root,
|
|
5823
|
+
harness_task_id,
|
|
5824
|
+
task,
|
|
5825
|
+
action,
|
|
5826
|
+
key,
|
|
5827
|
+
lambda design_digest, execution_revision, source_task_id=source_task_id: record_task_status(
|
|
5828
|
+
stored_spec_path(root, task),
|
|
5829
|
+
str(source_task_id),
|
|
5830
|
+
"completed",
|
|
5831
|
+
"Harness MEMORY completed and the Canonical task is complete",
|
|
5832
|
+
SPEC_WRITEBACK_APP,
|
|
5833
|
+
spec_writeback_agent(agent),
|
|
5834
|
+
design_digest,
|
|
5835
|
+
execution_revision,
|
|
5836
|
+
run_id=harness_task_id,
|
|
5837
|
+
idempotency_key=key,
|
|
5838
|
+
),
|
|
5839
|
+
)
|
|
5840
|
+
|
|
5841
|
+
|
|
5842
|
+
def cancel_shared_tasks(
|
|
5843
|
+
root: Path,
|
|
5844
|
+
harness_task_id: str,
|
|
5845
|
+
task: dict,
|
|
5846
|
+
reason: str,
|
|
5847
|
+
agent: str,
|
|
5848
|
+
) -> None:
|
|
5849
|
+
inspection, _ = inspect_task_spec(root, task)
|
|
5850
|
+
snapshots = _selected_execution_snapshots(inspection, task)
|
|
5851
|
+
for source_task_id in task.get("selected_spec_tasks") or []:
|
|
5852
|
+
current = snapshots.get(str(source_task_id), {}).get("status")
|
|
5853
|
+
if current in {"completed", "cancelled"}:
|
|
5854
|
+
continue
|
|
5855
|
+
if current in {"implemented", "verified"}:
|
|
5856
|
+
blocked_key = f"{harness_task_id}:{source_task_id}:close-blocked"
|
|
5857
|
+
blocked_action = {
|
|
5858
|
+
"kind": "task",
|
|
5859
|
+
"source_task_id": source_task_id,
|
|
5860
|
+
"status": "blocked",
|
|
5861
|
+
"summary": reason,
|
|
5862
|
+
"evidence": [],
|
|
5863
|
+
"idempotency_key": blocked_key,
|
|
5864
|
+
"agent": agent,
|
|
5865
|
+
}
|
|
5866
|
+
_execute_spec_writeback(
|
|
5867
|
+
root,
|
|
5868
|
+
harness_task_id,
|
|
5869
|
+
task,
|
|
5870
|
+
blocked_action,
|
|
5871
|
+
blocked_key,
|
|
5872
|
+
lambda design_digest, execution_revision, source_task_id=source_task_id: record_task_status(
|
|
5873
|
+
stored_spec_path(root, task),
|
|
5874
|
+
str(source_task_id),
|
|
5875
|
+
"blocked",
|
|
5876
|
+
reason,
|
|
5877
|
+
SPEC_WRITEBACK_APP,
|
|
5878
|
+
spec_writeback_agent(agent),
|
|
5879
|
+
design_digest,
|
|
5880
|
+
execution_revision,
|
|
5881
|
+
run_id=harness_task_id,
|
|
5882
|
+
idempotency_key=blocked_key,
|
|
5883
|
+
),
|
|
5884
|
+
)
|
|
5885
|
+
cancel_key = f"{harness_task_id}:{source_task_id}:cancel"
|
|
5886
|
+
cancel_action = {
|
|
5887
|
+
"kind": "task",
|
|
5888
|
+
"source_task_id": source_task_id,
|
|
5889
|
+
"status": "cancelled",
|
|
5890
|
+
"summary": reason,
|
|
5891
|
+
"evidence": [],
|
|
5892
|
+
"idempotency_key": cancel_key,
|
|
5893
|
+
"agent": agent,
|
|
5894
|
+
}
|
|
5895
|
+
_execute_spec_writeback(
|
|
5896
|
+
root,
|
|
5897
|
+
harness_task_id,
|
|
5898
|
+
task,
|
|
5899
|
+
cancel_action,
|
|
5900
|
+
cancel_key,
|
|
5901
|
+
lambda design_digest, execution_revision, source_task_id=source_task_id: record_task_status(
|
|
5902
|
+
stored_spec_path(root, task),
|
|
5903
|
+
str(source_task_id),
|
|
5904
|
+
"cancelled",
|
|
5905
|
+
reason,
|
|
5906
|
+
SPEC_WRITEBACK_APP,
|
|
5907
|
+
spec_writeback_agent(agent),
|
|
5908
|
+
design_digest,
|
|
5909
|
+
execution_revision,
|
|
5910
|
+
run_id=harness_task_id,
|
|
5911
|
+
idempotency_key=cancel_key,
|
|
5912
|
+
),
|
|
5913
|
+
)
|
|
5914
|
+
|
|
5915
|
+
|
|
5916
|
+
def satisfy_spec_dependency(
|
|
5917
|
+
root: Path,
|
|
5918
|
+
dependency_task_id: str,
|
|
5919
|
+
evidence: str,
|
|
5920
|
+
agent: str,
|
|
5921
|
+
source_task_id: str | None = None,
|
|
5922
|
+
task_id: str | None = None,
|
|
5923
|
+
session_file: str | Path | None = None,
|
|
5924
|
+
) -> dict:
|
|
5925
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
5926
|
+
if task.get("status") in TERMINAL_STATUSES or task.get("status") == "MEMORY":
|
|
5927
|
+
raise StateError("Spec dependency evidence cannot change after MEMORY begins.")
|
|
5928
|
+
if not is_non_empty_string(evidence):
|
|
5929
|
+
raise StateError("Spec dependency evidence must be non-empty.")
|
|
5930
|
+
inspect_task_spec(root, task)
|
|
5931
|
+
records = task.get("spec_dependency_evidence")
|
|
5932
|
+
if not isinstance(records, list):
|
|
5933
|
+
raise StateError("Current task is not backed by Canonical Spec dependency metadata.")
|
|
5934
|
+
matches = [
|
|
5935
|
+
record
|
|
5936
|
+
for record in records
|
|
5937
|
+
if isinstance(record, dict)
|
|
5938
|
+
and record.get("task_id") == dependency_task_id
|
|
5939
|
+
and (source_task_id is None or record.get("source_task_id") == source_task_id)
|
|
5940
|
+
]
|
|
5941
|
+
if not matches:
|
|
5942
|
+
raise StateError("Canonical Spec dependency edge was not found.")
|
|
5943
|
+
if source_task_id is None and len(matches) > 1:
|
|
5944
|
+
raise StateError(
|
|
5945
|
+
"Canonical Spec dependency is ambiguous; pass --source-task to identify the edge."
|
|
5946
|
+
)
|
|
5947
|
+
record = matches[0]
|
|
5948
|
+
if record.get("dependency_type") == "contract":
|
|
5949
|
+
raise StateError("Contract dependencies are satisfied by the frozen READY Spec.")
|
|
5950
|
+
record["status"] = "satisfied"
|
|
5951
|
+
record["evidence"] = evidence.strip()
|
|
5952
|
+
record["satisfied_at"] = now_iso()
|
|
5953
|
+
record["satisfied_by"] = agent
|
|
5954
|
+
task["last_agent"] = agent
|
|
5955
|
+
evidence_digest = hashlib.sha256(evidence.strip().encode("utf-8")).hexdigest()[:16]
|
|
5956
|
+
idempotency_key = (
|
|
5957
|
+
f"{resolved_task_id}:{record.get('source_task_id')}:{dependency_task_id}:"
|
|
5958
|
+
f"dependency-satisfied:revision-{task['spec_source']['revision']}:{evidence_digest}"
|
|
5959
|
+
)
|
|
5960
|
+
action = {
|
|
5961
|
+
"kind": "dependency",
|
|
5962
|
+
"source_task_id": str(record.get("source_task_id")),
|
|
5963
|
+
"dependency_task_id": dependency_task_id,
|
|
5964
|
+
"status": "satisfied",
|
|
5965
|
+
"summary": evidence.strip(),
|
|
5966
|
+
"evidence": [{"kind": "dependency", "status": "passed", "ref": evidence.strip()}],
|
|
5967
|
+
"idempotency_key": idempotency_key,
|
|
5968
|
+
"agent": agent,
|
|
5969
|
+
}
|
|
5970
|
+
_execute_spec_writeback(
|
|
5971
|
+
root,
|
|
5972
|
+
resolved_task_id,
|
|
5973
|
+
task,
|
|
5974
|
+
action,
|
|
5975
|
+
idempotency_key,
|
|
5976
|
+
lambda design_digest, execution_revision: record_dependency_status(
|
|
5977
|
+
stored_spec_path(root, task),
|
|
5978
|
+
str(record.get("source_task_id")),
|
|
5979
|
+
dependency_task_id,
|
|
5980
|
+
"satisfied",
|
|
5981
|
+
evidence.strip(),
|
|
5982
|
+
SPEC_WRITEBACK_APP,
|
|
5983
|
+
spec_writeback_agent(agent),
|
|
5984
|
+
design_digest,
|
|
5985
|
+
execution_revision,
|
|
5986
|
+
evidence=[{"kind": "dependency", "status": "passed", "ref": evidence.strip()}],
|
|
5987
|
+
run_id=resolved_task_id,
|
|
5988
|
+
idempotency_key=idempotency_key,
|
|
5989
|
+
),
|
|
5990
|
+
)
|
|
5991
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
5992
|
+
snapshot["action"] = "satisfy-spec-dependency"
|
|
5993
|
+
return snapshot
|
|
5994
|
+
|
|
5995
|
+
|
|
5996
|
+
def append_stage_history(task: dict, stage: str, agent: str) -> None:
|
|
5997
|
+
history = task.setdefault("stage_history", [])
|
|
5998
|
+
history.append({"stage": stage, "agent": agent, "entered_at": now_iso()})
|
|
5999
|
+
|
|
6000
|
+
|
|
6001
|
+
def resolve_current_task(
|
|
6002
|
+
root: Path,
|
|
6003
|
+
task_id: str | None = None,
|
|
6004
|
+
session_file: str | Path | None = None,
|
|
6005
|
+
) -> tuple[dict, str, dict]:
|
|
6006
|
+
session = ensure_session(root, session_file)
|
|
6007
|
+
resolved_task_id = task_id or session.get("current_task")
|
|
6008
|
+
if not resolved_task_id:
|
|
6009
|
+
raise StateError("No current task is set.")
|
|
6010
|
+
task = load_task(root, str(resolved_task_id))
|
|
6011
|
+
if task is None:
|
|
6012
|
+
raise StateError(f"Task not found: {resolved_task_id}")
|
|
6013
|
+
return session, str(resolved_task_id), task
|
|
4222
6014
|
|
|
4223
6015
|
|
|
4224
6016
|
def validate_workflow_mode_proposal(
|
|
@@ -4536,14 +6328,27 @@ def apply_transition(
|
|
|
4536
6328
|
if task.get("workflow_mode_legacy") is not True:
|
|
4537
6329
|
freeze_workflow_mode(root, session, resolved_task_id, task, agent)
|
|
4538
6330
|
freeze_tdd_mode(root, session, resolved_task_id, task, agent)
|
|
6331
|
+
if stage == "IMPLEMENT" and previous != "IMPLEMENT":
|
|
6332
|
+
if isinstance(task.get("spec_source"), dict):
|
|
6333
|
+
writeback_ready_tasks_for_implement(
|
|
6334
|
+
root,
|
|
6335
|
+
resolved_task_id,
|
|
6336
|
+
task,
|
|
6337
|
+
agent,
|
|
6338
|
+
{"blocked"} if previous in {"REVIEW", "VERIFICATION"} else None,
|
|
6339
|
+
)
|
|
4539
6340
|
if previous == "REVIEW" and stage == "VERIFICATION":
|
|
4540
6341
|
validate_review_readiness(root, resolved_task_id, task)
|
|
4541
6342
|
if previous == "VERIFICATION" and stage == "MEMORY":
|
|
4542
6343
|
validate_verification_readiness(root, resolved_task_id, task)
|
|
6344
|
+
if isinstance(task.get("spec_source"), dict):
|
|
6345
|
+
require_shared_task_statuses(root, task, {"verified", "completed"})
|
|
4543
6346
|
if previous == "MEMORY" and stage == "COMPLETE":
|
|
4544
6347
|
progress = task.get("memory_progress")
|
|
4545
6348
|
if not isinstance(progress, dict) or progress.get("completed") is not True:
|
|
4546
6349
|
raise StateError("MEMORY cannot advance to COMPLETE before memory processing completes.")
|
|
6350
|
+
if isinstance(task.get("spec_source"), dict):
|
|
6351
|
+
writeback_completed_tasks(root, resolved_task_id, task, agent)
|
|
4547
6352
|
if (previous, stage) == READ_ONLY_COMPLETION_TRANSITION:
|
|
4548
6353
|
validate_read_only_completion(root, resolved_task_id)
|
|
4549
6354
|
if previous != stage:
|
|
@@ -4770,6 +6575,7 @@ def memory_complete(
|
|
|
4770
6575
|
action == "distill" and instruction.get("checkpoint_disposition") == "candidate"
|
|
4771
6576
|
),
|
|
4772
6577
|
)
|
|
6578
|
+
validate_recorded_architecture_assessment(root, progress, instruction)
|
|
4773
6579
|
if action == "distill":
|
|
4774
6580
|
validate_distillation_file_sets(root, instruction)
|
|
4775
6581
|
progress["long_memory_action"] = action
|
|
@@ -4797,6 +6603,8 @@ def close_current_task(
|
|
|
4797
6603
|
task = load_task(root, str(task_id))
|
|
4798
6604
|
if task is None:
|
|
4799
6605
|
raise StateError(f"Task not found: {task_id}")
|
|
6606
|
+
if isinstance(task.get("spec_source"), dict) and task.get("status") not in TERMINAL_STATUSES:
|
|
6607
|
+
cancel_shared_tasks(root, str(task_id), task, reason, agent)
|
|
4800
6608
|
if task.get("status") != "CLOSED":
|
|
4801
6609
|
task["status"] = "CLOSED"
|
|
4802
6610
|
append_stage_history(task, "CLOSED", agent)
|
|
@@ -4898,6 +6706,19 @@ def parse_mapping_args(values: list[str], label: str) -> dict[str, str]:
|
|
|
4898
6706
|
return mappings
|
|
4899
6707
|
|
|
4900
6708
|
|
|
6709
|
+
def parse_evidence_args(values: list[str]) -> list[dict]:
|
|
6710
|
+
evidence: list[dict] = []
|
|
6711
|
+
for value in values:
|
|
6712
|
+
try:
|
|
6713
|
+
parsed = json.loads(value)
|
|
6714
|
+
except json.JSONDecodeError as exc:
|
|
6715
|
+
raise StateError(f"--evidence must be a JSON object: {exc}") from exc
|
|
6716
|
+
if not isinstance(parsed, dict):
|
|
6717
|
+
raise StateError("--evidence must be a JSON object.")
|
|
6718
|
+
evidence.append(parsed)
|
|
6719
|
+
return evidence
|
|
6720
|
+
|
|
6721
|
+
|
|
4901
6722
|
def main() -> int:
|
|
4902
6723
|
configure_stdio()
|
|
4903
6724
|
common = argparse.ArgumentParser(add_help=False)
|
|
@@ -4915,6 +6736,9 @@ def main() -> int:
|
|
|
4915
6736
|
inspect_spec_parser.add_argument("--spec", required=True)
|
|
4916
6737
|
inspect_spec_parser.add_argument("--repo-path", action="append", default=[])
|
|
4917
6738
|
|
|
6739
|
+
initialize_spec = subcommands.add_parser("initialize-spec-execution", parents=[common])
|
|
6740
|
+
initialize_spec.add_argument("--spec", required=True)
|
|
6741
|
+
|
|
4918
6742
|
select_spec_scope = subcommands.add_parser("select-dev-spec-scope", parents=[common])
|
|
4919
6743
|
select_spec_scope.add_argument("--spec", required=True)
|
|
4920
6744
|
select_spec_scope.add_argument("--spec-task", required=True, action="append")
|
|
@@ -4937,6 +6761,66 @@ def main() -> int:
|
|
|
4937
6761
|
create_from_spec.add_argument("--agent", required=True)
|
|
4938
6762
|
create_from_spec.add_argument("--no-set-current", action="store_true")
|
|
4939
6763
|
|
|
6764
|
+
rebind_spec = subcommands.add_parser("rebind-spec-source", parents=[common])
|
|
6765
|
+
rebind_spec.add_argument("--spec", required=True)
|
|
6766
|
+
rebind_spec.add_argument("--agent", required=True)
|
|
6767
|
+
rebind_spec.add_argument("--task-id")
|
|
6768
|
+
|
|
6769
|
+
writeback_task = subcommands.add_parser("writeback-spec-task", parents=[common])
|
|
6770
|
+
writeback_task.add_argument("--spec-task", required=True)
|
|
6771
|
+
writeback_task.add_argument(
|
|
6772
|
+
"--status",
|
|
6773
|
+
required=True,
|
|
6774
|
+
choices=[
|
|
6775
|
+
"in_progress",
|
|
6776
|
+
"blocked",
|
|
6777
|
+
"implemented",
|
|
6778
|
+
"verified",
|
|
6779
|
+
"completed",
|
|
6780
|
+
"cancelled",
|
|
6781
|
+
],
|
|
6782
|
+
)
|
|
6783
|
+
writeback_task.add_argument("--summary", required=True)
|
|
6784
|
+
writeback_task.add_argument("--evidence", action="append", default=[])
|
|
6785
|
+
writeback_task.add_argument("--idempotency-key", required=True)
|
|
6786
|
+
writeback_task.add_argument("--agent", required=True)
|
|
6787
|
+
writeback_task.add_argument("--task-id")
|
|
6788
|
+
|
|
6789
|
+
writeback_step = subcommands.add_parser("writeback-spec-step", parents=[common])
|
|
6790
|
+
writeback_step.add_argument("--spec-task", required=True)
|
|
6791
|
+
writeback_step.add_argument("--step", required=True)
|
|
6792
|
+
writeback_step.add_argument("--status", required=True, choices=["completed", "failed"])
|
|
6793
|
+
writeback_step.add_argument("--summary", required=True)
|
|
6794
|
+
writeback_step.add_argument("--evidence", action="append", default=[])
|
|
6795
|
+
writeback_step.add_argument("--idempotency-key", required=True)
|
|
6796
|
+
writeback_step.add_argument("--agent", required=True)
|
|
6797
|
+
writeback_step.add_argument("--task-id")
|
|
6798
|
+
|
|
6799
|
+
writeback_dependency = subcommands.add_parser(
|
|
6800
|
+
"writeback-spec-dependency", parents=[common]
|
|
6801
|
+
)
|
|
6802
|
+
writeback_dependency.add_argument("--source-task", required=True)
|
|
6803
|
+
writeback_dependency.add_argument("--dependency-task", required=True)
|
|
6804
|
+
writeback_dependency.add_argument(
|
|
6805
|
+
"--status", required=True, choices=["pending", "satisfied"]
|
|
6806
|
+
)
|
|
6807
|
+
writeback_dependency.add_argument("--summary", required=True)
|
|
6808
|
+
writeback_dependency.add_argument("--evidence", action="append", default=[])
|
|
6809
|
+
writeback_dependency.add_argument("--idempotency-key", required=True)
|
|
6810
|
+
writeback_dependency.add_argument("--agent", required=True)
|
|
6811
|
+
writeback_dependency.add_argument("--task-id")
|
|
6812
|
+
|
|
6813
|
+
sync_spec = subcommands.add_parser("sync-spec-design", parents=[common])
|
|
6814
|
+
sync_spec.add_argument("--affected-task", action="append", default=[])
|
|
6815
|
+
sync_spec.add_argument("--summary", required=True)
|
|
6816
|
+
sync_spec.add_argument("--idempotency-key", required=True)
|
|
6817
|
+
sync_spec.add_argument("--agent", required=True)
|
|
6818
|
+
sync_spec.add_argument("--task-id")
|
|
6819
|
+
|
|
6820
|
+
reconcile_spec = subcommands.add_parser("reconcile-spec-execution", parents=[common])
|
|
6821
|
+
reconcile_spec.add_argument("--agent", required=True)
|
|
6822
|
+
reconcile_spec.add_argument("--task-id")
|
|
6823
|
+
|
|
4940
6824
|
set_current = subcommands.add_parser("set-current", parents=[common])
|
|
4941
6825
|
set_current.add_argument("--task-id", required=True)
|
|
4942
6826
|
set_current.add_argument("--agent", required=True)
|
|
@@ -5066,6 +6950,18 @@ def main() -> int:
|
|
|
5066
6950
|
memory_instruction_parser.add_argument("--agent")
|
|
5067
6951
|
memory_instruction_parser.add_argument("--task-id")
|
|
5068
6952
|
|
|
6953
|
+
memory_architecture_parser = subcommands.add_parser(
|
|
6954
|
+
"memory-architecture-assessment", parents=[common]
|
|
6955
|
+
)
|
|
6956
|
+
memory_architecture_parser.add_argument(
|
|
6957
|
+
"--action", required=True, choices=sorted(ARCHITECTURE_ACTIONS)
|
|
6958
|
+
)
|
|
6959
|
+
memory_architecture_parser.add_argument("--reason", required=True)
|
|
6960
|
+
memory_architecture_parser.add_argument("--evidence", action="append", default=[])
|
|
6961
|
+
memory_architecture_parser.add_argument("--affected-section", action="append", default=[])
|
|
6962
|
+
memory_architecture_parser.add_argument("--agent", required=True)
|
|
6963
|
+
memory_architecture_parser.add_argument("--task-id")
|
|
6964
|
+
|
|
5069
6965
|
memory_complete_parser = subcommands.add_parser("memory-complete", parents=[common])
|
|
5070
6966
|
memory_complete_parser.add_argument("--action", required=True, choices=["no-op", "distill"])
|
|
5071
6967
|
memory_complete_parser.add_argument("--agent", required=True)
|
|
@@ -5107,6 +7003,7 @@ def main() -> int:
|
|
|
5107
7003
|
)
|
|
5108
7004
|
if session_file is None and command not in {
|
|
5109
7005
|
"inspect-dev-spec",
|
|
7006
|
+
"initialize-spec-execution",
|
|
5110
7007
|
"select-dev-spec-scope",
|
|
5111
7008
|
"list-tasks",
|
|
5112
7009
|
"memory-new-id",
|
|
@@ -5119,7 +7016,7 @@ def main() -> int:
|
|
|
5119
7016
|
if command == "snapshot":
|
|
5120
7017
|
emit(snapshot_state(root, session_file))
|
|
5121
7018
|
elif command == "inspect-dev-spec":
|
|
5122
|
-
spec_path = Path(args.spec)
|
|
7019
|
+
spec_path = Path(args.spec).expanduser()
|
|
5123
7020
|
emit(
|
|
5124
7021
|
inspection_summary(
|
|
5125
7022
|
inspect_spec(
|
|
@@ -5129,8 +7026,10 @@ def main() -> int:
|
|
|
5129
7026
|
)
|
|
5130
7027
|
)
|
|
5131
7028
|
)
|
|
7029
|
+
elif command == "initialize-spec-execution":
|
|
7030
|
+
emit(initialize_spec_execution_state(root, args.spec))
|
|
5132
7031
|
elif command == "select-dev-spec-scope":
|
|
5133
|
-
spec_path = Path(args.spec)
|
|
7032
|
+
spec_path = Path(args.spec).expanduser()
|
|
5134
7033
|
emit(
|
|
5135
7034
|
select_consumption_scopes(
|
|
5136
7035
|
spec_path if spec_path.is_absolute() else root / spec_path,
|
|
@@ -5181,6 +7080,100 @@ def main() -> int:
|
|
|
5181
7080
|
session_file,
|
|
5182
7081
|
)
|
|
5183
7082
|
)
|
|
7083
|
+
elif command == "rebind-spec-source":
|
|
7084
|
+
emit(
|
|
7085
|
+
attach_status_context(
|
|
7086
|
+
root,
|
|
7087
|
+
rebind_spec_source(root, args.spec, agent, args.task_id, session_file),
|
|
7088
|
+
agent,
|
|
7089
|
+
session_file,
|
|
7090
|
+
)
|
|
7091
|
+
)
|
|
7092
|
+
elif command == "writeback-spec-task":
|
|
7093
|
+
emit(
|
|
7094
|
+
attach_status_context(
|
|
7095
|
+
root,
|
|
7096
|
+
writeback_spec_task(
|
|
7097
|
+
root,
|
|
7098
|
+
args.spec_task,
|
|
7099
|
+
args.status,
|
|
7100
|
+
args.summary,
|
|
7101
|
+
parse_evidence_args(args.evidence),
|
|
7102
|
+
args.idempotency_key,
|
|
7103
|
+
agent,
|
|
7104
|
+
args.task_id,
|
|
7105
|
+
session_file,
|
|
7106
|
+
),
|
|
7107
|
+
agent,
|
|
7108
|
+
session_file,
|
|
7109
|
+
)
|
|
7110
|
+
)
|
|
7111
|
+
elif command == "writeback-spec-step":
|
|
7112
|
+
emit(
|
|
7113
|
+
attach_status_context(
|
|
7114
|
+
root,
|
|
7115
|
+
writeback_spec_step(
|
|
7116
|
+
root,
|
|
7117
|
+
args.spec_task,
|
|
7118
|
+
args.step,
|
|
7119
|
+
args.status,
|
|
7120
|
+
args.summary,
|
|
7121
|
+
parse_evidence_args(args.evidence),
|
|
7122
|
+
args.idempotency_key,
|
|
7123
|
+
agent,
|
|
7124
|
+
args.task_id,
|
|
7125
|
+
session_file,
|
|
7126
|
+
),
|
|
7127
|
+
agent,
|
|
7128
|
+
session_file,
|
|
7129
|
+
)
|
|
7130
|
+
)
|
|
7131
|
+
elif command == "writeback-spec-dependency":
|
|
7132
|
+
emit(
|
|
7133
|
+
attach_status_context(
|
|
7134
|
+
root,
|
|
7135
|
+
writeback_spec_dependency(
|
|
7136
|
+
root,
|
|
7137
|
+
args.source_task,
|
|
7138
|
+
args.dependency_task,
|
|
7139
|
+
args.status,
|
|
7140
|
+
args.summary,
|
|
7141
|
+
parse_evidence_args(args.evidence),
|
|
7142
|
+
args.idempotency_key,
|
|
7143
|
+
agent,
|
|
7144
|
+
args.task_id,
|
|
7145
|
+
session_file,
|
|
7146
|
+
),
|
|
7147
|
+
agent,
|
|
7148
|
+
session_file,
|
|
7149
|
+
)
|
|
7150
|
+
)
|
|
7151
|
+
elif command == "sync-spec-design":
|
|
7152
|
+
emit(
|
|
7153
|
+
attach_status_context(
|
|
7154
|
+
root,
|
|
7155
|
+
sync_spec_design_state(
|
|
7156
|
+
root,
|
|
7157
|
+
args.affected_task,
|
|
7158
|
+
args.summary,
|
|
7159
|
+
args.idempotency_key,
|
|
7160
|
+
agent,
|
|
7161
|
+
args.task_id,
|
|
7162
|
+
session_file,
|
|
7163
|
+
),
|
|
7164
|
+
agent,
|
|
7165
|
+
session_file,
|
|
7166
|
+
)
|
|
7167
|
+
)
|
|
7168
|
+
elif command == "reconcile-spec-execution":
|
|
7169
|
+
emit(
|
|
7170
|
+
attach_status_context(
|
|
7171
|
+
root,
|
|
7172
|
+
reconcile_spec_execution(root, agent, args.task_id, session_file),
|
|
7173
|
+
agent,
|
|
7174
|
+
session_file,
|
|
7175
|
+
)
|
|
7176
|
+
)
|
|
5184
7177
|
elif command == "set-current":
|
|
5185
7178
|
emit(
|
|
5186
7179
|
attach_status_context(
|
|
@@ -5447,6 +7440,24 @@ def main() -> int:
|
|
|
5447
7440
|
session_file,
|
|
5448
7441
|
)
|
|
5449
7442
|
)
|
|
7443
|
+
elif command == "memory-architecture-assessment":
|
|
7444
|
+
emit(
|
|
7445
|
+
attach_status_context(
|
|
7446
|
+
root,
|
|
7447
|
+
record_architecture_assessment(
|
|
7448
|
+
root,
|
|
7449
|
+
args.action,
|
|
7450
|
+
args.reason,
|
|
7451
|
+
args.evidence,
|
|
7452
|
+
args.affected_section,
|
|
7453
|
+
agent,
|
|
7454
|
+
args.task_id,
|
|
7455
|
+
session_file,
|
|
7456
|
+
),
|
|
7457
|
+
agent,
|
|
7458
|
+
session_file,
|
|
7459
|
+
)
|
|
7460
|
+
)
|
|
5450
7461
|
elif command == "memory-complete":
|
|
5451
7462
|
emit(
|
|
5452
7463
|
attach_status_context(
|