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.
@@ -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[(str(record.get("source_task_id") or ""), dimension)] = record
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
- if covered_files[source_task_id] != {
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
- workflow_mode = str(task.get("workflow_mode") or "") if task else ""
3085
- digest = hashlib.sha256()
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
- repo_paths = task.get("repo_paths") if isinstance(task.get("repo_paths"), dict) else {}
3181
- fingerprints: dict[str, str] = {}
3182
- for repo_id in sorted(
3183
- {
3184
- str(unit.get("repo_id"))
3185
- for unit in plan.get("units", [])
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
- path = root / ".easy-coding" / "config.yaml"
3284
- digest = hashlib.sha256()
3285
- try:
3286
- payload = path.read_bytes()
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") == "review" and is_non_empty_string(
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[latest_plan_index + 1 :]:
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 = f"{dimension}\0{source_task_id}" if is_spec_task else dimension
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 = str(record["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 - executed_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, str(record["dimension"]))] = record
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(f"review:{dimension}")
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
- label = f"verify:{check}"
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(set(unit.get("test_commands", [])) - passed_commands)
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")) not in passed_commands
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 "") == command
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 not isinstance(proposal, dict):
9578
- raise StateError("workflow_mode_proposal is missing.")
9579
- configured = str(proposal.get("configured_mode") or "")
9580
- selected = str(proposal.get("selected_mode") or "")
9581
- minimum = str(proposal.get("minimum_mode") or "")
9582
- source = str(proposal.get("source") or "")
9583
- reasons = proposal.get("reasons")
9584
- effective_configured = resolve_behavior(root, session)[5]
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, plan)
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
- current = str(task.get("workflow_mode") or "")
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", required=True, choices=sorted(CONFIGURED_WORKFLOW_MODES)
10636
+ "--configured", default="adaptive", choices=sorted(CONFIGURED_WORKFLOW_MODES)
10664
10637
  )
10665
10638
  propose_workflow_parser.add_argument(
10666
- "--selected", required=True, choices=sorted(WORKFLOW_MODES)
10639
+ "--selected", default="fast", choices=sorted(WORKFLOW_MODES)
10667
10640
  )
10668
10641
  propose_workflow_parser.add_argument(
10669
- "--minimum", required=True, choices=sorted(WORKFLOW_MODES)
10642
+ "--minimum", default="fast", choices=sorted(WORKFLOW_MODES)
10670
10643
  )
10671
10644
  propose_workflow_parser.add_argument(
10672
10645
  "--source",
10673
- required=True,
10646
+ default="adaptive",
10674
10647
  choices=["project", "session", "adaptive", "user", "migration"],
10675
10648
  )
10676
- propose_workflow_parser.add_argument("--reason", required=True, action="append")
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: