easy-coding-harness 1.0.1 → 1.1.0-beta.0
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 +8 -0
- package/README.md +6 -5
- package/dist/cli.js +1 -30
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/agents/ec-implementer.md +4 -0
- package/templates/claude/agents/ec-reviewer.md +6 -0
- package/templates/codex/agents/ec-implementer.toml +4 -0
- package/templates/codex/agents/ec-reviewer.toml +6 -0
- package/templates/common/skills/ec-analysis/SKILL.md +27 -64
- package/templates/common/skills/ec-config/SKILL.md +5 -6
- package/templates/common/skills/ec-implementing/SKILL.md +22 -7
- package/templates/common/skills/ec-quality/SKILL.md +38 -22
- package/templates/common/skills/ec-workflow/SKILL.md +16 -14
- package/templates/main-constraint/AGENTS.md.tpl +8 -2
- package/templates/main-constraint/CLAUDE.md.tpl +8 -2
- package/templates/qoder/agents/ec-implementer.md +4 -0
- package/templates/qoder/agents/ec-reviewer.md +6 -0
- package/templates/shared-hooks/easy_coding_inputs.py +305 -0
- package/templates/shared-hooks/easy_coding_state.py +328 -329
|
@@ -16,6 +16,10 @@ from datetime import datetime, timezone
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
import sys
|
|
18
18
|
|
|
19
|
+
from easy_coding_inputs import (
|
|
20
|
+
evidence_operation, memo, digest, input_spec, capture, changed_inputs, command_covers,
|
|
21
|
+
)
|
|
22
|
+
|
|
19
23
|
from easy_dev_spec import (
|
|
20
24
|
EasyDevSpecError,
|
|
21
25
|
inspect_manifest,
|
|
@@ -1825,13 +1829,42 @@ def is_valid_review_finding(value: object) -> bool:
|
|
|
1825
1829
|
)
|
|
1826
1830
|
|
|
1827
1831
|
|
|
1832
|
+
def gate_identity(record: dict) -> tuple:
|
|
1833
|
+
return (str(record.get("source_task_id") or ""), str(record.get("unit_id") or ""),
|
|
1834
|
+
str(record.get("dimension") or record.get("check") or ""),
|
|
1835
|
+
str(record.get("review_scope") or record.get("coverage_scope") or ""))
|
|
1836
|
+
|
|
1837
|
+
|
|
1838
|
+
def failure_label(record: dict) -> str:
|
|
1839
|
+
_, unit, name, scope = gate_identity(record)
|
|
1840
|
+
label = f"{record['type']}:{name}"
|
|
1841
|
+
if scope:
|
|
1842
|
+
label += f":{scope}"
|
|
1843
|
+
if unit:
|
|
1844
|
+
label += f":unit={unit}"
|
|
1845
|
+
return label
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
def require_unit_evidence(plan: dict, records: list[dict], label: str, dimensions: int = 0) -> None:
|
|
1849
|
+
if not any(record.get("unit_id") for record in records):
|
|
1850
|
+
return
|
|
1851
|
+
for unit in plan.get("units", []):
|
|
1852
|
+
applicable = [record for record in records
|
|
1853
|
+
if record.get("unit_id") == unit["id"] or
|
|
1854
|
+
(not record.get("unit_id") and
|
|
1855
|
+
(not record.get("source_task_id") or
|
|
1856
|
+
record["source_task_id"] == unit.get("source_task_id")))]
|
|
1857
|
+
if not applicable or (dimensions and len({r.get("dimension") for r in applicable}) < dimensions):
|
|
1858
|
+
raise StateError(f"{label} evidence does not cover Unit {unit['id']} at the required depth.")
|
|
1859
|
+
|
|
1860
|
+
|
|
1828
1861
|
def validate_quality_gate_record_schemas(
|
|
1829
1862
|
review_records: list[dict], verification_records: list[dict]
|
|
1830
1863
|
) -> None:
|
|
1831
1864
|
latest_reviews: dict[tuple[str, str], dict] = {}
|
|
1832
1865
|
for index, record in enumerate(review_records):
|
|
1833
1866
|
dimension = str(record.get("dimension") or f"<missing-{index}>")
|
|
1834
|
-
latest_reviews[(
|
|
1867
|
+
latest_reviews[gate_identity(record)] = record
|
|
1835
1868
|
for record in latest_reviews.values():
|
|
1836
1869
|
findings = record.get("findings")
|
|
1837
1870
|
if (
|
|
@@ -1859,13 +1892,7 @@ def validate_quality_gate_record_schemas(
|
|
|
1859
1892
|
latest_verifications: dict[tuple[str, str, str], dict] = {}
|
|
1860
1893
|
for index, record in enumerate(verification_records):
|
|
1861
1894
|
check = str(record.get("check") or f"<missing-{index}>")
|
|
1862
|
-
latest_verifications[
|
|
1863
|
-
(
|
|
1864
|
-
str(record.get("source_task_id") or ""),
|
|
1865
|
-
check,
|
|
1866
|
-
str(record.get("coverage_scope") or ""),
|
|
1867
|
-
)
|
|
1868
|
-
] = record
|
|
1895
|
+
latest_verifications[gate_identity(record)] = record
|
|
1869
1896
|
for record in latest_verifications.values():
|
|
1870
1897
|
applicable = record.get("applicable") is not False
|
|
1871
1898
|
if (
|
|
@@ -2249,6 +2276,34 @@ def require_spec_context(
|
|
|
2249
2276
|
raise StateError("Current session must consume the bound Canonical Spec via resume-spec-context before advancing.")
|
|
2250
2277
|
|
|
2251
2278
|
|
|
2279
|
+
def refresh_correction_plan(root: Path, task_id: str, task: dict, inspection: dict) -> None:
|
|
2280
|
+
plan = latest_execution_plan(root, task_id)
|
|
2281
|
+
if plan is None:
|
|
2282
|
+
raise StateError("A correction must preserve its original execution plan.")
|
|
2283
|
+
selection = select_tasks(inspection, task["selected_spec_tasks"], allow_pending_hard_dependencies=True)
|
|
2284
|
+
steps = {s["step_id"]: s for s in selection["selected_steps"]}
|
|
2285
|
+
changes = {c["change_id"]: c for c in selection["selected_changes"]}
|
|
2286
|
+
tests = {t["test_id"]: t for t in selection["selected_tests"]}
|
|
2287
|
+
correction = task["correction"]
|
|
2288
|
+
plan["spec_design_sha256"] = inspection["design_sha256"]
|
|
2289
|
+
for unit in plan["units"]:
|
|
2290
|
+
if unit["id"] not in correction["unit_ids"]:
|
|
2291
|
+
continue
|
|
2292
|
+
source_steps = [steps[s] for s in unit["source_step_ids"] if s in steps]
|
|
2293
|
+
selected_changes = [changes[c] for s in source_steps for c in s.get("change_ids", [])]
|
|
2294
|
+
original_files = set(unit["files"])
|
|
2295
|
+
mapped_files = {c["path"] for c in selected_changes}
|
|
2296
|
+
if not mapped_files <= original_files:
|
|
2297
|
+
raise StateError("Source design expands the correction scope; analyze the new requirement.")
|
|
2298
|
+
unit["source_step_ids"] = [s["step_id"] for s in source_steps]
|
|
2299
|
+
unit["files"] = sorted(mapped_files | (original_files & set(correction["files"])))
|
|
2300
|
+
unit["symbols"] = sorted({symbol for c in selected_changes for symbol in c.get("symbols", [])})
|
|
2301
|
+
unit["test_commands"] = sorted({tests[t]["command"] for s in source_steps for t in s.get("test_ids", [])})
|
|
2302
|
+
unit["acceptance_criteria"] = [correction["summary"]]
|
|
2303
|
+
unit["contracts"] = [correction["summary"]]
|
|
2304
|
+
append_execution_record(root, task_id, plan)
|
|
2305
|
+
|
|
2306
|
+
|
|
2252
2307
|
def begin_spec_change(
|
|
2253
2308
|
root: Path, affected_task_ids: list[str], summary: str, agent: str,
|
|
2254
2309
|
task_id: str | None = None, session_file: str | Path | None = None,
|
|
@@ -2375,7 +2430,7 @@ def is_valid_spec_execution_plan(root: Path, task: dict, plan: object) -> bool:
|
|
|
2375
2430
|
if (
|
|
2376
2431
|
not step_change_ids.issubset(change_by_id)
|
|
2377
2432
|
or not step_test_ids.issubset(test_by_id)
|
|
2378
|
-
or set(unit.get("files", [])) != step_files
|
|
2433
|
+
or (set(unit.get("files", [])) - set(task.get("correction", {}).get("files", []))) != (step_files - set(task.get("correction", {}).get("files", [])))
|
|
2379
2434
|
or set(unit["symbols"]) != step_symbols
|
|
2380
2435
|
or not set(unit["test_commands"]).issuperset(step_commands)
|
|
2381
2436
|
):
|
|
@@ -2397,9 +2452,10 @@ def is_valid_spec_execution_plan(root: Path, task: dict, plan: object) -> bool:
|
|
|
2397
2452
|
steps = covered_steps[source_task_id]
|
|
2398
2453
|
if len(steps) != len(set(steps)) or set(steps) != set(source_task.get("step_ids", [])):
|
|
2399
2454
|
return False
|
|
2400
|
-
|
|
2455
|
+
restoration_files = set(task.get("correction", {}).get("files", []))
|
|
2456
|
+
if covered_files[source_task_id] - restoration_files != {
|
|
2401
2457
|
str(change["path"]) for change in changes_by_task[source_task_id]
|
|
2402
|
-
}:
|
|
2458
|
+
} - restoration_files:
|
|
2403
2459
|
return False
|
|
2404
2460
|
if covered_symbols[source_task_id] != {
|
|
2405
2461
|
str(symbol)
|
|
@@ -2545,7 +2601,7 @@ def has_valid_execution_plan(root: Path, task_id: str) -> bool:
|
|
|
2545
2601
|
return False
|
|
2546
2602
|
if isinstance(record, dict) and record.get("type") == "plan":
|
|
2547
2603
|
latest_plan = record
|
|
2548
|
-
elif isinstance(record, dict) and record.get("type") == "spec-design-sync":
|
|
2604
|
+
elif isinstance(record, dict) and record.get("type") == "spec-design-sync" and not record.get("preserve_plan"):
|
|
2549
2605
|
latest_plan = None
|
|
2550
2606
|
except OSError:
|
|
2551
2607
|
return False
|
|
@@ -2583,7 +2639,7 @@ def latest_execution_plan(root: Path, task_id: str) -> dict | None:
|
|
|
2583
2639
|
for record in execution_records(root, task_id):
|
|
2584
2640
|
if record.get("type") == "plan":
|
|
2585
2641
|
latest = record
|
|
2586
|
-
elif record.get("type") == "spec-design-sync":
|
|
2642
|
+
elif record.get("type") == "spec-design-sync" and not record.get("preserve_plan"):
|
|
2587
2643
|
latest = None
|
|
2588
2644
|
if latest is None or not is_valid_execution_plan(latest, allow_empty_files=True):
|
|
2589
2645
|
return None
|
|
@@ -3076,220 +3132,165 @@ def tdd_infrastructure_fingerprint(repositories: set[Path]) -> str:
|
|
|
3076
3132
|
return digest.hexdigest()
|
|
3077
3133
|
|
|
3078
3134
|
|
|
3135
|
+
def check_identity(record: dict) -> tuple:
|
|
3136
|
+
return tuple(str(record.get(key) or "") for key in (
|
|
3137
|
+
"type", "unit_id", "source_task_id", "dimension", "review_scope", "check", "check_type", "command", "coverage_scope"
|
|
3138
|
+
))
|
|
3139
|
+
|
|
3140
|
+
|
|
3141
|
+
def prepare_check(root: Path, task_id: str, task: dict, descriptor: dict, agent: str) -> dict:
|
|
3142
|
+
if task.get("status") not in {"IMPLEMENT", "QUALITY"}:
|
|
3143
|
+
raise StateError("Checks belong to IMPLEMENT or QUALITY.")
|
|
3144
|
+
if descriptor.get("type") not in {"review", "verify"}:
|
|
3145
|
+
raise StateError("A check must identify type review or verify.")
|
|
3146
|
+
if descriptor.get("type") == "verify" and not is_non_empty_string(descriptor.get("command")):
|
|
3147
|
+
raise StateError("Verification must identify the actual command before execution.")
|
|
3148
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3149
|
+
if descriptor.get("unit_id"):
|
|
3150
|
+
owner = next((u for u in plan.get("units", []) if u["id"] == descriptor["unit_id"]), None)
|
|
3151
|
+
if owner is None:
|
|
3152
|
+
raise StateError("Check unit does not belong to the current plan.")
|
|
3153
|
+
for field in ("repo_id", "source_task_id"):
|
|
3154
|
+
if field in owner:
|
|
3155
|
+
descriptor[field] = owner[field]
|
|
3156
|
+
inputs = capture(input_spec(root, task, plan, descriptor))
|
|
3157
|
+
records = execution_records(root, task_id)
|
|
3158
|
+
previous = next((r for r in reversed(records)
|
|
3159
|
+
if check_identity(r) == check_identity(descriptor)), None)
|
|
3160
|
+
if previous and previous.get("passed") is True and previous.get("inputs") == inputs:
|
|
3161
|
+
return {"reusable": True, "evidence_index": records.index(previous),
|
|
3162
|
+
"input_signature": inputs["signature"], "changed_inputs": []}
|
|
3163
|
+
prepared_id = digest([descriptor, inputs["signature"]])
|
|
3164
|
+
if not any(r.get("prepared_id") == prepared_id and r.get("type") == "check-inputs" for r in records):
|
|
3165
|
+
append_execution_record(root, task_id, {
|
|
3166
|
+
"type": "check-inputs", "prepared_id": prepared_id, "descriptor": descriptor,
|
|
3167
|
+
"inputs": inputs, "timestamp": now_iso(), "agent": agent,
|
|
3168
|
+
})
|
|
3169
|
+
return {"reusable": False, "prepared_id": prepared_id,
|
|
3170
|
+
"input_signature": inputs["signature"],
|
|
3171
|
+
"changed_inputs": changed_inputs(previous["inputs"], inputs)
|
|
3172
|
+
if previous and isinstance(previous.get("inputs"), dict) else ["no matching input-bound evidence"]}
|
|
3173
|
+
|
|
3174
|
+
|
|
3175
|
+
def record_check(root: Path, task_id: str, task: dict, prepared_id: str, result: dict, agent: str) -> dict:
|
|
3176
|
+
prepared = next((r for r in reversed(execution_records(root, task_id))
|
|
3177
|
+
if r.get("type") == "check-inputs" and r.get("prepared_id") == prepared_id), None)
|
|
3178
|
+
if prepared is None:
|
|
3179
|
+
raise StateError("Prepare the check before executing it.")
|
|
3180
|
+
descriptor = prepared["descriptor"]
|
|
3181
|
+
current = capture(input_spec(root, task, latest_execution_plan(root, task_id) or {}, descriptor))
|
|
3182
|
+
changes = changed_inputs(prepared["inputs"], current)
|
|
3183
|
+
if changes:
|
|
3184
|
+
raise StateError("Check inputs changed during execution: " + "; ".join(changes))
|
|
3185
|
+
if type(result.get("passed")) is not bool:
|
|
3186
|
+
raise StateError("Check result must include passed.")
|
|
3187
|
+
if descriptor["type"] == "verify" and result.get("applicable") is not False:
|
|
3188
|
+
if type(result.get("exit_code")) is not int or result["passed"] != (result["exit_code"] == 0):
|
|
3189
|
+
raise StateError("Verification passed must agree with its real exit_code.")
|
|
3190
|
+
context = None
|
|
3191
|
+
if task.get("status") == "QUALITY":
|
|
3192
|
+
context = ensure_quality_attempt_context(root, task_id, task, agent, persist=True)
|
|
3193
|
+
elif task.get("status") != "IMPLEMENT":
|
|
3194
|
+
raise StateError("Record checks only during implementation or quality.")
|
|
3195
|
+
record = {
|
|
3196
|
+
**result, **descriptor, **evidence_fingerprints(root, task_id),
|
|
3197
|
+
"inputs": current, "prepared_id": prepared_id, "timestamp": now_iso(),
|
|
3198
|
+
"quality_attempt": context["attempt"] if context else 0,
|
|
3199
|
+
}
|
|
3200
|
+
append_execution_record(root, task_id, record)
|
|
3201
|
+
return {"recorded": True, "passed": record["passed"], "input_signature": current["signature"]}
|
|
3202
|
+
|
|
3203
|
+
|
|
3204
|
+
def carry_forward_scoped_evidence(root: Path, task_id: str, task: dict, context: dict) -> None:
|
|
3205
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3206
|
+
latest = {}
|
|
3207
|
+
for index, record in enumerate(execution_records(root, task_id)):
|
|
3208
|
+
if record.get("type") in {"review", "verify"}:
|
|
3209
|
+
latest[check_identity(record)] = (index, record)
|
|
3210
|
+
for index, record in latest.values():
|
|
3211
|
+
if record.get("passed") is not True or not isinstance(record.get("inputs"), dict):
|
|
3212
|
+
continue
|
|
3213
|
+
if any(f.get("severity") == "error" for f in record.get("findings", [])):
|
|
3214
|
+
continue
|
|
3215
|
+
if record.get("unit_id") and not any(u["id"] == record["unit_id"] for u in plan.get("units", [])):
|
|
3216
|
+
continue
|
|
3217
|
+
current = capture(input_spec(root, task, plan, record))
|
|
3218
|
+
if current != record["inputs"]:
|
|
3219
|
+
continue
|
|
3220
|
+
if record.get("quality_attempt") == context["attempt"]:
|
|
3221
|
+
continue
|
|
3222
|
+
# 仅运行时生成引用,保留原始执行时间与输入;Agent 不重新包装历史结论。
|
|
3223
|
+
append_execution_record(root, task_id, {
|
|
3224
|
+
**record, "reused_from": index, "quality_attempt": context["attempt"],
|
|
3225
|
+
"implementation_fingerprint": context["implementation_fingerprint"],
|
|
3226
|
+
"config_fingerprint": context["config_fingerprint"],
|
|
3227
|
+
})
|
|
3228
|
+
|
|
3229
|
+
|
|
3230
|
+
def begin_correction(root: Path, task_id: str, task: dict, files: list[str], summary: str,
|
|
3231
|
+
risks: list[str], agent: str) -> dict:
|
|
3232
|
+
if task.get("status") not in {"IMPLEMENT", "QUALITY", "MEMORY", "ANALYSIS"}:
|
|
3233
|
+
raise StateError("A correction needs an active implementation task.")
|
|
3234
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3235
|
+
units = plan.get("units", [])
|
|
3236
|
+
allowed = {f for unit in units for f in unit.get("files", [])}
|
|
3237
|
+
if not files or not set(files) <= allowed or not summary.strip():
|
|
3238
|
+
raise StateError("A correction must name existing task files and the confirmed change.")
|
|
3239
|
+
cancel_active_quality_attempt(root, task_id, task, agent, summary, "manual-return")
|
|
3240
|
+
task = load_task(root, task_id) or task
|
|
3241
|
+
cleanup_verification_checkpoint(root, task_id, task)
|
|
3242
|
+
task["correction"] = {
|
|
3243
|
+
"files": sorted(set(files)), "summary": summary.strip(), "risks": risks,
|
|
3244
|
+
"unit_ids": [u["id"] for u in units if set(u.get("files", [])) & set(files)],
|
|
3245
|
+
"started_at": now_iso(),
|
|
3246
|
+
}
|
|
3247
|
+
records = validated_quality_records(root, task_id)
|
|
3248
|
+
if records:
|
|
3249
|
+
task["quality_consumed_attempt"] = records[-1][1]["attempt"]
|
|
3250
|
+
task.pop("quality_return_required", None)
|
|
3251
|
+
task.pop("pending_transition", None)
|
|
3252
|
+
task["status"] = "IMPLEMENT"
|
|
3253
|
+
append_stage_history(task, "IMPLEMENT", agent)
|
|
3254
|
+
write_task(root, task_id, task)
|
|
3255
|
+
task["workflow_mode"], reasons = calculate_workflow_floor(root, task_id)
|
|
3256
|
+
write_task(root, task_id, task)
|
|
3257
|
+
if isinstance(task.get("spec_source"), dict):
|
|
3258
|
+
writeback_ready_tasks_for_implement(root, task_id, task, agent, source_task_ids={
|
|
3259
|
+
str(u["source_task_id"]) for u in units if u["id"] in task["correction"]["unit_ids"]
|
|
3260
|
+
})
|
|
3261
|
+
append_execution_record(root, task_id, {
|
|
3262
|
+
"type": "correction", **task["correction"], "workflow_mode": task["workflow_mode"],
|
|
3263
|
+
})
|
|
3264
|
+
return {"task_id": task_id, "status": "IMPLEMENT", "workflow_mode": task["workflow_mode"],
|
|
3265
|
+
"reasons": reasons, "correction": task["correction"]}
|
|
3266
|
+
|
|
3267
|
+
|
|
3079
3268
|
def implementation_fingerprint(root: Path, task_id: str) -> str:
|
|
3080
3269
|
plan = latest_execution_plan(root, task_id)
|
|
3081
3270
|
if not plan:
|
|
3082
3271
|
raise StateError("Cannot calculate implementation fingerprint without a valid plan.")
|
|
3083
|
-
task = load_task(root, task_id)
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
digest.update(b"workflow-mode\0")
|
|
3087
|
-
digest.update(workflow_mode.encode("utf-8"))
|
|
3088
|
-
digest.update(b"\0")
|
|
3089
|
-
if task and task.get("tdd_enabled") is True:
|
|
3090
|
-
digest.update(b"tdd\0enabled\0")
|
|
3091
|
-
digest.update(str(task.get("tdd_coverage_threshold") or "").encode("utf-8"))
|
|
3092
|
-
digest.update(b"\0")
|
|
3093
|
-
digest.update(
|
|
3094
|
-
json.dumps(
|
|
3095
|
-
task.get("tdd_baselines") or {},
|
|
3096
|
-
sort_keys=True,
|
|
3097
|
-
separators=(",", ":"),
|
|
3098
|
-
).encode("utf-8")
|
|
3099
|
-
)
|
|
3100
|
-
digest.update(b"\0")
|
|
3101
|
-
digest.update(tdd_infrastructure_fingerprint(
|
|
3102
|
-
{root.resolve(), *task_repository_roots(root, task, plan)}
|
|
3103
|
-
).encode("ascii"))
|
|
3104
|
-
digest.update(b"execution-plan\0")
|
|
3105
|
-
digest.update(
|
|
3106
|
-
json.dumps(
|
|
3107
|
-
plan,
|
|
3108
|
-
ensure_ascii=False,
|
|
3109
|
-
sort_keys=True,
|
|
3110
|
-
separators=(",", ":"),
|
|
3111
|
-
).encode("utf-8")
|
|
3112
|
-
)
|
|
3113
|
-
digest.update(b"\0")
|
|
3114
|
-
if task and isinstance(task.get("spec_source"), dict):
|
|
3115
|
-
digest.update(b"canonical-spec\0")
|
|
3116
|
-
source = task.get("spec_source") or {}
|
|
3117
|
-
digest.update(
|
|
3118
|
-
json.dumps(
|
|
3119
|
-
{
|
|
3120
|
-
"source": {
|
|
3121
|
-
"schema": source.get("schema"),
|
|
3122
|
-
"spec_id": source.get("spec_id"),
|
|
3123
|
-
"revision": source.get("revision"),
|
|
3124
|
-
"design_sha256": source.get("design_sha256"),
|
|
3125
|
-
},
|
|
3126
|
-
"selected_tasks": task.get("selected_spec_tasks"),
|
|
3127
|
-
},
|
|
3128
|
-
ensure_ascii=False,
|
|
3129
|
-
sort_keys=True,
|
|
3130
|
-
separators=(",", ":"),
|
|
3131
|
-
).encode("utf-8")
|
|
3132
|
-
)
|
|
3133
|
-
digest.update(b"\0")
|
|
3134
|
-
update_git_worktree_fingerprint(digest, root, task, plan)
|
|
3135
|
-
repo_paths = task.get("repo_paths") if task else None
|
|
3136
|
-
file_entries: set[tuple[str, str | None]] = {
|
|
3137
|
-
(str(file_name), str(unit.get("repo_id")) if unit.get("repo_id") else None)
|
|
3138
|
-
for unit in plan.get("units", [])
|
|
3139
|
-
if isinstance(unit, dict)
|
|
3140
|
-
for file_name in unit.get("files", [])
|
|
3141
|
-
if is_non_empty_string(file_name)
|
|
3142
|
-
}
|
|
3143
|
-
for file_name, repo_id in sorted(file_entries, key=lambda item: (item[0], item[1] or "")):
|
|
3144
|
-
candidate = Path(file_name)
|
|
3145
|
-
was_absolute = candidate.is_absolute()
|
|
3146
|
-
base = root
|
|
3147
|
-
if (
|
|
3148
|
-
task
|
|
3149
|
-
and isinstance(task.get("spec_source"), dict)
|
|
3150
|
-
and isinstance(repo_paths, dict)
|
|
3151
|
-
and repo_id
|
|
3152
|
-
and is_non_empty_string(repo_paths.get(repo_id))
|
|
3153
|
-
):
|
|
3154
|
-
raw_base = Path(str(repo_paths[repo_id]))
|
|
3155
|
-
base = raw_base if raw_base.is_absolute() else root / raw_base
|
|
3156
|
-
if not was_absolute:
|
|
3157
|
-
candidate = base / candidate
|
|
3158
|
-
resolved = candidate.resolve()
|
|
3159
|
-
if not was_absolute:
|
|
3160
|
-
try:
|
|
3161
|
-
resolved.relative_to(base.resolve())
|
|
3162
|
-
except ValueError as error:
|
|
3163
|
-
raise StateError(f"Execution plan file escapes repository: {file_name}") from error
|
|
3164
|
-
digest.update(f"{repo_id or ''}:{file_name}".encode("utf-8"))
|
|
3165
|
-
digest.update(b"\0")
|
|
3166
|
-
try:
|
|
3167
|
-
digest.update(resolved.read_bytes())
|
|
3168
|
-
except OSError:
|
|
3169
|
-
digest.update(b"<missing>")
|
|
3170
|
-
digest.update(b"\0")
|
|
3171
|
-
return digest.hexdigest()
|
|
3272
|
+
task = load_task(root, task_id) or {}
|
|
3273
|
+
# 候选只绑定实际输入与验收契约,执行状态、revision、模式和计划说明不参与。
|
|
3274
|
+
return capture(input_spec(root, task, plan, {"type": "review"}))["signature"]
|
|
3172
3275
|
|
|
3173
3276
|
|
|
3174
|
-
def canonical_repository_fingerprints(
|
|
3175
|
-
root: Path, task_id: str, task: dict
|
|
3176
|
-
) -> dict[str, str]:
|
|
3277
|
+
def canonical_repository_fingerprints(root: Path, task_id: str, task: dict) -> dict[str, str]:
|
|
3177
3278
|
if not isinstance(task.get("spec_source"), dict):
|
|
3178
3279
|
return {}
|
|
3179
3280
|
plan = latest_execution_plan(root, task_id) or {}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
if isinstance(unit, dict) and is_non_empty_string(unit.get("repo_id"))
|
|
3187
|
-
}
|
|
3188
|
-
):
|
|
3189
|
-
raw_base = repo_paths.get(repo_id)
|
|
3190
|
-
if not is_non_empty_string(raw_base):
|
|
3191
|
-
continue
|
|
3192
|
-
base = Path(str(raw_base))
|
|
3193
|
-
if not base.is_absolute():
|
|
3194
|
-
base = root / base
|
|
3195
|
-
base = base.resolve()
|
|
3196
|
-
digest = hashlib.sha256()
|
|
3197
|
-
if task.get("tdd_enabled") is True:
|
|
3198
|
-
digest.update(tdd_infrastructure_fingerprint({root.resolve(), base}).encode("ascii"))
|
|
3199
|
-
units = [
|
|
3200
|
-
unit
|
|
3201
|
-
for unit in plan.get("units", [])
|
|
3202
|
-
if isinstance(unit, dict) and unit.get("repo_id") == repo_id
|
|
3203
|
-
]
|
|
3204
|
-
digest.update(
|
|
3205
|
-
json.dumps(
|
|
3206
|
-
units,
|
|
3207
|
-
ensure_ascii=False,
|
|
3208
|
-
sort_keys=True,
|
|
3209
|
-
separators=(",", ":"),
|
|
3210
|
-
).encode("utf-8")
|
|
3211
|
-
)
|
|
3212
|
-
digest.update(b"\0")
|
|
3213
|
-
repository = git_repository_root(base)
|
|
3214
|
-
if repository is not None and repository.resolve() == base:
|
|
3215
|
-
update_git_repository_content_fingerprint(
|
|
3216
|
-
digest,
|
|
3217
|
-
root,
|
|
3218
|
-
repository,
|
|
3219
|
-
[base],
|
|
3220
|
-
set(),
|
|
3221
|
-
)
|
|
3222
|
-
else:
|
|
3223
|
-
for unit in units:
|
|
3224
|
-
for file_name in sorted(
|
|
3225
|
-
str(value)
|
|
3226
|
-
for value in unit.get("files", [])
|
|
3227
|
-
if is_non_empty_string(value)
|
|
3228
|
-
):
|
|
3229
|
-
candidate = (base / file_name).resolve()
|
|
3230
|
-
try:
|
|
3231
|
-
candidate.relative_to(base)
|
|
3232
|
-
except ValueError as error:
|
|
3233
|
-
raise StateError(
|
|
3234
|
-
f"Execution plan file escapes repository: {file_name}"
|
|
3235
|
-
) from error
|
|
3236
|
-
digest.update(file_name.encode("utf-8"))
|
|
3237
|
-
digest.update(b"\0")
|
|
3238
|
-
try:
|
|
3239
|
-
digest.update(candidate.read_bytes())
|
|
3240
|
-
except OSError:
|
|
3241
|
-
digest.update(b"<missing>")
|
|
3242
|
-
digest.update(b"\0")
|
|
3243
|
-
fingerprints[repo_id] = digest.hexdigest()
|
|
3244
|
-
return fingerprints
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
def config_without_frozen_tdd_settings(payload: bytes) -> bytes:
|
|
3248
|
-
"""任务冻结 TDD 契约后,从证据指纹中排除仅影响未来任务的实时 TDD 配置。"""
|
|
3249
|
-
try:
|
|
3250
|
-
lines = payload.decode("utf-8").splitlines(keepends=True)
|
|
3251
|
-
except UnicodeDecodeError:
|
|
3252
|
-
return payload
|
|
3253
|
-
filtered: list[str] = []
|
|
3254
|
-
in_behavior = False
|
|
3255
|
-
behavior_indent = 0
|
|
3256
|
-
behavior_key_indent: int | None = None
|
|
3257
|
-
for line in lines:
|
|
3258
|
-
clean = line.split("#", 1)[0].rstrip()
|
|
3259
|
-
stripped = clean.strip()
|
|
3260
|
-
indent = len(clean) - len(clean.lstrip(" "))
|
|
3261
|
-
if stripped == "behavior:":
|
|
3262
|
-
in_behavior = True
|
|
3263
|
-
behavior_indent = indent
|
|
3264
|
-
behavior_key_indent = None
|
|
3265
|
-
filtered.append(line)
|
|
3266
|
-
continue
|
|
3267
|
-
if in_behavior and stripped and indent <= behavior_indent:
|
|
3268
|
-
in_behavior = False
|
|
3269
|
-
if in_behavior and stripped:
|
|
3270
|
-
if behavior_key_indent is None:
|
|
3271
|
-
behavior_key_indent = indent
|
|
3272
|
-
key = stripped.split(":", 1)[0]
|
|
3273
|
-
if (
|
|
3274
|
-
indent == behavior_key_indent
|
|
3275
|
-
and key in {"tdd_enabled", "tdd_coverage_threshold"}
|
|
3276
|
-
):
|
|
3277
|
-
continue
|
|
3278
|
-
filtered.append(line)
|
|
3279
|
-
return "".join(filtered).encode("utf-8")
|
|
3281
|
+
return {
|
|
3282
|
+
repo: digest([capture(input_spec(root, task, plan, {
|
|
3283
|
+
"type": "review", "unit_id": unit["id"]
|
|
3284
|
+
}))["signature"] for unit in plan.get("units", []) if unit.get("repo_id") == repo])
|
|
3285
|
+
for repo in sorted({unit["repo_id"] for unit in plan.get("units", [])})
|
|
3286
|
+
}
|
|
3280
3287
|
|
|
3281
3288
|
|
|
3282
3289
|
def behavior_config_fingerprint(root: Path, task: dict | None = None) -> str:
|
|
3283
|
-
|
|
3284
|
-
digest
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
if task and isinstance(task.get("tdd_enabled"), bool):
|
|
3288
|
-
payload = config_without_frozen_tdd_settings(payload)
|
|
3289
|
-
digest.update(payload)
|
|
3290
|
-
except OSError:
|
|
3291
|
-
digest.update(b"<missing-config>")
|
|
3292
|
-
return digest.hexdigest()
|
|
3290
|
+
# 审批方式、记忆策略等配置不影响已经执行的测试。
|
|
3291
|
+
return digest({key: (task or {}).get(key) for key in (
|
|
3292
|
+
"tdd_enabled", "tdd_coverage_threshold", "tdd_baselines"
|
|
3293
|
+
)})
|
|
3293
3294
|
|
|
3294
3295
|
|
|
3295
3296
|
def evidence_fingerprints(root: Path, task_id: str) -> dict[str, str]:
|
|
@@ -3599,6 +3600,7 @@ def ensure_quality_attempt_context(
|
|
|
3599
3600
|
)
|
|
3600
3601
|
if (
|
|
3601
3602
|
finalized.get("outcome") in {"passed", "repair", "replan"}
|
|
3603
|
+
and finalized.get("attempt") != task.get("quality_consumed_attempt")
|
|
3602
3604
|
and finalized.get("implementation_fingerprint")
|
|
3603
3605
|
== expected["implementation_fingerprint"]
|
|
3604
3606
|
and finalized.get("config_fingerprint") == expected["config_fingerprint"]
|
|
@@ -3612,6 +3614,7 @@ def ensure_quality_attempt_context(
|
|
|
3612
3614
|
)
|
|
3613
3615
|
if persist:
|
|
3614
3616
|
append_canonical_quality_carry_forward(root, task_id, task, context, agent)
|
|
3617
|
+
carry_forward_scoped_evidence(root, task_id, task, context)
|
|
3615
3618
|
task["quality_attempt"] = context
|
|
3616
3619
|
task["last_agent"] = agent
|
|
3617
3620
|
write_task(root, task_id, task)
|
|
@@ -3888,19 +3891,9 @@ def finalize_quality_attempt(
|
|
|
3888
3891
|
latest_failure_records: dict[tuple[str, str], dict] = {}
|
|
3889
3892
|
for record in [*current_reviews, *current_verifications]:
|
|
3890
3893
|
owner = str(record.get("source_task_id")) if canonical else task_id
|
|
3891
|
-
if record.get("type")
|
|
3892
|
-
record.get("dimension")
|
|
3893
|
-
):
|
|
3894
|
-
label = f"review:{record['dimension']}"
|
|
3895
|
-
elif record.get("type") == "verify" and is_non_empty_string(
|
|
3896
|
-
record.get("check")
|
|
3897
|
-
):
|
|
3898
|
-
coverage_scope = str(record.get("coverage_scope") or "")
|
|
3899
|
-
label = f"verify:{record['check']}"
|
|
3900
|
-
if coverage_scope:
|
|
3901
|
-
label = f"{label}:{coverage_scope}"
|
|
3902
|
-
else:
|
|
3894
|
+
if record.get("type") not in {"review", "verify"}:
|
|
3903
3895
|
continue
|
|
3896
|
+
label = failure_label(record)
|
|
3904
3897
|
latest_failure_records[(owner, label)] = record
|
|
3905
3898
|
evidence_failure_classes: set[str] = set()
|
|
3906
3899
|
for owner, labels in failures.items():
|
|
@@ -4981,8 +4974,10 @@ def validate_spec_implementation_results(root: Path, task_id: str, task: dict) -
|
|
|
4981
4974
|
default=-1,
|
|
4982
4975
|
)
|
|
4983
4976
|
lifecycle_by_unit: dict[str, list[dict]] = {unit_id: [] for unit_id in unit_by_id}
|
|
4984
|
-
for record in records
|
|
4977
|
+
for index, record in enumerate(records):
|
|
4985
4978
|
unit_id = str(record.get("unit_id") or "")
|
|
4979
|
+
if index <= latest_plan_index and unit_id in task.get("correction", {}).get("unit_ids", list(unit_by_id)):
|
|
4980
|
+
continue
|
|
4986
4981
|
if record.get("type") in {"dispatch", "result"} and unit_id in unit_by_id:
|
|
4987
4982
|
lifecycle_by_unit[unit_id].append(record)
|
|
4988
4983
|
missing_dispatches = sorted(
|
|
@@ -5065,7 +5060,7 @@ def validate_review_readiness(
|
|
|
5065
5060
|
):
|
|
5066
5061
|
dimension = str(record["dimension"])
|
|
5067
5062
|
source_task_id = str(record.get("source_task_id") or "")
|
|
5068
|
-
record_key =
|
|
5063
|
+
record_key = gate_identity(record)
|
|
5069
5064
|
latest_by_dimension[record_key] = record
|
|
5070
5065
|
if not latest_by_dimension:
|
|
5071
5066
|
raise StateError(
|
|
@@ -5121,6 +5116,9 @@ def validate_review_readiness(
|
|
|
5121
5116
|
"Canonical Spec review evidence does not cover selected source tasks: "
|
|
5122
5117
|
+ ", ".join(missing_review_tasks)
|
|
5123
5118
|
)
|
|
5119
|
+
require_unit_evidence(latest_execution_plan(root, task_id) or {},
|
|
5120
|
+
list(latest_by_dimension.values()), "Review",
|
|
5121
|
+
2 if task.get("workflow_mode") == "strict" else 1)
|
|
5124
5122
|
has_failed_dimension = False
|
|
5125
5123
|
for record in latest_by_dimension.values():
|
|
5126
5124
|
findings = record.get("findings")
|
|
@@ -5167,7 +5165,7 @@ def validate_review_readiness(
|
|
|
5167
5165
|
"Strict Canonical Spec review requires at least two passed dimensions for "
|
|
5168
5166
|
"every selected source task: " + ", ".join(missing_strict_dimensions)
|
|
5169
5167
|
)
|
|
5170
|
-
elif len(latest_by_dimension) < 2:
|
|
5168
|
+
elif len({record["dimension"] for record in latest_by_dimension.values()}) < 2:
|
|
5171
5169
|
raise StateError(
|
|
5172
5170
|
"Strict workflow requires at least two passed review dimensions for the current implementation fingerprint."
|
|
5173
5171
|
)
|
|
@@ -5214,11 +5212,7 @@ def validate_verification_readiness(
|
|
|
5214
5212
|
):
|
|
5215
5213
|
# 远程 CI 只作为生成的自动化能力,历史 pending/failed 记录不再参与本地验收。
|
|
5216
5214
|
continue
|
|
5217
|
-
check =
|
|
5218
|
-
if task.get("tdd_enabled") is True and record.get("check_type") == "coverage":
|
|
5219
|
-
check = f"{check}\0{record.get('coverage_scope') or ''}"
|
|
5220
|
-
if is_spec_task:
|
|
5221
|
-
check = f"{check}\0{record.get('source_task_id') or ''}"
|
|
5215
|
+
check = gate_identity(record)
|
|
5222
5216
|
previous = latest_by_check.get(check)
|
|
5223
5217
|
if (
|
|
5224
5218
|
record.get("applicable") is False
|
|
@@ -5273,6 +5267,7 @@ def validate_verification_readiness(
|
|
|
5273
5267
|
applicable_records = [
|
|
5274
5268
|
record for record in latest_by_check.values() if record.get("applicable") is not False
|
|
5275
5269
|
]
|
|
5270
|
+
require_unit_evidence(latest_execution_plan(root, task_id) or {}, applicable_records, "Verification")
|
|
5276
5271
|
if not applicable_records:
|
|
5277
5272
|
raise StateError(
|
|
5278
5273
|
"QUALITY cannot advance to MEMORY without at least one applicable executed check."
|
|
@@ -5506,7 +5501,10 @@ def validate_verification_readiness(
|
|
|
5506
5501
|
for record in applicable_records
|
|
5507
5502
|
if is_non_empty_string(record.get("command"))
|
|
5508
5503
|
}
|
|
5509
|
-
missing_commands = sorted(required_test_commands
|
|
5504
|
+
missing_commands = sorted(required for required in required_test_commands if not any(
|
|
5505
|
+
actual[:2] == required[:2] and command_covers(actual[2], required[2])
|
|
5506
|
+
for actual in executed_commands
|
|
5507
|
+
))
|
|
5510
5508
|
if missing_commands:
|
|
5511
5509
|
raise StateError(
|
|
5512
5510
|
"Canonical Spec verification is missing source test commands: "
|
|
@@ -5575,7 +5573,7 @@ def quality_repair_failures_for_window(
|
|
|
5575
5573
|
and is_non_empty_string(unit.get("source_task_id"))
|
|
5576
5574
|
and is_non_empty_string(unit.get("repo_id"))
|
|
5577
5575
|
}
|
|
5578
|
-
fingerprints = evidence_fingerprints(root, task_id)
|
|
5576
|
+
fingerprints = evidence_fingerprints(root, task_id) if not (implementation_fingerprint_value and config_fingerprint_value) else {}
|
|
5579
5577
|
implementation = (
|
|
5580
5578
|
implementation_fingerprint_value or fingerprints["implementation_fingerprint"]
|
|
5581
5579
|
)
|
|
@@ -5640,7 +5638,7 @@ def quality_repair_failures_for_window(
|
|
|
5640
5638
|
)
|
|
5641
5639
|
):
|
|
5642
5640
|
owner = source_task_id if canonical else task_id
|
|
5643
|
-
latest_reviews[(owner,
|
|
5641
|
+
latest_reviews[(owner, *gate_identity(record)[1:])] = record
|
|
5644
5642
|
elif record_type == "verify" and record.get(
|
|
5645
5643
|
"implementation_fingerprint"
|
|
5646
5644
|
) == implementation and record.get("config_fingerprint") == config:
|
|
@@ -5669,16 +5667,10 @@ def quality_repair_failures_for_window(
|
|
|
5669
5667
|
):
|
|
5670
5668
|
continue
|
|
5671
5669
|
owner = source_task_id if canonical else task_id
|
|
5672
|
-
latest_verifications[
|
|
5673
|
-
(
|
|
5674
|
-
owner,
|
|
5675
|
-
str(record["check"]),
|
|
5676
|
-
str(record.get("coverage_scope") or ""),
|
|
5677
|
-
)
|
|
5678
|
-
] = record
|
|
5670
|
+
latest_verifications[(owner, *gate_identity(record)[1:])] = record
|
|
5679
5671
|
|
|
5680
5672
|
failures: dict[str, list[str]] = {}
|
|
5681
|
-
for (source_task_id, dimension), record in latest_reviews.items():
|
|
5673
|
+
for (source_task_id, unit_id, dimension, _scope), record in latest_reviews.items():
|
|
5682
5674
|
findings = record.get("findings")
|
|
5683
5675
|
has_error = isinstance(findings, list) and any(
|
|
5684
5676
|
isinstance(finding, dict)
|
|
@@ -5686,13 +5678,10 @@ def quality_repair_failures_for_window(
|
|
|
5686
5678
|
for finding in findings
|
|
5687
5679
|
)
|
|
5688
5680
|
if record.get("passed") is not True or has_error:
|
|
5689
|
-
failures.setdefault(source_task_id, []).append(
|
|
5690
|
-
for (source_task_id, check, scope), record in latest_verifications.items():
|
|
5681
|
+
failures.setdefault(source_task_id, []).append(failure_label(record))
|
|
5682
|
+
for (source_task_id, unit_id, check, scope), record in latest_verifications.items():
|
|
5691
5683
|
if record.get("applicable") is not False and record.get("passed") is not True:
|
|
5692
|
-
|
|
5693
|
-
if scope:
|
|
5694
|
-
label = f"{label}:{scope}"
|
|
5695
|
-
failures.setdefault(source_task_id, []).append(label)
|
|
5684
|
+
failures.setdefault(source_task_id, []).append(failure_label(record))
|
|
5696
5685
|
return failures
|
|
5697
5686
|
|
|
5698
5687
|
|
|
@@ -6422,6 +6411,16 @@ def validate_analysis_readiness(
|
|
|
6422
6411
|
tdd_enabled = behavior[8] if task_type != TDD_INIT_TASK_TYPE else False
|
|
6423
6412
|
tdd_threshold = behavior[11]
|
|
6424
6413
|
|
|
6414
|
+
if dev_spec.is_file() and not tdd_enabled:
|
|
6415
|
+
compact = dev_spec.read_text(encoding="utf-8")
|
|
6416
|
+
if compact.startswith("<!-- easy-coding:compact -->"):
|
|
6417
|
+
mode, _ = calculate_workflow_floor(root, task_id)
|
|
6418
|
+
if mode != "fast" or not has_valid_execution_plan(root, task_id):
|
|
6419
|
+
raise StateError("Compact analysis requires a valid Fast implementation plan.")
|
|
6420
|
+
if re.findall(r"^decision_status:\s*(\w+)\s*$", compact, re.MULTILINE) != ["closed"]:
|
|
6421
|
+
raise StateError("Compact analysis must record the confirmed scope.")
|
|
6422
|
+
return
|
|
6423
|
+
|
|
6425
6424
|
dev_spec_content = ""
|
|
6426
6425
|
if not dev_spec.exists():
|
|
6427
6426
|
reasons.append("dev-spec.md is missing")
|
|
@@ -8592,7 +8591,8 @@ def reconcile_local_result_evidence(
|
|
|
8592
8591
|
and check.get("passed") is True
|
|
8593
8592
|
and is_non_empty_string(check.get("command"))
|
|
8594
8593
|
}
|
|
8595
|
-
missing_unit_commands = sorted(
|
|
8594
|
+
missing_unit_commands = sorted(command for command in unit.get("test_commands", [])
|
|
8595
|
+
if not any(command_covers(actual, command) for actual in passed_commands))
|
|
8596
8596
|
if missing_unit_commands:
|
|
8597
8597
|
unresolved.append(
|
|
8598
8598
|
f"{unit_id}:missing-passed-command=" + ",".join(missing_unit_commands)
|
|
@@ -8630,7 +8630,7 @@ def reconcile_local_result_evidence(
|
|
|
8630
8630
|
missing_commands = [
|
|
8631
8631
|
str(test.get("command"))
|
|
8632
8632
|
for test in tests
|
|
8633
|
-
if str(test.get("command"))
|
|
8633
|
+
if not any(command_covers(actual, str(test.get("command"))) for actual in passed_commands)
|
|
8634
8634
|
]
|
|
8635
8635
|
if missing_commands:
|
|
8636
8636
|
unresolved.append(
|
|
@@ -9035,23 +9035,29 @@ def sync_spec_design_state(
|
|
|
9035
9035
|
progress.pop("pending_action", None)
|
|
9036
9036
|
task.pop("spec_change", None)
|
|
9037
9037
|
task.pop("spec_context", None)
|
|
9038
|
-
if task.get("status") not in {"INIT", "ANALYSIS"}:
|
|
9039
|
-
cleanup_verification_checkpoint(root, resolved_task_id, task)
|
|
9040
|
-
task["status"] = "ANALYSIS"
|
|
9041
|
-
append_stage_history(task, "ANALYSIS", agent)
|
|
9042
|
-
task.pop("pending_transition", None)
|
|
9043
|
-
task["last_agent"] = agent
|
|
9044
9038
|
already_acknowledged = any(
|
|
9045
9039
|
record.get("type") == "spec-design-sync"
|
|
9046
9040
|
and record.get("idempotency_key") == idempotency_key
|
|
9047
9041
|
for record in execution_records(root, resolved_task_id)
|
|
9048
9042
|
)
|
|
9043
|
+
if task.get("correction") and not already_acknowledged:
|
|
9044
|
+
refresh_correction_plan(root, resolved_task_id, task, inspection)
|
|
9045
|
+
if task.get("correction") and not already_acknowledged:
|
|
9046
|
+
task["status"] = "IMPLEMENT"
|
|
9047
|
+
append_stage_history(task, "IMPLEMENT", agent)
|
|
9048
|
+
if not task.get("correction") and task.get("status") not in {"INIT", "ANALYSIS"}:
|
|
9049
|
+
cleanup_verification_checkpoint(root, resolved_task_id, task)
|
|
9050
|
+
task["status"] = "ANALYSIS"
|
|
9051
|
+
append_stage_history(task, "ANALYSIS", agent)
|
|
9052
|
+
task.pop("pending_transition", None)
|
|
9053
|
+
task["last_agent"] = agent
|
|
9049
9054
|
if not already_acknowledged:
|
|
9050
9055
|
append_execution_record(
|
|
9051
9056
|
root,
|
|
9052
9057
|
resolved_task_id,
|
|
9053
9058
|
{
|
|
9054
9059
|
"type": "spec-design-sync",
|
|
9060
|
+
"preserve_plan": bool(task.get("correction")),
|
|
9055
9061
|
"affected_task_ids": requested_task_ids,
|
|
9056
9062
|
"event_id": event["event_id"],
|
|
9057
9063
|
"design_sha256": details["design_sha256"],
|
|
@@ -9061,6 +9067,10 @@ def sync_spec_design_state(
|
|
|
9061
9067
|
},
|
|
9062
9068
|
)
|
|
9063
9069
|
write_task(root, resolved_task_id, task)
|
|
9070
|
+
if task.get("correction"):
|
|
9071
|
+
writeback_ready_tasks_for_implement(root, resolved_task_id, task, agent,
|
|
9072
|
+
source_task_ids=set(requested_task_ids),
|
|
9073
|
+
restart_statuses={"not_started"})
|
|
9064
9074
|
snapshot = snapshot_state(root, session_file, session)
|
|
9065
9075
|
snapshot["action"] = "sync-spec-design"
|
|
9066
9076
|
return snapshot
|
|
@@ -9297,7 +9307,7 @@ def writeback_verified_tasks(
|
|
|
9297
9307
|
if record.get("passed") is True
|
|
9298
9308
|
and str(record.get("source_task_id") or "") == source_task_id
|
|
9299
9309
|
and str(record.get("repo_id") or "") == repo_id
|
|
9300
|
-
and str(record.get("command") or "")
|
|
9310
|
+
and command_covers(str(record.get("command") or ""), command)
|
|
9301
9311
|
),
|
|
9302
9312
|
None,
|
|
9303
9313
|
)
|
|
@@ -9569,52 +9579,16 @@ def resolve_current_task(
|
|
|
9569
9579
|
|
|
9570
9580
|
|
|
9571
9581
|
def validate_workflow_mode_proposal(
|
|
9572
|
-
root: Path,
|
|
9573
|
-
session: dict,
|
|
9574
|
-
proposal: object,
|
|
9575
|
-
task_id: str | None = None,
|
|
9582
|
+
root: Path, session: dict, proposal: object, task_id: str | None = None,
|
|
9576
9583
|
) -> dict:
|
|
9577
|
-
if
|
|
9578
|
-
raise StateError("
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
if configured != effective_configured:
|
|
9586
|
-
raise StateError(
|
|
9587
|
-
"Workflow proposal configured_mode no longer matches the effective project/session setting."
|
|
9588
|
-
)
|
|
9589
|
-
if configured not in CONFIGURED_WORKFLOW_MODES:
|
|
9590
|
-
raise StateError("Invalid configured workflow mode.")
|
|
9591
|
-
if selected not in WORKFLOW_MODES or minimum not in WORKFLOW_MODES:
|
|
9592
|
-
raise StateError("selected_mode and minimum_mode must be fast, standard, or strict.")
|
|
9593
|
-
if source not in {"project", "session", "adaptive", "user", "migration"}:
|
|
9594
|
-
raise StateError("Invalid workflow proposal source.")
|
|
9595
|
-
if not is_string_list(reasons, allow_empty=False):
|
|
9596
|
-
raise StateError("Workflow proposal reasons must contain at least one non-empty reason.")
|
|
9597
|
-
required_rank = WORKFLOW_MODE_RANK[minimum]
|
|
9598
|
-
if configured in WORKFLOW_MODES and WORKFLOW_MODE_RANK[minimum] < WORKFLOW_MODE_RANK[configured]:
|
|
9599
|
-
raise StateError(
|
|
9600
|
-
f"Workflow minimum {minimum} is below configured floor {configured}."
|
|
9601
|
-
)
|
|
9602
|
-
if task_id:
|
|
9603
|
-
calculated_minimum, calculated_reasons = calculate_workflow_floor(root, task_id)
|
|
9604
|
-
calculated_rank = WORKFLOW_MODE_RANK[calculated_minimum]
|
|
9605
|
-
if WORKFLOW_MODE_RANK[minimum] < calculated_rank:
|
|
9606
|
-
raise StateError(
|
|
9607
|
-
f"Workflow minimum {minimum} is below calculated floor {calculated_minimum}: "
|
|
9608
|
-
+ ", ".join(calculated_reasons)
|
|
9609
|
-
)
|
|
9610
|
-
required_rank = max(required_rank, calculated_rank)
|
|
9611
|
-
if configured in WORKFLOW_MODES:
|
|
9612
|
-
required_rank = max(required_rank, WORKFLOW_MODE_RANK[configured])
|
|
9613
|
-
if WORKFLOW_MODE_RANK[selected] < required_rank:
|
|
9614
|
-
raise StateError(
|
|
9615
|
-
f"Workflow mode {selected} is below the allowed minimum for this task."
|
|
9616
|
-
)
|
|
9617
|
-
return proposal
|
|
9584
|
+
if task_id is None:
|
|
9585
|
+
raise StateError("A task is required to calculate the mechanical workflow mode.")
|
|
9586
|
+
mode, reasons = calculate_workflow_floor(root, task_id)
|
|
9587
|
+
return {
|
|
9588
|
+
"configured_mode": resolve_behavior(root, session)[5],
|
|
9589
|
+
"selected_mode": mode, "minimum_mode": mode, "source": "adaptive",
|
|
9590
|
+
"reasons": reasons,
|
|
9591
|
+
}
|
|
9618
9592
|
|
|
9619
9593
|
|
|
9620
9594
|
def calculate_workflow_floor(root: Path, task_id: str) -> tuple[str, list[str]]:
|
|
@@ -9626,6 +9600,12 @@ def calculate_workflow_floor(root: Path, task_id: str) -> tuple[str, list[str]]:
|
|
|
9626
9600
|
if not plan:
|
|
9627
9601
|
raise StateError("Cannot calculate workflow floor without a valid execution plan.")
|
|
9628
9602
|
units = [unit for unit in plan.get("units", []) if isinstance(unit, dict)]
|
|
9603
|
+
correction = task.get("correction")
|
|
9604
|
+
if isinstance(correction, dict):
|
|
9605
|
+
files = set(correction["files"])
|
|
9606
|
+
units = [{**unit, "files": sorted(set(unit.get("files", [])) & files),
|
|
9607
|
+
"risks": correction.get("risks", []), "contracts": []}
|
|
9608
|
+
for unit in units if unit["id"] in correction["unit_ids"]]
|
|
9629
9609
|
missing_local_baseline = [
|
|
9630
9610
|
str(unit.get("id") or "<unknown>")
|
|
9631
9611
|
for unit in units
|
|
@@ -9642,7 +9622,7 @@ def calculate_workflow_floor(root: Path, task_id: str) -> tuple[str, list[str]]:
|
|
|
9642
9622
|
for file_name in unit.get("files", [])
|
|
9643
9623
|
if is_non_empty_string(file_name)
|
|
9644
9624
|
}
|
|
9645
|
-
repositories = workflow_plan_repository_roots(root, task,
|
|
9625
|
+
repositories = workflow_plan_repository_roots(root, task, {"units": units})
|
|
9646
9626
|
ignored_values = {"none", "no", "n/a", "无", "无风险"}
|
|
9647
9627
|
risk_values = [
|
|
9648
9628
|
str(item)
|
|
@@ -9711,7 +9691,7 @@ def propose_workflow_mode(
|
|
|
9711
9691
|
"proposed_at": now_iso(),
|
|
9712
9692
|
"proposed_by": agent,
|
|
9713
9693
|
}
|
|
9714
|
-
validate_workflow_mode_proposal(root, session, proposal, resolved_task_id)
|
|
9694
|
+
proposal.update(validate_workflow_mode_proposal(root, session, proposal, resolved_task_id))
|
|
9715
9695
|
task["workflow_mode_proposal"] = proposal
|
|
9716
9696
|
task["last_agent"] = agent
|
|
9717
9697
|
write_task(root, resolved_task_id, task)
|
|
@@ -9784,21 +9764,8 @@ def raise_workflow_mode(
|
|
|
9784
9764
|
)
|
|
9785
9765
|
if stage != "IMPLEMENT":
|
|
9786
9766
|
raise StateError("A frozen workflow mode can only be raised during active execution.")
|
|
9787
|
-
|
|
9788
|
-
if current not in WORKFLOW_MODES or mode not in WORKFLOW_MODES:
|
|
9789
|
-
raise StateError("Workflow mode must be frozen before it can be raised.")
|
|
9790
|
-
if WORKFLOW_MODE_RANK[mode] <= WORKFLOW_MODE_RANK[current]:
|
|
9791
|
-
raise StateError(f"Workflow mode can only be raised above {current}.")
|
|
9767
|
+
mode, _reasons = calculate_workflow_floor(root, resolved_task_id)
|
|
9792
9768
|
task["workflow_mode"] = mode
|
|
9793
|
-
task.setdefault("workflow_mode_escalations", []).append(
|
|
9794
|
-
{
|
|
9795
|
-
"from": current,
|
|
9796
|
-
"to": mode,
|
|
9797
|
-
"reason": reason.strip(),
|
|
9798
|
-
"raised_at": now_iso(),
|
|
9799
|
-
"raised_by": agent,
|
|
9800
|
-
}
|
|
9801
|
-
)
|
|
9802
9769
|
task["last_agent"] = agent
|
|
9803
9770
|
write_task(root, resolved_task_id, task)
|
|
9804
9771
|
snapshot = snapshot_state(root, session_file, session)
|
|
@@ -9987,7 +9954,7 @@ def apply_transition(
|
|
|
9987
9954
|
if (
|
|
9988
9955
|
previous == "QUALITY"
|
|
9989
9956
|
and stage in {"IMPLEMENT", "ANALYSIS"}
|
|
9990
|
-
and quality_exit_outcome in {"repair", "replan"}
|
|
9957
|
+
and quality_exit_outcome in {"repair", "replan", "cancelled"}
|
|
9991
9958
|
):
|
|
9992
9959
|
quality_records = validated_quality_records(root, resolved_task_id)
|
|
9993
9960
|
task["quality_consumed_attempt"] = quality_records[-1][1]["attempt"]
|
|
@@ -10012,6 +9979,11 @@ def apply_transition(
|
|
|
10012
9979
|
task = load_task(root, resolved_task_id) or task
|
|
10013
9980
|
if stage in {"ANALYSIS", "IMPLEMENT", "MEMORY", "COMPLETE", "CLOSED"}:
|
|
10014
9981
|
cleanup_verification_checkpoint(root, resolved_task_id, task)
|
|
9982
|
+
if previous == "IMPLEMENT" and stage == "QUALITY":
|
|
9983
|
+
quality_records = validated_quality_records(root, resolved_task_id)
|
|
9984
|
+
if quality_records:
|
|
9985
|
+
task["quality_consumed_attempt"] = quality_records[-1][1]["attempt"]
|
|
9986
|
+
task.pop("quality_return_required", None)
|
|
10015
9987
|
task.pop("pending_transition", None)
|
|
10016
9988
|
if stage == "MEMORY" and previous != stage:
|
|
10017
9989
|
task["memory_progress"] = {}
|
|
@@ -10501,6 +10473,7 @@ def parse_evidence_args(values: list[str]) -> list[dict]:
|
|
|
10501
10473
|
return evidence
|
|
10502
10474
|
|
|
10503
10475
|
|
|
10476
|
+
@evidence_operation()
|
|
10504
10477
|
def main() -> int:
|
|
10505
10478
|
configure_stdio()
|
|
10506
10479
|
common = argparse.ArgumentParser(add_help=False)
|
|
@@ -10660,20 +10633,20 @@ def main() -> int:
|
|
|
10660
10633
|
"propose-workflow-mode", parents=[common]
|
|
10661
10634
|
)
|
|
10662
10635
|
propose_workflow_parser.add_argument(
|
|
10663
|
-
"--configured",
|
|
10636
|
+
"--configured", default="adaptive", choices=sorted(CONFIGURED_WORKFLOW_MODES)
|
|
10664
10637
|
)
|
|
10665
10638
|
propose_workflow_parser.add_argument(
|
|
10666
|
-
"--selected",
|
|
10639
|
+
"--selected", default="fast", choices=sorted(WORKFLOW_MODES)
|
|
10667
10640
|
)
|
|
10668
10641
|
propose_workflow_parser.add_argument(
|
|
10669
|
-
"--minimum",
|
|
10642
|
+
"--minimum", default="fast", choices=sorted(WORKFLOW_MODES)
|
|
10670
10643
|
)
|
|
10671
10644
|
propose_workflow_parser.add_argument(
|
|
10672
10645
|
"--source",
|
|
10673
|
-
|
|
10646
|
+
default="adaptive",
|
|
10674
10647
|
choices=["project", "session", "adaptive", "user", "migration"],
|
|
10675
10648
|
)
|
|
10676
|
-
propose_workflow_parser.add_argument("--reason",
|
|
10649
|
+
propose_workflow_parser.add_argument("--reason", action="append", default=[])
|
|
10677
10650
|
propose_workflow_parser.add_argument("--agent", required=True)
|
|
10678
10651
|
propose_workflow_parser.add_argument("--task-id")
|
|
10679
10652
|
|
|
@@ -10691,6 +10664,22 @@ def main() -> int:
|
|
|
10691
10664
|
fingerprints_parser.add_argument("--agent", required=True)
|
|
10692
10665
|
fingerprints_parser.add_argument("--task-id")
|
|
10693
10666
|
|
|
10667
|
+
for name in ("prepare-check", "record-check"):
|
|
10668
|
+
check_parser = subcommands.add_parser(name, parents=[common])
|
|
10669
|
+
check_parser.add_argument("--agent", required=True)
|
|
10670
|
+
check_parser.add_argument("--task-id")
|
|
10671
|
+
if name == "prepare-check":
|
|
10672
|
+
check_parser.add_argument("--record", required=True)
|
|
10673
|
+
else:
|
|
10674
|
+
check_parser.add_argument("--prepared-id", required=True)
|
|
10675
|
+
check_parser.add_argument("--result", required=True)
|
|
10676
|
+
correction_parser = subcommands.add_parser("begin-correction", parents=[common])
|
|
10677
|
+
correction_parser.add_argument("--file", action="append", required=True)
|
|
10678
|
+
correction_parser.add_argument("--summary", required=True)
|
|
10679
|
+
correction_parser.add_argument("--risk", action="append", default=[])
|
|
10680
|
+
correction_parser.add_argument("--agent", required=True)
|
|
10681
|
+
correction_parser.add_argument("--task-id")
|
|
10682
|
+
|
|
10694
10683
|
finalize_quality_parser = subcommands.add_parser(
|
|
10695
10684
|
"finalize-quality", parents=[common]
|
|
10696
10685
|
)
|
|
@@ -11261,6 +11250,16 @@ def main() -> int:
|
|
|
11261
11250
|
session_file,
|
|
11262
11251
|
)
|
|
11263
11252
|
)
|
|
11253
|
+
elif command in {"prepare-check", "record-check", "begin-correction"}:
|
|
11254
|
+
session, task_id, task = resolve_current_task(root, args.task_id, session_file)
|
|
11255
|
+
require_spec_context(root, task, agent, session_file)
|
|
11256
|
+
if command == "prepare-check":
|
|
11257
|
+
result = prepare_check(root, task_id, task, json.loads(args.record), agent)
|
|
11258
|
+
elif command == "record-check":
|
|
11259
|
+
result = record_check(root, task_id, task, args.prepared_id, json.loads(args.result), agent)
|
|
11260
|
+
else:
|
|
11261
|
+
result = begin_correction(root, task_id, task, args.file, args.summary, args.risk, agent)
|
|
11262
|
+
emit(result)
|
|
11264
11263
|
elif command == "finalize-quality":
|
|
11265
11264
|
emit(
|
|
11266
11265
|
attach_status_context(
|
|
@@ -11547,7 +11546,7 @@ def main() -> int:
|
|
|
11547
11546
|
)
|
|
11548
11547
|
)
|
|
11549
11548
|
return 0
|
|
11550
|
-
except (StateError, EasyDevSpecError) as error:
|
|
11549
|
+
except (StateError, EasyDevSpecError, ValueError) as error:
|
|
11551
11550
|
print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)
|
|
11552
11551
|
return 1
|
|
11553
11552
|
finally:
|