easy-coding-harness 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +15 -4
- package/dist/cli.js +33 -92
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +13 -14
- package/templates/common/skills/ec-config/SKILL.md +3 -1
- package/templates/common/skills/ec-implementing/SKILL.md +11 -2
- package/templates/common/skills/ec-quality/SKILL.md +5 -0
- package/templates/common/skills/ec-task-management/SKILL.md +9 -2
- package/templates/common/skills/ec-tdd-init/SKILL.md +13 -9
- package/templates/common/skills/ec-workflow/SKILL.md +12 -3
- package/templates/main-constraint/AGENTS.md.tpl +7 -1
- package/templates/main-constraint/CLAUDE.md.tpl +7 -1
- package/templates/runtime/tools/easy_coding_tdd_readiness.py +18 -18
- package/templates/shared-hooks/easy_coding_state.py +341 -55
- package/templates/shared-hooks/easy_dev_spec.py +3 -1
|
@@ -539,11 +539,7 @@ def read_project_behavior(root: Path) -> tuple[str, str, bool, int]:
|
|
|
539
539
|
"expected adaptive, fast, standard, or strict."
|
|
540
540
|
)
|
|
541
541
|
if schema_version >= 4:
|
|
542
|
-
tdd_enabled = (
|
|
543
|
-
parse_yaml_bool(behavior.get("tdd_enabled"), "behavior.tdd_enabled")
|
|
544
|
-
if schema_version >= 5
|
|
545
|
-
else DEFAULT_TDD_ENABLED
|
|
546
|
-
)
|
|
542
|
+
tdd_enabled = parse_yaml_bool(behavior.get("tdd_enabled"), "behavior.tdd_enabled")
|
|
547
543
|
tdd_threshold = parse_tdd_threshold(
|
|
548
544
|
behavior.get("tdd_coverage_threshold", DEFAULT_TDD_COVERAGE_THRESHOLD),
|
|
549
545
|
"behavior.tdd_coverage_threshold",
|
|
@@ -610,17 +606,17 @@ def tdd_ci_contract_reasons(contents: list[str]) -> list[str]:
|
|
|
610
606
|
return reasons
|
|
611
607
|
|
|
612
608
|
|
|
613
|
-
def tdd_readiness(root: Path) -> dict[str, object]:
|
|
609
|
+
def tdd_readiness(root: Path, include_ci: bool = False) -> dict[str, object]:
|
|
614
610
|
receipt = root / TDD_READINESS_PATH
|
|
615
611
|
if not receipt.is_file():
|
|
616
612
|
return {"status": "needs_init", "reasons": ["TDD readiness receipt is missing"]}
|
|
617
613
|
try:
|
|
618
614
|
manifest = json.loads(receipt.read_text(encoding="utf-8"))
|
|
619
615
|
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
620
|
-
return {"status": "
|
|
616
|
+
return {"status": "needs_repair", "reasons": ["TDD readiness receipt is invalid"]}
|
|
621
617
|
if not isinstance(manifest, dict):
|
|
622
618
|
return {
|
|
623
|
-
"status": "
|
|
619
|
+
"status": "needs_repair",
|
|
624
620
|
"reasons": ["TDD readiness receipt must be a JSON object"],
|
|
625
621
|
}
|
|
626
622
|
|
|
@@ -650,9 +646,10 @@ def tdd_readiness(root: Path) -> dict[str, object]:
|
|
|
650
646
|
|
|
651
647
|
contents: dict[str, list[str]] = {
|
|
652
648
|
"build_files": [],
|
|
653
|
-
"ci_files": [],
|
|
654
649
|
"tool_files": [],
|
|
655
650
|
}
|
|
651
|
+
if include_ci:
|
|
652
|
+
contents["ci_files"] = []
|
|
656
653
|
for field in contents:
|
|
657
654
|
records = manifest.get(field)
|
|
658
655
|
if not isinstance(records, list) or not records:
|
|
@@ -663,11 +660,8 @@ def tdd_readiness(root: Path) -> dict[str, object]:
|
|
|
663
660
|
reasons.append(f"{field} contains an invalid record")
|
|
664
661
|
continue
|
|
665
662
|
file_name = record.get("path")
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
r"[a-f0-9]{64}", str(expected or "")
|
|
669
|
-
):
|
|
670
|
-
reasons.append(f"{field} contains an invalid path or SHA-256")
|
|
663
|
+
if not is_non_empty_string(file_name):
|
|
664
|
+
reasons.append(f"{field} contains an invalid path")
|
|
671
665
|
continue
|
|
672
666
|
candidate = Path(str(file_name))
|
|
673
667
|
if candidate.is_absolute():
|
|
@@ -678,8 +672,6 @@ def tdd_readiness(root: Path) -> dict[str, object]:
|
|
|
678
672
|
resolved.relative_to(root.resolve())
|
|
679
673
|
payload = resolved.read_bytes()
|
|
680
674
|
contents[field].append(payload.decode("utf-8"))
|
|
681
|
-
if hashlib.sha256(payload).hexdigest() != expected:
|
|
682
|
-
reasons.append(f"readiness file changed: {file_name}")
|
|
683
675
|
except (OSError, UnicodeError, ValueError):
|
|
684
676
|
reasons.append(f"readiness file is missing or unreadable: {file_name}")
|
|
685
677
|
|
|
@@ -698,8 +690,6 @@ def tdd_readiness(root: Path) -> dict[str, object]:
|
|
|
698
690
|
} if isinstance(manifest_ci_files, list) else set()
|
|
699
691
|
if not build_paths.intersection(JAVA_BUILD_FILE_NAMES):
|
|
700
692
|
reasons.append("build_files must include a Maven or Gradle Java build file")
|
|
701
|
-
if not ci_paths.intersection(GITLAB_CI_ENTRY_FILES):
|
|
702
|
-
reasons.append("ci_files must include the project-root GitLab CI entry file")
|
|
703
693
|
tool_paths = {
|
|
704
694
|
str(item.get("path", "")).replace("\\", "/")
|
|
705
695
|
for item in manifest_tool_files
|
|
@@ -707,11 +697,14 @@ def tdd_readiness(root: Path) -> dict[str, object]:
|
|
|
707
697
|
} if isinstance(manifest_tool_files, list) else set()
|
|
708
698
|
if COVERAGE_TOOL_PATH not in tool_paths:
|
|
709
699
|
reasons.append(f"tool_files must include {COVERAGE_TOOL_PATH}")
|
|
710
|
-
if
|
|
711
|
-
|
|
712
|
-
|
|
700
|
+
if include_ci:
|
|
701
|
+
if not ci_paths.intersection(GITLAB_CI_ENTRY_FILES):
|
|
702
|
+
reasons.append("ci_files must include the project-root GitLab CI entry file")
|
|
703
|
+
if not any("jacoco" in content.lower() for content in contents["build_files"]):
|
|
704
|
+
reasons.append("build files do not configure JaCoCo")
|
|
705
|
+
reasons.extend(tdd_ci_contract_reasons(contents["ci_files"]))
|
|
713
706
|
return {
|
|
714
|
-
"status": "ready" if not reasons else "
|
|
707
|
+
"status": "ready" if not reasons else "needs_repair",
|
|
715
708
|
"reasons": list(dict.fromkeys(reasons)),
|
|
716
709
|
}
|
|
717
710
|
|
|
@@ -720,7 +713,8 @@ def require_tdd_readiness(root: Path) -> None:
|
|
|
720
713
|
readiness = tdd_readiness(root)
|
|
721
714
|
if readiness["status"] != "ready":
|
|
722
715
|
reasons = "; ".join(str(reason) for reason in readiness["reasons"])
|
|
723
|
-
|
|
716
|
+
action = "Run ec-tdd-init first" if readiness["status"] == "needs_init" else "Repair TDD readiness"
|
|
717
|
+
raise StateError(f"{action}: {reasons}. TDD settings are unchanged.")
|
|
724
718
|
|
|
725
719
|
|
|
726
720
|
def resolve_behavior(
|
|
@@ -1121,6 +1115,7 @@ def record_architecture_assessment(
|
|
|
1121
1115
|
if action not in ARCHITECTURE_ACTIONS:
|
|
1122
1116
|
raise StateError(f"Unknown architecture assessment action: {action}")
|
|
1123
1117
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
1118
|
+
require_spec_context(root, task, agent, session_file)
|
|
1124
1119
|
if task.get("status") != "MEMORY":
|
|
1125
1120
|
raise StateError("Architecture assessment is only available during MEMORY.")
|
|
1126
1121
|
progress = task.get("memory_progress")
|
|
@@ -2050,7 +2045,9 @@ def legacy_source_digest_matches(
|
|
|
2050
2045
|
return legacy_sha256 == design_document_sha256
|
|
2051
2046
|
|
|
2052
2047
|
|
|
2053
|
-
def inspect_task_spec(
|
|
2048
|
+
def inspect_task_spec(
|
|
2049
|
+
root: Path, task: dict, *, allow_pending_hard_dependencies: bool = False,
|
|
2050
|
+
) -> tuple[dict, dict]:
|
|
2054
2051
|
source = task.get("spec_source")
|
|
2055
2052
|
selected = task.get("selected_spec_tasks")
|
|
2056
2053
|
repo_paths = task.get("repo_paths")
|
|
@@ -2072,7 +2069,10 @@ def inspect_task_spec(root: Path, task: dict) -> tuple[dict, dict]:
|
|
|
2072
2069
|
and is_non_empty_string(record.get("task_id"))
|
|
2073
2070
|
and is_non_empty_string(record.get("evidence"))
|
|
2074
2071
|
}
|
|
2075
|
-
selection = select_tasks(
|
|
2072
|
+
selection = select_tasks(
|
|
2073
|
+
inspection, selected, satisfied,
|
|
2074
|
+
allow_pending_hard_dependencies=allow_pending_hard_dependencies,
|
|
2075
|
+
)
|
|
2076
2076
|
except EasyDevSpecError as exc:
|
|
2077
2077
|
raise StateError(f"Canonical Spec validation failed: {exc}") from exc
|
|
2078
2078
|
if not isinstance(inspection.get("execution"), dict):
|
|
@@ -2179,6 +2179,117 @@ def inspect_task_spec(root: Path, task: dict) -> tuple[dict, dict]:
|
|
|
2179
2179
|
return inspection, selection
|
|
2180
2180
|
|
|
2181
2181
|
|
|
2182
|
+
def restore_spec_context(
|
|
2183
|
+
root: Path, task_id: str, task: dict, agent: str,
|
|
2184
|
+
session_file: str | Path | None = None,
|
|
2185
|
+
) -> dict | None:
|
|
2186
|
+
if not isinstance(task.get("spec_source"), dict):
|
|
2187
|
+
return None
|
|
2188
|
+
task.pop("spec_context", None)
|
|
2189
|
+
try:
|
|
2190
|
+
inspection, _ = inspect_task_spec(root, task, allow_pending_hard_dependencies=True)
|
|
2191
|
+
context = select_consumption_scopes(
|
|
2192
|
+
stored_spec_path(root, task), root, task["selected_spec_tasks"]
|
|
2193
|
+
)
|
|
2194
|
+
if context.get("design_sha256") != inspection.get("design_sha256"):
|
|
2195
|
+
raise StateError("Canonical Spec changed while restoring context; retry resume-spec-context.")
|
|
2196
|
+
if isinstance(task.get("spec_change"), dict):
|
|
2197
|
+
raise StateError("Confirmed Spec change is pending; update the bound source and run sync-spec-design.")
|
|
2198
|
+
task["spec_context"] = {
|
|
2199
|
+
"session_file": resolve_session_path(root, session_file).relative_to(root.resolve()).as_posix(),
|
|
2200
|
+
"agent": normalize_session_agent(agent),
|
|
2201
|
+
"spec_id": inspection["spec_id"],
|
|
2202
|
+
"revision": inspection["revision"],
|
|
2203
|
+
"design_sha256": inspection["design_sha256"],
|
|
2204
|
+
"selected_spec_tasks": task["selected_spec_tasks"],
|
|
2205
|
+
"loaded_at": now_iso(),
|
|
2206
|
+
}
|
|
2207
|
+
result = {"status": "ready", "consumption": context}
|
|
2208
|
+
except (StateError, EasyDevSpecError, OSError, UnicodeError) as exc:
|
|
2209
|
+
# 接手仍可成功,修复来源与同步状态后必须重新加载,不能沿用旧会话的消费记录。
|
|
2210
|
+
result = {"status": "blocked", "reason": str(exc), "source": task["spec_source"]}
|
|
2211
|
+
write_task(root, task_id, task)
|
|
2212
|
+
return result
|
|
2213
|
+
|
|
2214
|
+
|
|
2215
|
+
def resume_spec_context(
|
|
2216
|
+
root: Path, agent: str, task_id: str | None = None,
|
|
2217
|
+
session_file: str | Path | None = None,
|
|
2218
|
+
) -> dict:
|
|
2219
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
2220
|
+
if not isinstance(task.get("spec_source"), dict):
|
|
2221
|
+
raise StateError("Current task is not backed by a Canonical Spec.")
|
|
2222
|
+
context = restore_spec_context(root, resolved_task_id, task, agent, session_file)
|
|
2223
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
2224
|
+
snapshot.update({"action": "resume-spec-context", "spec_context": context})
|
|
2225
|
+
return snapshot
|
|
2226
|
+
|
|
2227
|
+
|
|
2228
|
+
def require_spec_context(
|
|
2229
|
+
root: Path, task: dict, agent: str, session_file: str | Path | None = None,
|
|
2230
|
+
*, allow_pending_hard_dependencies: bool = False,
|
|
2231
|
+
) -> None:
|
|
2232
|
+
if not isinstance(task.get("spec_source"), dict):
|
|
2233
|
+
return
|
|
2234
|
+
if isinstance(task.get("spec_change"), dict):
|
|
2235
|
+
raise StateError("Confirmed Spec change is pending; update the bound source and run sync-spec-design.")
|
|
2236
|
+
inspection, _ = inspect_task_spec(
|
|
2237
|
+
root, task, allow_pending_hard_dependencies=allow_pending_hard_dependencies,
|
|
2238
|
+
)
|
|
2239
|
+
receipt = task.get("spec_context")
|
|
2240
|
+
expected = {
|
|
2241
|
+
"session_file": resolve_session_path(root, session_file).relative_to(root.resolve()).as_posix(),
|
|
2242
|
+
"agent": normalize_session_agent(agent),
|
|
2243
|
+
"spec_id": inspection["spec_id"],
|
|
2244
|
+
"revision": inspection["revision"],
|
|
2245
|
+
"design_sha256": inspection["design_sha256"],
|
|
2246
|
+
"selected_spec_tasks": task["selected_spec_tasks"],
|
|
2247
|
+
}
|
|
2248
|
+
if not isinstance(receipt, dict) or any(receipt.get(key) != value for key, value in expected.items()):
|
|
2249
|
+
raise StateError("Current session must consume the bound Canonical Spec via resume-spec-context before advancing.")
|
|
2250
|
+
|
|
2251
|
+
|
|
2252
|
+
def begin_spec_change(
|
|
2253
|
+
root: Path, affected_task_ids: list[str], summary: str, agent: str,
|
|
2254
|
+
task_id: str | None = None, session_file: str | Path | None = None,
|
|
2255
|
+
) -> dict:
|
|
2256
|
+
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
2257
|
+
if task.get("status") in TERMINAL_STATUSES:
|
|
2258
|
+
raise StateError("Cannot change the design of a terminal task.")
|
|
2259
|
+
selected = task.get("selected_spec_tasks") or []
|
|
2260
|
+
affected = sorted(set(affected_task_ids))
|
|
2261
|
+
if not affected or not set(affected).issubset(selected) or not is_non_empty_string(summary):
|
|
2262
|
+
raise StateError("Spec change requires selected affected task IDs and a confirmed summary.")
|
|
2263
|
+
existing = task.get("spec_change")
|
|
2264
|
+
if isinstance(existing, dict):
|
|
2265
|
+
if existing.get("affected_task_ids") != affected or existing.get("summary") != summary.strip():
|
|
2266
|
+
raise StateError("A different confirmed Spec change is pending; finish its synchronization first.")
|
|
2267
|
+
else:
|
|
2268
|
+
inspect_task_spec(root, task, allow_pending_hard_dependencies=True)
|
|
2269
|
+
if _writeback_progress(task).get("pending_action"):
|
|
2270
|
+
raise StateError("Reconcile pending Spec writeback before beginning a design change.")
|
|
2271
|
+
cancel_active_quality_attempt(root, resolved_task_id, task, agent, summary, "manual-return")
|
|
2272
|
+
task = load_task(root, resolved_task_id) or task
|
|
2273
|
+
cleanup_verification_checkpoint(root, resolved_task_id, task)
|
|
2274
|
+
task["spec_change"] = {
|
|
2275
|
+
"summary": summary.strip(), "affected_task_ids": affected,
|
|
2276
|
+
"spec_id": task["spec_source"]["spec_id"],
|
|
2277
|
+
"revision": task["spec_source"]["revision"],
|
|
2278
|
+
"design_sha256": task["spec_source"]["design_sha256"],
|
|
2279
|
+
"confirmed_by": agent, "confirmed_at": now_iso(),
|
|
2280
|
+
}
|
|
2281
|
+
task.pop("spec_context", None)
|
|
2282
|
+
task.pop("pending_transition", None)
|
|
2283
|
+
if task.get("status") != "ANALYSIS":
|
|
2284
|
+
task["status"] = "ANALYSIS"
|
|
2285
|
+
append_stage_history(task, "ANALYSIS", agent)
|
|
2286
|
+
task["last_agent"] = agent
|
|
2287
|
+
write_task(root, resolved_task_id, task)
|
|
2288
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
2289
|
+
snapshot["action"] = "begin-spec-change"
|
|
2290
|
+
return snapshot
|
|
2291
|
+
|
|
2292
|
+
|
|
2182
2293
|
def is_valid_spec_execution_plan(root: Path, task: dict, plan: object) -> bool:
|
|
2183
2294
|
if not isinstance(plan, dict):
|
|
2184
2295
|
return False
|
|
@@ -2931,6 +3042,40 @@ def update_git_worktree_fingerprint(
|
|
|
2931
3042
|
)
|
|
2932
3043
|
|
|
2933
3044
|
|
|
3045
|
+
def tdd_infrastructure_fingerprint(repositories: set[Path]) -> str:
|
|
3046
|
+
# 初始化快照不参与就绪判断;当前输入同时约束验收和跨仓证据继承。
|
|
3047
|
+
digest = hashlib.sha256()
|
|
3048
|
+
for repository in sorted(repositories, key=str):
|
|
3049
|
+
receipt = repository / TDD_READINESS_PATH
|
|
3050
|
+
digest.update(str(repository).encode("utf-8") + b"\0")
|
|
3051
|
+
try:
|
|
3052
|
+
receipt_payload = receipt.read_bytes()
|
|
3053
|
+
manifest = json.loads(receipt_payload)
|
|
3054
|
+
digest.update(receipt_payload)
|
|
3055
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
3056
|
+
digest.update(b"<missing-or-invalid-readiness>")
|
|
3057
|
+
continue
|
|
3058
|
+
if not isinstance(manifest, dict):
|
|
3059
|
+
continue
|
|
3060
|
+
for field in ("build_files", "tool_files"):
|
|
3061
|
+
records = manifest.get(field)
|
|
3062
|
+
for record in records if isinstance(records, list) else []:
|
|
3063
|
+
if not isinstance(record, dict) or not is_non_empty_string(record.get("path")):
|
|
3064
|
+
continue
|
|
3065
|
+
candidate = (repository / str(record["path"])).resolve()
|
|
3066
|
+
try:
|
|
3067
|
+
candidate.relative_to(repository.resolve())
|
|
3068
|
+
except ValueError as error:
|
|
3069
|
+
raise StateError("TDD evidence file escapes repository.") from error
|
|
3070
|
+
digest.update(f"{field}:{record['path']}".encode("utf-8") + b"\0")
|
|
3071
|
+
try:
|
|
3072
|
+
digest.update(candidate.read_bytes())
|
|
3073
|
+
except OSError:
|
|
3074
|
+
digest.update(b"<missing>")
|
|
3075
|
+
digest.update(b"\0")
|
|
3076
|
+
return digest.hexdigest()
|
|
3077
|
+
|
|
3078
|
+
|
|
2934
3079
|
def implementation_fingerprint(root: Path, task_id: str) -> str:
|
|
2935
3080
|
plan = latest_execution_plan(root, task_id)
|
|
2936
3081
|
if not plan:
|
|
@@ -2953,6 +3098,9 @@ def implementation_fingerprint(root: Path, task_id: str) -> str:
|
|
|
2953
3098
|
).encode("utf-8")
|
|
2954
3099
|
)
|
|
2955
3100
|
digest.update(b"\0")
|
|
3101
|
+
digest.update(tdd_infrastructure_fingerprint(
|
|
3102
|
+
{root.resolve(), *task_repository_roots(root, task, plan)}
|
|
3103
|
+
).encode("ascii"))
|
|
2956
3104
|
digest.update(b"execution-plan\0")
|
|
2957
3105
|
digest.update(
|
|
2958
3106
|
json.dumps(
|
|
@@ -3046,6 +3194,8 @@ def canonical_repository_fingerprints(
|
|
|
3046
3194
|
base = root / base
|
|
3047
3195
|
base = base.resolve()
|
|
3048
3196
|
digest = hashlib.sha256()
|
|
3197
|
+
if task.get("tdd_enabled") is True:
|
|
3198
|
+
digest.update(tdd_infrastructure_fingerprint({root.resolve(), base}).encode("ascii"))
|
|
3049
3199
|
units = [
|
|
3050
3200
|
unit
|
|
3051
3201
|
for unit in plan.get("units", [])
|
|
@@ -3911,6 +4061,7 @@ def finalize_quality_decision(
|
|
|
3911
4061
|
session_file: str | Path | None = None,
|
|
3912
4062
|
) -> dict:
|
|
3913
4063
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
4064
|
+
require_spec_context(root, task, agent, session_file)
|
|
3914
4065
|
if task.get("status") != "QUALITY":
|
|
3915
4066
|
raise StateError("A QUALITY decision can only be finalized during QUALITY.")
|
|
3916
4067
|
record = finalize_quality_attempt(
|
|
@@ -4064,6 +4215,9 @@ def verification_contract_fingerprint(root: Path, task_id: str, task: dict) -> s
|
|
|
4064
4215
|
"tdd_enabled": task.get("tdd_enabled"),
|
|
4065
4216
|
"tdd_coverage_threshold": task.get("tdd_coverage_threshold"),
|
|
4066
4217
|
"tdd_baselines": task.get("tdd_baselines"),
|
|
4218
|
+
**({"tdd_infrastructure": tdd_infrastructure_fingerprint(
|
|
4219
|
+
{root.resolve(), *task_repository_roots(root, task, plan)}
|
|
4220
|
+
)} if task.get("tdd_enabled") is True else {}),
|
|
4067
4221
|
"plan": plan,
|
|
4068
4222
|
"canonical": {
|
|
4069
4223
|
"schema": source.get("schema"),
|
|
@@ -4471,6 +4625,7 @@ def record_verification_checkpoint(
|
|
|
4471
4625
|
session_file: str | Path | None = None,
|
|
4472
4626
|
) -> dict:
|
|
4473
4627
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
4628
|
+
require_spec_context(root, task, agent, session_file)
|
|
4474
4629
|
if task.get("status") != "QUALITY":
|
|
4475
4630
|
raise StateError("Quality checkpoint can only be recorded during QUALITY.")
|
|
4476
4631
|
if isinstance(task.get("quality_checkpoint"), dict):
|
|
@@ -4567,6 +4722,7 @@ def inspect_transition_drift(
|
|
|
4567
4722
|
session_file: str | Path | None = None,
|
|
4568
4723
|
) -> dict:
|
|
4569
4724
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
4725
|
+
require_spec_context(root, task, agent, session_file)
|
|
4570
4726
|
if task.get("status") != "QUALITY":
|
|
4571
4727
|
raise StateError("Transition drift can only be inspected during QUALITY.")
|
|
4572
4728
|
task = ensure_verification_checkpoint(root, resolved_task_id, task, agent, session_file)
|
|
@@ -5152,7 +5308,7 @@ def validate_verification_readiness(
|
|
|
5152
5308
|
+ ", ".join(missing_source_tasks)
|
|
5153
5309
|
)
|
|
5154
5310
|
if str(task.get("type") or "").strip().lower() == TDD_INIT_TASK_TYPE:
|
|
5155
|
-
readiness = tdd_readiness(root)
|
|
5311
|
+
readiness = tdd_readiness(root, include_ci=True)
|
|
5156
5312
|
if readiness["status"] != "ready":
|
|
5157
5313
|
raise StateError(
|
|
5158
5314
|
"TDD initialization cannot advance to MEMORY until readiness passes: "
|
|
@@ -6376,7 +6532,8 @@ def validate_analysis_readiness(
|
|
|
6376
6532
|
readiness = tdd_readiness(root)
|
|
6377
6533
|
if readiness["status"] != "ready":
|
|
6378
6534
|
reasons.append(
|
|
6379
|
-
"TDD infrastructure is not ready; run ec-tdd-init first: "
|
|
6535
|
+
("TDD infrastructure is not ready; run ec-tdd-init first: "
|
|
6536
|
+
if readiness["status"] == "needs_init" else "Repair TDD readiness: ")
|
|
6380
6537
|
+ "; ".join(str(reason) for reason in readiness["reasons"])
|
|
6381
6538
|
)
|
|
6382
6539
|
plan = latest_execution_plan(root, task_id) or {}
|
|
@@ -6695,6 +6852,8 @@ def spec_task_summary(task: dict | None) -> dict | None:
|
|
|
6695
6852
|
"repositories": task.get("spec_repositories", []),
|
|
6696
6853
|
"pending_dependencies": pending_dependencies,
|
|
6697
6854
|
"writeback": task.get("spec_writeback_progress"),
|
|
6855
|
+
"context": task.get("spec_context"),
|
|
6856
|
+
"pending_change": task.get("spec_change"),
|
|
6698
6857
|
}
|
|
6699
6858
|
|
|
6700
6859
|
|
|
@@ -6939,6 +7098,14 @@ def build_machine_breadcrumbs(
|
|
|
6939
7098
|
|
|
6940
7099
|
if task_id:
|
|
6941
7100
|
lines.append(f"[current-task:{task_id}]")
|
|
7101
|
+
if task and isinstance(task.get("spec_source"), dict):
|
|
7102
|
+
source = task["spec_source"]
|
|
7103
|
+
lines.append(f"[easy-coding:spec:{source.get('spec_id')}:revision:{source.get('revision')}]")
|
|
7104
|
+
lines.append("[easy-coding:spec-context:resume-spec-context-on-session-resume]")
|
|
7105
|
+
if task.get("spec_change"):
|
|
7106
|
+
lines.append("[easy-coding:spec-change:pending-sync-spec-design]")
|
|
7107
|
+
if (task.get("spec_writeback_progress") or {}).get("pending_action"):
|
|
7108
|
+
lines.append("[easy-coding:spec-writeback:reconcile-spec-execution-required]")
|
|
6942
7109
|
if state["task_missing"]:
|
|
6943
7110
|
lines.append(f"[easy-coding:current-task-missing:{task_id}]")
|
|
6944
7111
|
handoff = pending_handoff_record(root, str(task_id))
|
|
@@ -7116,7 +7283,11 @@ def set_current_task(root: Path, task_id: str, agent: str, session_file: str | P
|
|
|
7116
7283
|
session["last_seen_stage"] = str(task.get("status") or "PENDING")
|
|
7117
7284
|
session["last_agent"] = agent
|
|
7118
7285
|
write_session(root, session, session_file)
|
|
7119
|
-
|
|
7286
|
+
context = restore_spec_context(root, task_id, task, agent, session_file)
|
|
7287
|
+
snapshot = snapshot_state(root, session_file, session)
|
|
7288
|
+
if context is not None:
|
|
7289
|
+
snapshot["spec_context"] = context
|
|
7290
|
+
return snapshot
|
|
7120
7291
|
|
|
7121
7292
|
|
|
7122
7293
|
def clear_current_task(root: Path, agent: str, session_file: str | Path | None = None) -> dict:
|
|
@@ -7721,7 +7892,10 @@ def claim_task(root: Path, task_id: str, agent: str, session_file: str | Path |
|
|
|
7721
7892
|
}
|
|
7722
7893
|
append_execution_record(root, task_id, claim)
|
|
7723
7894
|
|
|
7895
|
+
context = restore_spec_context(root, task_id, task, agent, session_file)
|
|
7724
7896
|
snapshot = snapshot_state(root, session_file, session)
|
|
7897
|
+
if context is not None:
|
|
7898
|
+
snapshot["spec_context"] = context
|
|
7725
7899
|
snapshot["task_id"] = task_id
|
|
7726
7900
|
snapshot["action"] = action
|
|
7727
7901
|
snapshot["previous_agent"] = previous_agent
|
|
@@ -7771,7 +7945,8 @@ def create_task(
|
|
|
7771
7945
|
write_task(root, task_id, task)
|
|
7772
7946
|
if set_current:
|
|
7773
7947
|
return set_current_task(root, task_id, agent, session_file)
|
|
7774
|
-
|
|
7948
|
+
context = restore_spec_context(root, task_id, task, agent, session_file)
|
|
7949
|
+
return {"task_id": task_id, "task": task, **({"spec_context": context} if context else {})}
|
|
7775
7950
|
|
|
7776
7951
|
|
|
7777
7952
|
def create_task_from_spec(
|
|
@@ -7927,8 +8102,13 @@ def _execute_spec_writeback(
|
|
|
7927
8102
|
action: dict,
|
|
7928
8103
|
idempotency_key: str,
|
|
7929
8104
|
invoke,
|
|
8105
|
+
*, allow_pending_hard_dependencies: bool = False,
|
|
7930
8106
|
) -> dict:
|
|
7931
|
-
inspection, _ = inspect_task_spec(
|
|
8107
|
+
inspection, _ = inspect_task_spec(
|
|
8108
|
+
root, task, allow_pending_hard_dependencies=(
|
|
8109
|
+
allow_pending_hard_dependencies or action.get("kind") == "dependency"
|
|
8110
|
+
),
|
|
8111
|
+
)
|
|
7932
8112
|
source = task["spec_source"]
|
|
7933
8113
|
progress = _writeback_progress(task)
|
|
7934
8114
|
serialized_action = json.dumps(action, ensure_ascii=False, sort_keys=True)
|
|
@@ -8018,7 +8198,11 @@ def _execute_spec_writeback(
|
|
|
8018
8198
|
"execution_revision": execution["execution_revision"],
|
|
8019
8199
|
}
|
|
8020
8200
|
)
|
|
8021
|
-
inspect_task_spec(
|
|
8201
|
+
inspect_task_spec(
|
|
8202
|
+
root, task, allow_pending_hard_dependencies=(
|
|
8203
|
+
allow_pending_hard_dependencies or action.get("kind") == "dependency"
|
|
8204
|
+
),
|
|
8205
|
+
)
|
|
8022
8206
|
progress.update(
|
|
8023
8207
|
{
|
|
8024
8208
|
"last_execution_revision": execution["execution_revision"],
|
|
@@ -8058,8 +8242,11 @@ def writeback_spec_task(
|
|
|
8058
8242
|
agent: str,
|
|
8059
8243
|
task_id: str | None = None,
|
|
8060
8244
|
session_file: str | Path | None = None,
|
|
8245
|
+
*, event_agent: str | None = None,
|
|
8061
8246
|
) -> dict:
|
|
8062
8247
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
8248
|
+
require_spec_context(root, task, agent, session_file)
|
|
8249
|
+
writer_agent = event_agent or agent
|
|
8063
8250
|
if source_task_id not in set(task.get("selected_spec_tasks") or []):
|
|
8064
8251
|
raise StateError("Canonical source task is outside the Harness task selection.")
|
|
8065
8252
|
action = {
|
|
@@ -8069,7 +8256,7 @@ def writeback_spec_task(
|
|
|
8069
8256
|
"summary": summary,
|
|
8070
8257
|
"evidence": evidence,
|
|
8071
8258
|
"idempotency_key": idempotency_key,
|
|
8072
|
-
"agent":
|
|
8259
|
+
"agent": writer_agent,
|
|
8073
8260
|
}
|
|
8074
8261
|
acknowledgment = _execute_spec_writeback(
|
|
8075
8262
|
root,
|
|
@@ -8083,7 +8270,7 @@ def writeback_spec_task(
|
|
|
8083
8270
|
status_value,
|
|
8084
8271
|
summary,
|
|
8085
8272
|
SPEC_WRITEBACK_APP,
|
|
8086
|
-
spec_writeback_agent(
|
|
8273
|
+
spec_writeback_agent(writer_agent),
|
|
8087
8274
|
design_digest,
|
|
8088
8275
|
execution_revision,
|
|
8089
8276
|
evidence=evidence,
|
|
@@ -8108,8 +8295,11 @@ def writeback_spec_step(
|
|
|
8108
8295
|
agent: str,
|
|
8109
8296
|
task_id: str | None = None,
|
|
8110
8297
|
session_file: str | Path | None = None,
|
|
8298
|
+
*, event_agent: str | None = None,
|
|
8111
8299
|
) -> dict:
|
|
8112
8300
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
8301
|
+
require_spec_context(root, task, agent, session_file)
|
|
8302
|
+
writer_agent = event_agent or agent
|
|
8113
8303
|
if source_task_id not in set(task.get("selected_spec_tasks") or []):
|
|
8114
8304
|
raise StateError("Canonical source task is outside the Harness task selection.")
|
|
8115
8305
|
action = {
|
|
@@ -8120,7 +8310,7 @@ def writeback_spec_step(
|
|
|
8120
8310
|
"summary": summary,
|
|
8121
8311
|
"evidence": evidence,
|
|
8122
8312
|
"idempotency_key": idempotency_key,
|
|
8123
|
-
"agent":
|
|
8313
|
+
"agent": writer_agent,
|
|
8124
8314
|
}
|
|
8125
8315
|
acknowledgment = _execute_spec_writeback(
|
|
8126
8316
|
root,
|
|
@@ -8135,7 +8325,7 @@ def writeback_spec_step(
|
|
|
8135
8325
|
status_value,
|
|
8136
8326
|
summary,
|
|
8137
8327
|
SPEC_WRITEBACK_APP,
|
|
8138
|
-
spec_writeback_agent(
|
|
8328
|
+
spec_writeback_agent(writer_agent),
|
|
8139
8329
|
design_digest,
|
|
8140
8330
|
execution_revision,
|
|
8141
8331
|
evidence=evidence,
|
|
@@ -8160,8 +8350,11 @@ def writeback_spec_dependency(
|
|
|
8160
8350
|
agent: str,
|
|
8161
8351
|
task_id: str | None = None,
|
|
8162
8352
|
session_file: str | Path | None = None,
|
|
8353
|
+
*, event_agent: str | None = None,
|
|
8163
8354
|
) -> dict:
|
|
8164
8355
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
8356
|
+
require_spec_context(root, task, agent, session_file, allow_pending_hard_dependencies=True)
|
|
8357
|
+
writer_agent = event_agent or agent
|
|
8165
8358
|
if source_task_id not in set(task.get("selected_spec_tasks") or []):
|
|
8166
8359
|
raise StateError("Canonical source task is outside the Harness task selection.")
|
|
8167
8360
|
action = {
|
|
@@ -8172,7 +8365,7 @@ def writeback_spec_dependency(
|
|
|
8172
8365
|
"summary": summary,
|
|
8173
8366
|
"evidence": evidence,
|
|
8174
8367
|
"idempotency_key": idempotency_key,
|
|
8175
|
-
"agent":
|
|
8368
|
+
"agent": writer_agent,
|
|
8176
8369
|
}
|
|
8177
8370
|
acknowledgment = _execute_spec_writeback(
|
|
8178
8371
|
root,
|
|
@@ -8187,7 +8380,7 @@ def writeback_spec_dependency(
|
|
|
8187
8380
|
status_value,
|
|
8188
8381
|
summary,
|
|
8189
8382
|
SPEC_WRITEBACK_APP,
|
|
8190
|
-
spec_writeback_agent(
|
|
8383
|
+
spec_writeback_agent(writer_agent),
|
|
8191
8384
|
design_digest,
|
|
8192
8385
|
execution_revision,
|
|
8193
8386
|
evidence=evidence,
|
|
@@ -8239,7 +8432,7 @@ def rebind_spec_source(
|
|
|
8239
8432
|
source_path = str(resolved)
|
|
8240
8433
|
path_mode = "absolute"
|
|
8241
8434
|
source.update({"path": source_path, "path_mode": path_mode})
|
|
8242
|
-
inspect_task_spec(root, task)
|
|
8435
|
+
inspect_task_spec(root, task, allow_pending_hard_dependencies=True)
|
|
8243
8436
|
task["last_agent"] = agent
|
|
8244
8437
|
write_task(root, resolved_task_id, task)
|
|
8245
8438
|
snapshot = snapshot_state(root, session_file, session)
|
|
@@ -8254,6 +8447,7 @@ def reconcile_local_result_evidence(
|
|
|
8254
8447
|
agent: str,
|
|
8255
8448
|
session_file: str | Path | None,
|
|
8256
8449
|
) -> tuple[int, list[str]]:
|
|
8450
|
+
require_spec_context(root, task, agent, session_file)
|
|
8257
8451
|
plan = latest_execution_plan(root, resolved_task_id)
|
|
8258
8452
|
if not isinstance(plan, dict):
|
|
8259
8453
|
return 0, []
|
|
@@ -8554,9 +8748,10 @@ def reconcile_spec_execution(
|
|
|
8554
8748
|
affected_task_ids,
|
|
8555
8749
|
str(action.get("summary") or "Reconciled Canonical Spec design sync"),
|
|
8556
8750
|
str(action.get("idempotency_key") or ""),
|
|
8557
|
-
|
|
8751
|
+
agent,
|
|
8558
8752
|
resolved_task_id,
|
|
8559
8753
|
session_file,
|
|
8754
|
+
event_agent=str(action.get("agent") or agent),
|
|
8560
8755
|
)
|
|
8561
8756
|
result["action"] = "reconcile-spec-execution"
|
|
8562
8757
|
result["reconciled"] = True
|
|
@@ -8586,12 +8781,34 @@ def reconcile_spec_execution(
|
|
|
8586
8781
|
"summary": str(action.get("summary") or "Reconciled shared Spec writeback"),
|
|
8587
8782
|
"evidence": action.get("evidence") if isinstance(action.get("evidence"), list) else [],
|
|
8588
8783
|
"idempotency_key": str(action.get("idempotency_key") or ""),
|
|
8589
|
-
"agent":
|
|
8784
|
+
"agent": agent,
|
|
8785
|
+
"event_agent": str(action.get("agent") or agent),
|
|
8590
8786
|
"task_id": resolved_task_id,
|
|
8591
8787
|
"session_file": session_file,
|
|
8592
8788
|
}
|
|
8593
8789
|
if not common["idempotency_key"]:
|
|
8594
8790
|
raise StateError("Pending Canonical Spec writeback has no idempotency key.")
|
|
8791
|
+
cancellation_suffix = {"blocked": "close-blocked", "cancelled": "cancel"}.get(action.get("status"))
|
|
8792
|
+
source_task_id = str(action.get("source_task_id") or "")
|
|
8793
|
+
if (
|
|
8794
|
+
kind == "task" and cancellation_suffix
|
|
8795
|
+
and source_task_id in (task.get("selected_spec_tasks") or [])
|
|
8796
|
+
and common["idempotency_key"] == f"{resolved_task_id}:{source_task_id}:{cancellation_suffix}"
|
|
8797
|
+
):
|
|
8798
|
+
# 只恢复已经登记的取消链;沿用原动作,不能借恢复入口新增实施进度。
|
|
8799
|
+
acknowledgment = _execute_spec_writeback(
|
|
8800
|
+
root, resolved_task_id, task, action, common["idempotency_key"],
|
|
8801
|
+
lambda design_digest, execution_revision: record_task_status(
|
|
8802
|
+
stored_spec_path(root, task), source_task_id, str(action["status"]),
|
|
8803
|
+
common["summary"], SPEC_WRITEBACK_APP, spec_writeback_agent(common["event_agent"]),
|
|
8804
|
+
design_digest, execution_revision, evidence=common["evidence"],
|
|
8805
|
+
run_id=resolved_task_id, idempotency_key=common["idempotency_key"],
|
|
8806
|
+
),
|
|
8807
|
+
allow_pending_hard_dependencies=True,
|
|
8808
|
+
)
|
|
8809
|
+
result = snapshot_state(root, session_file, session)
|
|
8810
|
+
result.update({"action": "reconcile-spec-execution", "reconciled": True, "spec_writeback": acknowledgment})
|
|
8811
|
+
return result
|
|
8595
8812
|
if kind == "task":
|
|
8596
8813
|
result = writeback_spec_task(
|
|
8597
8814
|
source_task_id=str(action.get("source_task_id") or ""),
|
|
@@ -8627,6 +8844,7 @@ def sync_spec_design_state(
|
|
|
8627
8844
|
agent: str,
|
|
8628
8845
|
task_id: str | None = None,
|
|
8629
8846
|
session_file: str | Path | None = None,
|
|
8847
|
+
event_agent: str | None = None,
|
|
8630
8848
|
) -> dict:
|
|
8631
8849
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
8632
8850
|
source = task.get("spec_source")
|
|
@@ -8634,6 +8852,21 @@ def sync_spec_design_state(
|
|
|
8634
8852
|
raise StateError("Current task is not backed by a Canonical Spec.")
|
|
8635
8853
|
spec_path = stored_spec_path(root, task)
|
|
8636
8854
|
requested_task_ids = sorted(set(affected_task_ids))
|
|
8855
|
+
writer_agent = event_agent or agent
|
|
8856
|
+
change = task.get("spec_change")
|
|
8857
|
+
if isinstance(change, dict) and change.get("affected_task_ids") != requested_task_ids:
|
|
8858
|
+
raise StateError("Design sync must include exactly the confirmed affected tasks.")
|
|
8859
|
+
if isinstance(change, dict) and any(
|
|
8860
|
+
change.get(field) != source.get(field) for field in ("spec_id", "revision", "design_sha256")
|
|
8861
|
+
):
|
|
8862
|
+
raise StateError("Confirmed Spec change baseline no longer matches the bound design.")
|
|
8863
|
+
|
|
8864
|
+
def validate_change_event(event: dict) -> None:
|
|
8865
|
+
if isinstance(change, dict) and (
|
|
8866
|
+
event.get("from_design_revision") != change.get("revision")
|
|
8867
|
+
or event.get("to_design_revision") != int(change["revision"]) + 1
|
|
8868
|
+
):
|
|
8869
|
+
raise StateError("Old design sync cannot resolve the current confirmed Spec change.")
|
|
8637
8870
|
|
|
8638
8871
|
def current_execution_envelope() -> dict:
|
|
8639
8872
|
try:
|
|
@@ -8644,6 +8877,9 @@ def sync_spec_design_state(
|
|
|
8644
8877
|
raise StateError(f"Cannot inspect pre-sync Canonical execution state: {exc}") from exc
|
|
8645
8878
|
if not isinstance(execution, dict):
|
|
8646
8879
|
raise StateError("Canonical Spec shared execution is missing before sync-design.")
|
|
8880
|
+
for event in execution.get("events", []):
|
|
8881
|
+
if isinstance(event, dict) and event.get("idempotency_key") == idempotency_key:
|
|
8882
|
+
validate_change_event(event)
|
|
8647
8883
|
if execution.get("design_sha256") != source.get("design_sha256"):
|
|
8648
8884
|
matching_events = [
|
|
8649
8885
|
event
|
|
@@ -8667,7 +8903,7 @@ def sync_spec_design_state(
|
|
|
8667
8903
|
"affected_task_ids": requested_task_ids,
|
|
8668
8904
|
"summary": summary,
|
|
8669
8905
|
"idempotency_key": idempotency_key,
|
|
8670
|
-
"agent":
|
|
8906
|
+
"agent": writer_agent,
|
|
8671
8907
|
}
|
|
8672
8908
|
serialized_pending_action = json.dumps(
|
|
8673
8909
|
pending_action, ensure_ascii=False, sort_keys=True
|
|
@@ -8699,7 +8935,7 @@ def sync_spec_design_state(
|
|
|
8699
8935
|
requested_task_ids,
|
|
8700
8936
|
summary,
|
|
8701
8937
|
SPEC_WRITEBACK_APP,
|
|
8702
|
-
spec_writeback_agent(
|
|
8938
|
+
spec_writeback_agent(writer_agent),
|
|
8703
8939
|
str(source.get("design_sha256")),
|
|
8704
8940
|
execution_revision,
|
|
8705
8941
|
run_id=resolved_task_id,
|
|
@@ -8745,6 +8981,10 @@ def sync_spec_design_state(
|
|
|
8745
8981
|
raise StateError(f"Cannot synchronize Canonical Spec design: {exc}") from exc
|
|
8746
8982
|
if inspection.get("spec_id") != source.get("spec_id"):
|
|
8747
8983
|
raise StateError("Synchronized Canonical Spec identity changed unexpectedly.")
|
|
8984
|
+
event = _spec_event(execution, idempotency_key)
|
|
8985
|
+
validate_change_event(event)
|
|
8986
|
+
if isinstance(change, dict) and inspection.get("revision") != int(change["revision"]) + 1:
|
|
8987
|
+
raise StateError("Synchronized design does not match the confirmed Spec change revision.")
|
|
8748
8988
|
binding_was_synchronized = (
|
|
8749
8989
|
source.get("revision") == inspection.get("revision")
|
|
8750
8990
|
and source.get("design_sha256") == inspection.get("design_sha256")
|
|
@@ -8757,22 +8997,32 @@ def sync_spec_design_state(
|
|
|
8757
8997
|
"execution_revision": execution["execution_revision"],
|
|
8758
8998
|
}
|
|
8759
8999
|
)
|
|
8760
|
-
event = _spec_event(execution, idempotency_key)
|
|
8761
9000
|
if not binding_was_synchronized:
|
|
8762
9001
|
reset_task_ids = set(event.get("task_ids", []))
|
|
8763
|
-
|
|
9002
|
+
new_dependencies = {
|
|
9003
|
+
(item["source_task_id"], item["task_id"]): item
|
|
9004
|
+
for item in inspection["dependency_edges"]
|
|
9005
|
+
}
|
|
9006
|
+
preserved_evidence: dict[str, str] = {}
|
|
8764
9007
|
for dependency in task.get("spec_dependency_evidence", []):
|
|
8765
9008
|
if not isinstance(dependency, dict):
|
|
8766
9009
|
continue
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
9010
|
+
edge = (dependency.get("source_task_id"), dependency.get("task_id"))
|
|
9011
|
+
current = new_dependencies.get(edge)
|
|
9012
|
+
if (
|
|
9013
|
+
edge[0] not in reset_task_ids and edge[1] not in reset_task_ids
|
|
9014
|
+
and current is not None
|
|
9015
|
+
and dependency.get("dependency_type") == current.get("dependency_type")
|
|
9016
|
+
and dependency.get("required_evidence") == current.get("required_evidence")
|
|
9017
|
+
and dependency.get("status") == "satisfied"
|
|
9018
|
+
and is_non_empty_string(dependency.get("evidence"))
|
|
9019
|
+
):
|
|
9020
|
+
preserved_evidence[f"{edge[0]}->{edge[1]}"] = str(dependency["evidence"])
|
|
9021
|
+
task["spec_dependency_evidence"] = select_tasks(
|
|
9022
|
+
inspection, task["selected_spec_tasks"], preserved_evidence,
|
|
9023
|
+
allow_pending_hard_dependencies=True,
|
|
9024
|
+
)["dependency_records"]
|
|
9025
|
+
inspect_task_spec(root, task, allow_pending_hard_dependencies=True)
|
|
8776
9026
|
progress.update(
|
|
8777
9027
|
{
|
|
8778
9028
|
"last_execution_revision": execution["execution_revision"],
|
|
@@ -8783,6 +9033,8 @@ def sync_spec_design_state(
|
|
|
8783
9033
|
}
|
|
8784
9034
|
)
|
|
8785
9035
|
progress.pop("pending_action", None)
|
|
9036
|
+
task.pop("spec_change", None)
|
|
9037
|
+
task.pop("spec_context", None)
|
|
8786
9038
|
if task.get("status") not in {"INIT", "ANALYSIS"}:
|
|
8787
9039
|
cleanup_verification_checkpoint(root, resolved_task_id, task)
|
|
8788
9040
|
task["status"] = "ANALYSIS"
|
|
@@ -9147,7 +9399,7 @@ def cancel_shared_tasks(
|
|
|
9147
9399
|
reason: str,
|
|
9148
9400
|
agent: str,
|
|
9149
9401
|
) -> None:
|
|
9150
|
-
inspection, _ = inspect_task_spec(root, task)
|
|
9402
|
+
inspection, _ = inspect_task_spec(root, task, allow_pending_hard_dependencies=True)
|
|
9151
9403
|
snapshots = _selected_execution_snapshots(inspection, task)
|
|
9152
9404
|
for source_task_id in task.get("selected_spec_tasks") or []:
|
|
9153
9405
|
current = snapshots.get(str(source_task_id), {}).get("status")
|
|
@@ -9182,6 +9434,7 @@ def cancel_shared_tasks(
|
|
|
9182
9434
|
run_id=harness_task_id,
|
|
9183
9435
|
idempotency_key=blocked_key,
|
|
9184
9436
|
),
|
|
9437
|
+
allow_pending_hard_dependencies=True,
|
|
9185
9438
|
)
|
|
9186
9439
|
cancel_key = f"{harness_task_id}:{source_task_id}:cancel"
|
|
9187
9440
|
cancel_action = {
|
|
@@ -9211,6 +9464,7 @@ def cancel_shared_tasks(
|
|
|
9211
9464
|
run_id=harness_task_id,
|
|
9212
9465
|
idempotency_key=cancel_key,
|
|
9213
9466
|
),
|
|
9467
|
+
allow_pending_hard_dependencies=True,
|
|
9214
9468
|
)
|
|
9215
9469
|
|
|
9216
9470
|
|
|
@@ -9228,7 +9482,7 @@ def satisfy_spec_dependency(
|
|
|
9228
9482
|
raise StateError("Spec dependency evidence cannot change after MEMORY begins.")
|
|
9229
9483
|
if not is_non_empty_string(evidence):
|
|
9230
9484
|
raise StateError("Spec dependency evidence must be non-empty.")
|
|
9231
|
-
|
|
9485
|
+
require_spec_context(root, task, agent, session_file, allow_pending_hard_dependencies=True)
|
|
9232
9486
|
records = task.get("spec_dependency_evidence")
|
|
9233
9487
|
if not isinstance(records, list):
|
|
9234
9488
|
raise StateError("Current task is not backed by Canonical Spec dependency metadata.")
|
|
@@ -9563,6 +9817,8 @@ def request_transition(
|
|
|
9563
9817
|
if stage not in VALID_TRANSITIONS:
|
|
9564
9818
|
raise StateError(f"Unknown stage: {stage}")
|
|
9565
9819
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
9820
|
+
if stage in {"IMPLEMENT", "QUALITY", "MEMORY", "COMPLETE"}:
|
|
9821
|
+
require_spec_context(root, task, agent, session_file)
|
|
9566
9822
|
previous = str(task.get("status") or "idle")
|
|
9567
9823
|
task_type = str(task.get("type") or "")
|
|
9568
9824
|
approval_mode = resolve_approval_mode(root, session)[2]
|
|
@@ -9663,6 +9919,8 @@ def apply_transition(
|
|
|
9663
9919
|
if stage not in VALID_TRANSITIONS:
|
|
9664
9920
|
raise StateError(f"Unknown stage: {stage}")
|
|
9665
9921
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
9922
|
+
if stage in {"IMPLEMENT", "QUALITY", "MEMORY", "COMPLETE"}:
|
|
9923
|
+
require_spec_context(root, task, agent, session_file)
|
|
9666
9924
|
|
|
9667
9925
|
previous = str(task.get("status") or "idle")
|
|
9668
9926
|
task_type = str(task.get("type") or "")
|
|
@@ -9779,6 +10037,8 @@ def auto_transition(
|
|
|
9779
10037
|
session_file: str | Path | None = None,
|
|
9780
10038
|
) -> dict:
|
|
9781
10039
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
10040
|
+
if stage in {"IMPLEMENT", "QUALITY", "MEMORY", "COMPLETE"}:
|
|
10041
|
+
require_spec_context(root, task, agent, session_file)
|
|
9782
10042
|
previous = str(task.get("status") or "idle")
|
|
9783
10043
|
task_type = str(task.get("type") or "")
|
|
9784
10044
|
approval_mode = resolve_approval_mode(root, session)[2]
|
|
@@ -9857,6 +10117,8 @@ def confirm_transition(
|
|
|
9857
10117
|
approval_mode = resolve_approval_mode(root, session)[2]
|
|
9858
10118
|
source = str(pending.get("from") or "")
|
|
9859
10119
|
target = str(pending.get("to") or "")
|
|
10120
|
+
if target in {"IMPLEMENT", "QUALITY", "MEMORY", "COMPLETE"}:
|
|
10121
|
+
require_spec_context(root, task, agent, session_file)
|
|
9860
10122
|
if source != previous:
|
|
9861
10123
|
raise StateError(
|
|
9862
10124
|
f"Pending transition source {source or 'missing'} does not match current stage {previous}."
|
|
@@ -9930,6 +10192,7 @@ def memory_short_complete(
|
|
|
9930
10192
|
session_file: str | Path | None = None,
|
|
9931
10193
|
) -> dict:
|
|
9932
10194
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
10195
|
+
require_spec_context(root, task, agent, session_file)
|
|
9933
10196
|
if task.get("status") != "MEMORY":
|
|
9934
10197
|
raise StateError("Short-memory progress can only be recorded during MEMORY.")
|
|
9935
10198
|
if not memory_file.strip():
|
|
@@ -10057,6 +10320,7 @@ def memory_complete(
|
|
|
10057
10320
|
if action not in {"no-op", "distill"}:
|
|
10058
10321
|
raise StateError(f"Unknown memory action: {action}")
|
|
10059
10322
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
10323
|
+
require_spec_context(root, task, agent, session_file)
|
|
10060
10324
|
if task.get("status") != "MEMORY":
|
|
10061
10325
|
raise StateError("Memory completion can only be recorded during MEMORY.")
|
|
10062
10326
|
progress = task.get("memory_progress")
|
|
@@ -10286,6 +10550,16 @@ def main() -> int:
|
|
|
10286
10550
|
rebind_spec.add_argument("--agent", required=True)
|
|
10287
10551
|
rebind_spec.add_argument("--task-id")
|
|
10288
10552
|
|
|
10553
|
+
resume_spec = subcommands.add_parser("resume-spec-context", parents=[common])
|
|
10554
|
+
resume_spec.add_argument("--agent", required=True)
|
|
10555
|
+
resume_spec.add_argument("--task-id")
|
|
10556
|
+
|
|
10557
|
+
begin_change = subcommands.add_parser("begin-spec-change", parents=[common])
|
|
10558
|
+
begin_change.add_argument("--affected-task", action="append", required=True)
|
|
10559
|
+
begin_change.add_argument("--summary", required=True)
|
|
10560
|
+
begin_change.add_argument("--agent", required=True)
|
|
10561
|
+
begin_change.add_argument("--task-id")
|
|
10562
|
+
|
|
10289
10563
|
writeback_task = subcommands.add_parser("writeback-spec-task", parents=[common])
|
|
10290
10564
|
writeback_task.add_argument("--spec-task", required=True)
|
|
10291
10565
|
writeback_task.add_argument(
|
|
@@ -10614,6 +10888,9 @@ def main() -> int:
|
|
|
10614
10888
|
command_lock = acquire_session_command_lock(
|
|
10615
10889
|
root, resolve_session_path(root, session_file)
|
|
10616
10890
|
)
|
|
10891
|
+
if command == "evidence-fingerprints":
|
|
10892
|
+
_, _, current = resolve_current_task(root, getattr(args, "task_id", None), session_file)
|
|
10893
|
+
require_spec_context(root, current, agent, session_file)
|
|
10617
10894
|
if command == "snapshot":
|
|
10618
10895
|
emit(snapshot_state(root, session_file))
|
|
10619
10896
|
elif command == "inspect-dev-spec":
|
|
@@ -10696,6 +10973,15 @@ def main() -> int:
|
|
|
10696
10973
|
session_file,
|
|
10697
10974
|
)
|
|
10698
10975
|
)
|
|
10976
|
+
elif command == "resume-spec-context":
|
|
10977
|
+
emit(attach_status_context(
|
|
10978
|
+
root, resume_spec_context(root, agent, args.task_id, session_file), agent, session_file
|
|
10979
|
+
))
|
|
10980
|
+
elif command == "begin-spec-change":
|
|
10981
|
+
emit(attach_status_context(
|
|
10982
|
+
root, begin_spec_change(root, args.affected_task, args.summary, agent, args.task_id, session_file),
|
|
10983
|
+
agent, session_file,
|
|
10984
|
+
))
|
|
10699
10985
|
elif command == "writeback-spec-task":
|
|
10700
10986
|
emit(
|
|
10701
10987
|
attach_status_context(
|