easy-coding-harness 1.1.0-beta.1 → 1.1.0-beta.2
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 +10 -0
- package/README.md +13 -4
- package/dist/cli.js +395 -190
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +4 -2
- package/templates/common/skills/ec-config/SKILL.md +33 -5
- package/templates/common/skills/ec-implementing/SKILL.md +10 -2
- package/templates/common/skills/ec-quality/SKILL.md +42 -10
- package/templates/common/skills/ec-task-management/SKILL.md +10 -3
- package/templates/common/skills/ec-workflow/SKILL.md +30 -10
- package/templates/main-constraint/AGENTS.md.tpl +24 -7
- package/templates/main-constraint/CLAUDE.md.tpl +24 -7
- package/templates/shared-hooks/easy_coding_inputs.py +18 -0
- package/templates/shared-hooks/easy_coding_state.py +367 -86
|
@@ -17,7 +17,8 @@ from pathlib import Path
|
|
|
17
17
|
import sys
|
|
18
18
|
|
|
19
19
|
from easy_coding_inputs import (
|
|
20
|
-
evidence_operation, memo, digest, input_spec, capture, changed_inputs,
|
|
20
|
+
evidence_operation, memo, cached_memo, invalidate_memo, digest, input_spec, capture, changed_inputs,
|
|
21
|
+
command_covers, command_identity,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
from easy_dev_spec import (
|
|
@@ -476,17 +477,15 @@ def parse_unit_test_mode(value: object, source: str) -> str:
|
|
|
476
477
|
return str(value)
|
|
477
478
|
|
|
478
479
|
|
|
479
|
-
def
|
|
480
|
-
|
|
480
|
+
def read_behavior_file(path: Path) -> tuple[dict, int]:
|
|
481
|
+
return memo(("behavior", str(path)), lambda: _read_behavior_file(path))
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _read_behavior_file(path: Path) -> tuple[dict, int]:
|
|
481
485
|
try:
|
|
482
486
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
483
|
-
except
|
|
484
|
-
return
|
|
485
|
-
DEFAULT_APPROVAL_MODE,
|
|
486
|
-
DEFAULT_WORKFLOW_MODE,
|
|
487
|
-
DEFAULT_UNIT_TEST_MODE,
|
|
488
|
-
DEFAULT_UT_COVERAGE_THRESHOLD,
|
|
489
|
-
)
|
|
487
|
+
except FileNotFoundError:
|
|
488
|
+
return {}, 0
|
|
490
489
|
|
|
491
490
|
in_behavior = False
|
|
492
491
|
behavior_indent = 0
|
|
@@ -498,6 +497,8 @@ def read_project_behavior(root: Path) -> tuple[str, str, str, int]:
|
|
|
498
497
|
if not stripped:
|
|
499
498
|
continue
|
|
500
499
|
indent = len(without_comment) - len(without_comment.lstrip(" "))
|
|
500
|
+
if stripped.startswith("behavior:") and stripped != "behavior:":
|
|
501
|
+
raise StateError("Behavior configuration must use an indented YAML mapping; write it with easy-coding config.")
|
|
501
502
|
if stripped == "behavior:":
|
|
502
503
|
in_behavior = True
|
|
503
504
|
behavior_indent = indent
|
|
@@ -515,6 +516,11 @@ def read_project_behavior(root: Path) -> tuple[str, str, str, int]:
|
|
|
515
516
|
key, value = stripped.split(":", 1)
|
|
516
517
|
behavior[key] = value.strip().strip("'\"")
|
|
517
518
|
|
|
519
|
+
return behavior, schema_version
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def read_project_behavior(root: Path) -> tuple[str, str, str, int]:
|
|
523
|
+
behavior, schema_version = read_behavior_file(root / ".easy-coding" / "config.yaml")
|
|
518
524
|
legacy = behavior.get("confirm_mode")
|
|
519
525
|
approval_mode = behavior.get("approval_mode")
|
|
520
526
|
workflow_mode = behavior.get("workflow_mode")
|
|
@@ -553,6 +559,28 @@ def read_project_behavior(root: Path) -> tuple[str, str, str, int]:
|
|
|
553
559
|
return approval_mode, workflow_mode, unit_test_mode, threshold
|
|
554
560
|
|
|
555
561
|
|
|
562
|
+
def behavior_layers(root: Path, session: dict) -> dict:
|
|
563
|
+
project, _ = read_behavior_file(root / ".easy-coding" / "config.yaml")
|
|
564
|
+
local, _ = read_behavior_file(Path.home() / ".easy-coding" / "config.yaml")
|
|
565
|
+
defaults = {"approval_mode": DEFAULT_APPROVAL_MODE, "cooperate_mode": "default",
|
|
566
|
+
"unit_test_mode": DEFAULT_UNIT_TEST_MODE,
|
|
567
|
+
"ut_coverage_threshold": DEFAULT_UT_COVERAGE_THRESHOLD}
|
|
568
|
+
layers = {"project": project, "local": local, "session": session}
|
|
569
|
+
result = {}
|
|
570
|
+
for key, default in defaults.items():
|
|
571
|
+
source = next((name for name in ("session", "local", "project")
|
|
572
|
+
if layers[name].get(key) is not None), "default")
|
|
573
|
+
value = layers[source][key] if source != "default" else default
|
|
574
|
+
if key == "ut_coverage_threshold":
|
|
575
|
+
value = parse_ut_threshold(value, f"{source} {key}")
|
|
576
|
+
elif key == "unit_test_mode":
|
|
577
|
+
value = parse_unit_test_mode(value, f"{source} {key}")
|
|
578
|
+
elif value not in (APPROVAL_MODES if key == "approval_mode" else {"default", "dispatch"}):
|
|
579
|
+
raise StateError(f"Invalid {source} {key}: {value}")
|
|
580
|
+
result[key] = {"value": value, "source": source}
|
|
581
|
+
return result
|
|
582
|
+
|
|
583
|
+
|
|
556
584
|
def safe_tdd_report_pattern(value: object) -> bool:
|
|
557
585
|
if not is_non_empty_string(value):
|
|
558
586
|
return False
|
|
@@ -753,19 +781,23 @@ def resolve_behavior(
|
|
|
753
781
|
session_threshold = parse_ut_threshold(
|
|
754
782
|
session_threshold, "session ut_coverage_threshold"
|
|
755
783
|
)
|
|
784
|
+
local, _ = read_behavior_file(Path.home() / ".easy-coding" / "config.yaml")
|
|
785
|
+
effective = behavior_layers(root, session)
|
|
756
786
|
return (
|
|
757
787
|
project_approval,
|
|
758
788
|
str(session_approval) if session_approval else None,
|
|
759
|
-
str(session_approval or project_approval),
|
|
789
|
+
str(session_approval or (effective["approval_mode"]["value"] if "approval_mode" in local else project_approval)),
|
|
760
790
|
project_workflow,
|
|
761
791
|
str(session_workflow) if session_workflow else None,
|
|
762
792
|
str(session_workflow or project_workflow),
|
|
763
793
|
project_unit_test,
|
|
764
794
|
session_unit_test,
|
|
765
|
-
session_unit_test if session_unit_test is not None else
|
|
795
|
+
session_unit_test if session_unit_test is not None else (
|
|
796
|
+
effective["unit_test_mode"]["value"] if "unit_test_mode" in local else project_unit_test),
|
|
766
797
|
project_threshold,
|
|
767
798
|
session_threshold,
|
|
768
|
-
session_threshold if session_threshold is not None else
|
|
799
|
+
session_threshold if session_threshold is not None else (
|
|
800
|
+
effective["ut_coverage_threshold"]["value"] if "ut_coverage_threshold" in local else project_threshold),
|
|
769
801
|
)
|
|
770
802
|
|
|
771
803
|
|
|
@@ -1815,6 +1847,11 @@ def append_execution_record(root: Path, task_id: str, record: dict) -> None:
|
|
|
1815
1847
|
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
1816
1848
|
handle.flush()
|
|
1817
1849
|
os.fsync(handle.fileno())
|
|
1850
|
+
records = cached_memo(("execution", str(path)))
|
|
1851
|
+
if records is not None:
|
|
1852
|
+
records.append(record)
|
|
1853
|
+
if record.get("type") in {"plan", "spec-design-sync"}:
|
|
1854
|
+
invalidate_memo(("plan", str(path)))
|
|
1818
1855
|
|
|
1819
1856
|
|
|
1820
1857
|
def is_non_empty_string(value: object) -> bool:
|
|
@@ -1844,13 +1881,18 @@ def is_valid_review_finding(value: object) -> bool:
|
|
|
1844
1881
|
|
|
1845
1882
|
|
|
1846
1883
|
def gate_identity(record: dict) -> tuple:
|
|
1884
|
+
# 已有手写证据没有输入凭据,保留原覆盖语义;新证据与 prepare-check 共用身份。
|
|
1885
|
+
if isinstance(record.get("inputs"), dict):
|
|
1886
|
+
return check_identity(record)
|
|
1847
1887
|
return (str(record.get("source_task_id") or ""), str(record.get("unit_id") or ""),
|
|
1848
1888
|
str(record.get("dimension") or record.get("check") or ""),
|
|
1849
1889
|
str(record.get("review_scope") or record.get("coverage_scope") or ""))
|
|
1850
1890
|
|
|
1851
1891
|
|
|
1852
1892
|
def failure_label(record: dict) -> str:
|
|
1853
|
-
|
|
1893
|
+
unit = str(record.get("unit_id") or "")
|
|
1894
|
+
name = str(record.get("dimension") or record.get("check") or "")
|
|
1895
|
+
scope = str(record.get("review_scope") or record.get("coverage_scope") or "")
|
|
1854
1896
|
label = f"{record['type']}:{name}"
|
|
1855
1897
|
if scope:
|
|
1856
1898
|
label += f":{scope}"
|
|
@@ -2223,30 +2265,42 @@ def inspect_task_spec(
|
|
|
2223
2265
|
def restore_spec_context(
|
|
2224
2266
|
root: Path, task_id: str, task: dict, agent: str,
|
|
2225
2267
|
session_file: str | Path | None = None,
|
|
2268
|
+
*, force: bool = False,
|
|
2226
2269
|
) -> dict | None:
|
|
2227
2270
|
if not isinstance(task.get("spec_source"), dict):
|
|
2228
2271
|
return None
|
|
2229
|
-
|
|
2272
|
+
session_key = resolve_session_path(root, session_file).relative_to(root.resolve()).as_posix()
|
|
2230
2273
|
try:
|
|
2231
2274
|
inspection, _ = inspect_task_spec(root, task, allow_pending_hard_dependencies=True)
|
|
2275
|
+
if isinstance(task.get("spec_change"), dict):
|
|
2276
|
+
raise StateError("Confirmed Spec change is pending; update the bound source and run sync-spec-design.")
|
|
2277
|
+
expected = {
|
|
2278
|
+
"session_file": session_key, "agent": normalize_session_agent(agent),
|
|
2279
|
+
"spec_id": inspection["spec_id"], "revision": inspection["revision"],
|
|
2280
|
+
"design_sha256": inspection["design_sha256"],
|
|
2281
|
+
"selected_spec_tasks": task["selected_spec_tasks"],
|
|
2282
|
+
}
|
|
2283
|
+
receipts = task.setdefault("spec_contexts", {})
|
|
2284
|
+
receipt = receipts.get(session_key) or task.get("spec_context")
|
|
2285
|
+
if not force and isinstance(receipt, dict) and all(receipt.get(k) == v for k, v in expected.items()):
|
|
2286
|
+
receipts[session_key] = receipt
|
|
2287
|
+
task["spec_context"] = receipt
|
|
2288
|
+
write_task(root, task_id, task)
|
|
2289
|
+
return {"status": "ready", "reused": True, "receipt": receipt}
|
|
2232
2290
|
context = select_consumption_scopes(
|
|
2233
2291
|
stored_spec_path(root, task), root, task["selected_spec_tasks"]
|
|
2234
2292
|
)
|
|
2235
2293
|
if context.get("design_sha256") != inspection.get("design_sha256"):
|
|
2236
2294
|
raise StateError("Canonical Spec changed while restoring context; retry resume-spec-context.")
|
|
2237
|
-
if isinstance(task.get("spec_change"), dict):
|
|
2238
|
-
raise StateError("Confirmed Spec change is pending; update the bound source and run sync-spec-design.")
|
|
2239
2295
|
task["spec_context"] = {
|
|
2240
|
-
|
|
2241
|
-
"agent": normalize_session_agent(agent),
|
|
2242
|
-
"spec_id": inspection["spec_id"],
|
|
2243
|
-
"revision": inspection["revision"],
|
|
2244
|
-
"design_sha256": inspection["design_sha256"],
|
|
2245
|
-
"selected_spec_tasks": task["selected_spec_tasks"],
|
|
2296
|
+
**expected,
|
|
2246
2297
|
"loaded_at": now_iso(),
|
|
2247
2298
|
}
|
|
2299
|
+
receipts[session_key] = task["spec_context"]
|
|
2248
2300
|
result = {"status": "ready", "consumption": context}
|
|
2249
2301
|
except (StateError, EasyDevSpecError, OSError, UnicodeError) as exc:
|
|
2302
|
+
task.pop("spec_context", None)
|
|
2303
|
+
task.get("spec_contexts", {}).pop(session_key, None)
|
|
2250
2304
|
# 接手仍可成功,修复来源与同步状态后必须重新加载,不能沿用旧会话的消费记录。
|
|
2251
2305
|
result = {"status": "blocked", "reason": str(exc), "source": task["spec_source"]}
|
|
2252
2306
|
write_task(root, task_id, task)
|
|
@@ -2260,7 +2314,7 @@ def resume_spec_context(
|
|
|
2260
2314
|
session, resolved_task_id, task = resolve_current_task(root, task_id, session_file)
|
|
2261
2315
|
if not isinstance(task.get("spec_source"), dict):
|
|
2262
2316
|
raise StateError("Current task is not backed by a Canonical Spec.")
|
|
2263
|
-
context = restore_spec_context(root, resolved_task_id, task, agent, session_file)
|
|
2317
|
+
context = restore_spec_context(root, resolved_task_id, task, agent, session_file, force=True)
|
|
2264
2318
|
snapshot = snapshot_state(root, session_file, session)
|
|
2265
2319
|
snapshot.update({"action": "resume-spec-context", "spec_context": context})
|
|
2266
2320
|
return snapshot
|
|
@@ -2277,9 +2331,10 @@ def require_spec_context(
|
|
|
2277
2331
|
inspection, _ = inspect_task_spec(
|
|
2278
2332
|
root, task, allow_pending_hard_dependencies=allow_pending_hard_dependencies,
|
|
2279
2333
|
)
|
|
2280
|
-
|
|
2334
|
+
session_key = resolve_session_path(root, session_file).relative_to(root.resolve()).as_posix()
|
|
2335
|
+
receipt = task.get("spec_contexts", {}).get(session_key) or task.get("spec_context")
|
|
2281
2336
|
expected = {
|
|
2282
|
-
"session_file":
|
|
2337
|
+
"session_file": session_key,
|
|
2283
2338
|
"agent": normalize_session_agent(agent),
|
|
2284
2339
|
"spec_id": inspection["spec_id"],
|
|
2285
2340
|
"revision": inspection["revision"],
|
|
@@ -2633,6 +2688,10 @@ def has_valid_execution_plan(root: Path, task_id: str) -> bool:
|
|
|
2633
2688
|
|
|
2634
2689
|
def execution_records(root: Path, task_id: str) -> list[dict]:
|
|
2635
2690
|
path = execution_log_path(root, task_id)
|
|
2691
|
+
return memo(("execution", str(path)), lambda: _execution_records(path))
|
|
2692
|
+
|
|
2693
|
+
|
|
2694
|
+
def _execution_records(path: Path) -> list[dict]:
|
|
2636
2695
|
if not path.exists():
|
|
2637
2696
|
return []
|
|
2638
2697
|
records: list[dict] = []
|
|
@@ -2649,6 +2708,11 @@ def execution_records(root: Path, task_id: str) -> list[dict]:
|
|
|
2649
2708
|
|
|
2650
2709
|
|
|
2651
2710
|
def latest_execution_plan(root: Path, task_id: str) -> dict | None:
|
|
2711
|
+
return memo(("plan", str(execution_log_path(root, task_id))),
|
|
2712
|
+
lambda: _latest_execution_plan(root, task_id))
|
|
2713
|
+
|
|
2714
|
+
|
|
2715
|
+
def _latest_execution_plan(root: Path, task_id: str) -> dict | None:
|
|
2652
2716
|
latest: dict | None = None
|
|
2653
2717
|
for record in execution_records(root, task_id):
|
|
2654
2718
|
if record.get("type") == "plan":
|
|
@@ -3147,9 +3211,9 @@ def tdd_infrastructure_fingerprint(repositories: set[Path]) -> str:
|
|
|
3147
3211
|
|
|
3148
3212
|
|
|
3149
3213
|
def check_identity(record: dict) -> tuple:
|
|
3150
|
-
return tuple(str(record.get(key) or "") for key in (
|
|
3151
|
-
"type", "unit_id", "source_task_id", "dimension", "review_scope", "
|
|
3152
|
-
))
|
|
3214
|
+
return (*tuple(str(record.get(key) or "") for key in (
|
|
3215
|
+
"type", "repo_id", "unit_id", "source_task_id", "dimension", "review_scope", "check_type", "coverage_scope"
|
|
3216
|
+
)), command_identity(str(record.get("command") or "")))
|
|
3153
3217
|
|
|
3154
3218
|
|
|
3155
3219
|
def prepare_check(root: Path, task_id: str, task: dict, descriptor: dict, agent: str) -> dict:
|
|
@@ -3202,9 +3266,12 @@ def record_check(root: Path, task_id: str, task: dict, prepared_id: str, result:
|
|
|
3202
3266
|
if type(result.get("exit_code")) is not int or result["passed"] != (result["exit_code"] == 0):
|
|
3203
3267
|
raise StateError("Verification passed must agree with its real exit_code.")
|
|
3204
3268
|
context = None
|
|
3205
|
-
|
|
3269
|
+
repair = task.get("quality_repair")
|
|
3270
|
+
repairing = (task.get("status") == "QUALITY" and descriptor["type"] == "verify"
|
|
3271
|
+
and isinstance(repair, dict) and repair.get("approved_at") and not repair.get("completed_at"))
|
|
3272
|
+
if task.get("status") == "QUALITY" and not repairing:
|
|
3206
3273
|
context = ensure_quality_attempt_context(root, task_id, task, agent, persist=True)
|
|
3207
|
-
elif task.get("status") != "IMPLEMENT":
|
|
3274
|
+
elif task.get("status") != "IMPLEMENT" and not repairing:
|
|
3208
3275
|
raise StateError("Record checks only during implementation or quality.")
|
|
3209
3276
|
record = {
|
|
3210
3277
|
**result, **descriptor, **evidence_fingerprints(root, task_id),
|
|
@@ -3250,6 +3317,8 @@ def begin_correction(root: Path, task_id: str, task: dict, files: list[str], sum
|
|
|
3250
3317
|
allowed = {f for unit in units for f in unit.get("files", [])}
|
|
3251
3318
|
if not files or not set(files) <= allowed or not summary.strip():
|
|
3252
3319
|
raise StateError("A correction must name existing task files and the confirmed change.")
|
|
3320
|
+
if task.get("status") == "QUALITY":
|
|
3321
|
+
return prepare_quality_repair(root, task_id, task, files, summary, agent)
|
|
3253
3322
|
cancel_active_quality_attempt(root, task_id, task, agent, summary, "manual-return")
|
|
3254
3323
|
task = load_task(root, task_id) or task
|
|
3255
3324
|
cleanup_verification_checkpoint(root, task_id, task)
|
|
@@ -3279,6 +3348,136 @@ def begin_correction(root: Path, task_id: str, task: dict, files: list[str], sum
|
|
|
3279
3348
|
"reasons": reasons, "correction": task["correction"]}
|
|
3280
3349
|
|
|
3281
3350
|
|
|
3351
|
+
def prepare_quality_repair(root: Path, task_id: str, task: dict, files: list[str],
|
|
3352
|
+
summary: str, agent: str) -> dict:
|
|
3353
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3354
|
+
records = validated_quality_records(root, task_id)
|
|
3355
|
+
last = records[-1][1] if records else {}
|
|
3356
|
+
if last.get("outcome") == "replan" and last.get("attempt") != task.get("quality_consumed_attempt"):
|
|
3357
|
+
raise StateError("The confirmed contract changed; finish the ANALYSIS replan first.")
|
|
3358
|
+
inputs = capture(input_spec(root, task, plan, {"type": "review"}))
|
|
3359
|
+
repair_id = digest([last.get("attempt", 0), inputs["signature"], sorted(files), summary.strip()])
|
|
3360
|
+
existing = task.get("quality_repair")
|
|
3361
|
+
if isinstance(existing, dict) and not existing.get("completed_at"):
|
|
3362
|
+
if existing["repair_id"] == repair_id:
|
|
3363
|
+
return {"task_id": task_id, "status": "QUALITY", "quality_repair": existing}
|
|
3364
|
+
if existing.get("approved_at"):
|
|
3365
|
+
raise StateError("Complete the approved repair before preparing another one.")
|
|
3366
|
+
repair = {
|
|
3367
|
+
"repair_id": repair_id, "summary": summary.strip(), "files": sorted(set(files)),
|
|
3368
|
+
"unit_ids": [u["id"] for u in plan.get("units", []) if set(u["files"]) & set(files)],
|
|
3369
|
+
"quality_attempt": last.get("attempt", 0),
|
|
3370
|
+
"implementation_fingerprint": inputs["signature"], "inputs": inputs,
|
|
3371
|
+
}
|
|
3372
|
+
task["quality_repair"] = repair
|
|
3373
|
+
write_task(root, task_id, task)
|
|
3374
|
+
return {"task_id": task_id, "status": "QUALITY", "quality_repair": repair}
|
|
3375
|
+
|
|
3376
|
+
|
|
3377
|
+
def start_quality_repair(root: Path, repair_id: str, executor: str, confirmed: bool, agent: str,
|
|
3378
|
+
task_id: str | None = None, session_file: str | Path | None = None) -> dict:
|
|
3379
|
+
session, task_id, task = resolve_current_task(root, task_id, session_file)
|
|
3380
|
+
require_spec_context(root, task, agent, session_file)
|
|
3381
|
+
repair = task.get("quality_repair")
|
|
3382
|
+
if task.get("status") != "QUALITY" or not isinstance(repair, dict) or repair.get("repair_id") != repair_id:
|
|
3383
|
+
raise StateError("Select the current QUALITY repair bundle.")
|
|
3384
|
+
if repair.get("completed_at"):
|
|
3385
|
+
return snapshot_state(root, session_file, session)
|
|
3386
|
+
mode = behavior_layers(root, session)["cooperate_mode"]["value"]
|
|
3387
|
+
if executor not in {"current", "other"} or (not repair.get("approved_at") and executor == "other" and mode != "dispatch"):
|
|
3388
|
+
raise StateError("QUALITY repair handoff requires cooperate_mode dispatch.")
|
|
3389
|
+
if repair.get("approved_at"):
|
|
3390
|
+
if repair.get("executor") != executor:
|
|
3391
|
+
raise StateError("This repair already has an approved executor; resume that handoff.")
|
|
3392
|
+
else:
|
|
3393
|
+
if (mode == "dispatch" or resolve_approval_mode(root, session)[2] == "approve") and not confirmed:
|
|
3394
|
+
raise StateError("Confirm the repair scope and executor once before starting this repair.")
|
|
3395
|
+
if implementation_fingerprint(root, task_id) != repair["implementation_fingerprint"]:
|
|
3396
|
+
raise StateError("Repair inputs changed before approval; refresh the displayed repair bundle.")
|
|
3397
|
+
if isinstance(task.get("spec_source"), dict):
|
|
3398
|
+
task, failed_sources = prepare_canonical_repair_transition(root, task_id, task, agent)
|
|
3399
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3400
|
+
repair_sources = {u.get("source_task_id") for u in plan.get("units", [])
|
|
3401
|
+
if u["id"] in repair["unit_ids"]}
|
|
3402
|
+
if not failed_sources <= repair_sources:
|
|
3403
|
+
raise StateError("The repair bundle must cover all failed Canonical source tasks: "
|
|
3404
|
+
+ ", ".join(sorted(failed_sources - repair_sources)))
|
|
3405
|
+
cancel_active_quality_attempt(root, task_id, task, agent, repair["summary"], "manual-return")
|
|
3406
|
+
task = load_task(root, task_id) or task
|
|
3407
|
+
repair = task["quality_repair"]
|
|
3408
|
+
repair.update(approved_at=now_iso(), approved_by=agent, executor=executor,
|
|
3409
|
+
authorization="explicit-user" if confirmed else "approval-policy",
|
|
3410
|
+
execution_start_index=len(execution_records(root, task_id)))
|
|
3411
|
+
cleanup_verification_checkpoint(root, task_id, task)
|
|
3412
|
+
task.pop("pending_transition", None)
|
|
3413
|
+
write_task(root, task_id, task)
|
|
3414
|
+
if isinstance(task.get("canonical_repair_transition"), dict):
|
|
3415
|
+
sources = set(task["canonical_repair_transition"]["source_task_ids"])
|
|
3416
|
+
writeback_ready_tasks_for_implement(root, task_id, task, agent, {"blocked"}, sources,
|
|
3417
|
+
repair_id=repair_id)
|
|
3418
|
+
task = load_task(root, task_id) or task
|
|
3419
|
+
task.pop("canonical_repair_transition", None)
|
|
3420
|
+
write_task(root, task_id, task)
|
|
3421
|
+
if executor == "other" and not repair.get("handed_off"):
|
|
3422
|
+
# 同一授权直接生成接力,接手不再经过阶段审批。
|
|
3423
|
+
result = handoff_task(root, agent, repair["summary"], task_id, session_file,
|
|
3424
|
+
{"next_action": "repair", "unit_ids": repair["unit_ids"],
|
|
3425
|
+
"stop_after": "repair", "repair_id": repair_id})
|
|
3426
|
+
task = load_task(root, task_id)
|
|
3427
|
+
task["quality_repair"]["handed_off"] = True
|
|
3428
|
+
write_task(root, task_id, task)
|
|
3429
|
+
return result
|
|
3430
|
+
return snapshot_state(root, session_file, session)
|
|
3431
|
+
|
|
3432
|
+
|
|
3433
|
+
def complete_quality_repair(root: Path, repair_id: str, agent: str,
|
|
3434
|
+
task_id: str | None = None, session_file: str | Path | None = None) -> dict:
|
|
3435
|
+
session, task_id, task = resolve_current_task(root, task_id, session_file)
|
|
3436
|
+
require_spec_context(root, task, agent, session_file)
|
|
3437
|
+
repair = task.get("quality_repair")
|
|
3438
|
+
if task.get("status") != "QUALITY" or not isinstance(repair, dict) or repair.get("repair_id") != repair_id or not repair.get("approved_at"):
|
|
3439
|
+
raise StateError("Complete only the approved current QUALITY repair.")
|
|
3440
|
+
if repair.get("completed_at"):
|
|
3441
|
+
return snapshot_state(root, session_file, session)
|
|
3442
|
+
plan = latest_execution_plan(root, task_id) or {}
|
|
3443
|
+
current = capture(input_spec(root, task, plan, {"type": "review"}))
|
|
3444
|
+
previous = repair["inputs"]
|
|
3445
|
+
if previous["spec"] != current["spec"]:
|
|
3446
|
+
raise StateError("Repair changed the contract or input scope; reconcile the plan before verification.")
|
|
3447
|
+
allowed = set()
|
|
3448
|
+
for unit in plan.get("units", []):
|
|
3449
|
+
if unit["id"] not in repair["unit_ids"]:
|
|
3450
|
+
continue
|
|
3451
|
+
binding = (task.get("repo_paths") or {}).get(unit.get("repo_id"), str(root))
|
|
3452
|
+
repository = Path(binding)
|
|
3453
|
+
if not repository.is_absolute():
|
|
3454
|
+
repository = root / repository
|
|
3455
|
+
allowed.update(str((repository / file).resolve()) for file in repair["files"] if file in unit["files"])
|
|
3456
|
+
changed = [str((Path(repo) / file).resolve()) for repo, files in current["files"].items()
|
|
3457
|
+
for file in set(files) | set(previous["files"].get(repo, {}))
|
|
3458
|
+
if files.get(file) != previous["files"].get(repo, {}).get(file)]
|
|
3459
|
+
if any(not any(path == scope or path.startswith(scope + "/") for scope in allowed) for path in changed):
|
|
3460
|
+
raise StateError("Repair changed files outside the approved bundle.")
|
|
3461
|
+
if isinstance(task.get("spec_source"), dict):
|
|
3462
|
+
inspection, _ = inspect_task_spec(root, task)
|
|
3463
|
+
snapshots = _selected_execution_snapshots(inspection, task)
|
|
3464
|
+
sources = {u["source_task_id"] for u in plan.get("units", []) if u["id"] in repair["unit_ids"]}
|
|
3465
|
+
if any(snapshots.get(source, {}).get("status") not in {"implemented", "verified", "completed"}
|
|
3466
|
+
for source in sources):
|
|
3467
|
+
raise StateError("Write back the repaired Canonical tasks as implemented before returning.")
|
|
3468
|
+
repair["completed_at"] = now_iso()
|
|
3469
|
+
task.pop("quality_return_required", None)
|
|
3470
|
+
records = validated_quality_records(root, task_id)
|
|
3471
|
+
if records:
|
|
3472
|
+
task["quality_consumed_attempt"] = records[-1][1]["attempt"]
|
|
3473
|
+
task["continuation"] = {"next_action": "quality", "unit_ids": repair["unit_ids"]}
|
|
3474
|
+
write_task(root, task_id, task)
|
|
3475
|
+
if repair["executor"] == "other":
|
|
3476
|
+
return handoff_task(root, agent, "Repair complete; review the delta and verify affected inputs.",
|
|
3477
|
+
task_id, session_file, task["continuation"])
|
|
3478
|
+
return snapshot_state(root, session_file, session)
|
|
3479
|
+
|
|
3480
|
+
|
|
3282
3481
|
def implementation_fingerprint(root: Path, task_id: str) -> str:
|
|
3283
3482
|
plan = latest_execution_plan(root, task_id)
|
|
3284
3483
|
if not plan:
|
|
@@ -3524,6 +3723,12 @@ def ensure_quality_attempt_context(
|
|
|
3524
3723
|
persist: bool = False,
|
|
3525
3724
|
infer_existing_evidence: bool = False,
|
|
3526
3725
|
) -> dict:
|
|
3726
|
+
repair = task.get("quality_repair")
|
|
3727
|
+
if isinstance(repair, dict) and not repair.get("completed_at"):
|
|
3728
|
+
raise StateError("Complete the current QUALITY repair before collecting gate evidence.")
|
|
3729
|
+
if persist and (task.get("continuation") or {}).get("next_action") == "quality":
|
|
3730
|
+
task.pop("continuation", None)
|
|
3731
|
+
write_task(root, task_id, task)
|
|
3527
3732
|
if isinstance(task.get("canonical_repair_transition"), dict):
|
|
3528
3733
|
raise StateError(
|
|
3529
3734
|
"Canonical repair transition is incomplete; resume it before collecting new QUALITY evidence."
|
|
@@ -5660,8 +5865,7 @@ def quality_repair_failures_for_window(
|
|
|
5660
5865
|
and repo_id == task_repositories[source_task_id]
|
|
5661
5866
|
)
|
|
5662
5867
|
):
|
|
5663
|
-
|
|
5664
|
-
latest_reviews[(owner, *gate_identity(record)[1:])] = record
|
|
5868
|
+
latest_reviews[gate_identity(record)] = record
|
|
5665
5869
|
elif record_type == "verify" and record.get(
|
|
5666
5870
|
"implementation_fingerprint"
|
|
5667
5871
|
) == implementation and record.get("config_fingerprint") == config:
|
|
@@ -5689,11 +5893,11 @@ def quality_repair_failures_for_window(
|
|
|
5689
5893
|
)
|
|
5690
5894
|
):
|
|
5691
5895
|
continue
|
|
5692
|
-
|
|
5693
|
-
latest_verifications[(owner, *gate_identity(record)[1:])] = record
|
|
5896
|
+
latest_verifications[gate_identity(record)] = record
|
|
5694
5897
|
|
|
5695
5898
|
failures: dict[str, list[str]] = {}
|
|
5696
|
-
for
|
|
5899
|
+
for record in latest_reviews.values():
|
|
5900
|
+
source_task_id = str(record.get("source_task_id")) if canonical else task_id
|
|
5697
5901
|
findings = record.get("findings")
|
|
5698
5902
|
has_error = isinstance(findings, list) and any(
|
|
5699
5903
|
isinstance(finding, dict)
|
|
@@ -5702,7 +5906,8 @@ def quality_repair_failures_for_window(
|
|
|
5702
5906
|
)
|
|
5703
5907
|
if record.get("passed") is not True or has_error:
|
|
5704
5908
|
failures.setdefault(source_task_id, []).append(failure_label(record))
|
|
5705
|
-
for
|
|
5909
|
+
for record in latest_verifications.values():
|
|
5910
|
+
source_task_id = str(record.get("source_task_id")) if canonical else task_id
|
|
5706
5911
|
if record.get("applicable") is not False and record.get("passed") is not True:
|
|
5707
5912
|
failures.setdefault(source_task_id, []).append(failure_label(record))
|
|
5708
5913
|
return failures
|
|
@@ -6805,45 +7010,14 @@ def validate_analysis_readiness(
|
|
|
6805
7010
|
|
|
6806
7011
|
|
|
6807
7012
|
def latest_handoff_record(root: Path, task_id: str) -> dict | None:
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
return None
|
|
6811
|
-
latest: dict | None = None
|
|
6812
|
-
try:
|
|
6813
|
-
for line in path.read_text(encoding="utf-8").splitlines():
|
|
6814
|
-
if not line.strip():
|
|
6815
|
-
continue
|
|
6816
|
-
try:
|
|
6817
|
-
record = json.loads(line)
|
|
6818
|
-
except json.JSONDecodeError:
|
|
6819
|
-
continue
|
|
6820
|
-
if isinstance(record, dict) and record.get("type") == "handoff":
|
|
6821
|
-
latest = record
|
|
6822
|
-
except OSError:
|
|
6823
|
-
return None
|
|
6824
|
-
return latest
|
|
7013
|
+
return next((r for r in reversed(execution_records(root, task_id))
|
|
7014
|
+
if r.get("type") == "handoff"), None)
|
|
6825
7015
|
|
|
6826
7016
|
|
|
6827
7017
|
def pending_handoff_record(root: Path, task_id: str) -> dict | None:
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
latest_coordination: dict | None = None
|
|
6832
|
-
try:
|
|
6833
|
-
for line in path.read_text(encoding="utf-8").splitlines():
|
|
6834
|
-
if not line.strip():
|
|
6835
|
-
continue
|
|
6836
|
-
try:
|
|
6837
|
-
record = json.loads(line)
|
|
6838
|
-
except json.JSONDecodeError:
|
|
6839
|
-
continue
|
|
6840
|
-
if isinstance(record, dict) and record.get("type") in {"handoff", "claim"}:
|
|
6841
|
-
latest_coordination = record
|
|
6842
|
-
except OSError:
|
|
6843
|
-
return None
|
|
6844
|
-
if latest_coordination and latest_coordination.get("type") == "handoff":
|
|
6845
|
-
return latest_coordination
|
|
6846
|
-
return None
|
|
7018
|
+
latest = next((r for r in reversed(execution_records(root, task_id))
|
|
7019
|
+
if r.get("type") in {"handoff", "claim"}), None)
|
|
7020
|
+
return latest if latest and latest["type"] == "handoff" else None
|
|
6847
7021
|
|
|
6848
7022
|
|
|
6849
7023
|
def assert_safe_task_id(task_id: str) -> None:
|
|
@@ -7012,8 +7186,16 @@ def snapshot_state(
|
|
|
7012
7186
|
else {"status": "not_checked", "reasons": []}
|
|
7013
7187
|
)
|
|
7014
7188
|
|
|
7189
|
+
layers = behavior_layers(root, resolved_session)
|
|
7190
|
+
local, _ = read_behavior_file(Path.home() / ".easy-coding" / "config.yaml")
|
|
7015
7191
|
return {
|
|
7016
7192
|
"session_file": display_path(root, session_path),
|
|
7193
|
+
"behavior_sources": {key: item["source"] for key, item in layers.items()},
|
|
7194
|
+
"local_behavior": local,
|
|
7195
|
+
"effective_cooperate_mode": layers["cooperate_mode"]["value"],
|
|
7196
|
+
"cooperation": task.get("cooperation") if task else None,
|
|
7197
|
+
"quality_repair": task.get("quality_repair") if task else None,
|
|
7198
|
+
"continuation": task.get("continuation") if task else None,
|
|
7017
7199
|
"current_task": str(task_id) if task_id else None,
|
|
7018
7200
|
"task": task,
|
|
7019
7201
|
"pending_transition": task.get("pending_transition") if task else None,
|
|
@@ -7060,8 +7242,9 @@ def build_status_line(
|
|
|
7060
7242
|
session: dict,
|
|
7061
7243
|
agent: str | None = None,
|
|
7062
7244
|
session_file: str | Path | None = None,
|
|
7245
|
+
state: dict | None = None,
|
|
7063
7246
|
) -> str:
|
|
7064
|
-
state = snapshot_state(root, session_file, session)
|
|
7247
|
+
state = state if state is not None else snapshot_state(root, session_file, session)
|
|
7065
7248
|
if state["lite_mode"]:
|
|
7066
7249
|
lite_state = (
|
|
7067
7250
|
"Awaiting Confirmation"
|
|
@@ -7076,6 +7259,8 @@ def build_status_line(
|
|
|
7076
7259
|
approval = str(state["effective_approval_mode"]).capitalize()
|
|
7077
7260
|
workflow = str(state["concrete_workflow_mode"] or state["configured_workflow_mode"]).capitalize()
|
|
7078
7261
|
status_brand = f"> **Easy Coding** · **Approval: {approval}** · **Workflow: {workflow}**"
|
|
7262
|
+
if state["effective_cooperate_mode"] == "dispatch":
|
|
7263
|
+
status_brand += " · **Dispatch**"
|
|
7079
7264
|
if state["displayed_unit_test_mode"] in {"ut", "tdd"}:
|
|
7080
7265
|
status_brand += f" · **{state['displayed_unit_test_mode'].upper()}**"
|
|
7081
7266
|
task_id = state["current_task"]
|
|
@@ -7108,8 +7293,9 @@ def build_machine_breadcrumbs(
|
|
|
7108
7293
|
session: dict,
|
|
7109
7294
|
agent: str | None = None,
|
|
7110
7295
|
session_file: str | Path | None = None,
|
|
7296
|
+
state: dict | None = None,
|
|
7111
7297
|
) -> list[str]:
|
|
7112
|
-
state = snapshot_state(root, session_file, session)
|
|
7298
|
+
state = state if state is not None else snapshot_state(root, session_file, session)
|
|
7113
7299
|
task_id = state["current_task"]
|
|
7114
7300
|
task = state["task"]
|
|
7115
7301
|
stage = str(state["status"]) if task else "idle"
|
|
@@ -7119,6 +7305,7 @@ def build_machine_breadcrumbs(
|
|
|
7119
7305
|
f"[easy-coding:session-file:{resolved_session_file}]",
|
|
7120
7306
|
f"[easy-coding:approval-mode:{state['effective_approval_mode']}]",
|
|
7121
7307
|
f"[easy-coding:configured-workflow-mode:{state['configured_workflow_mode']}]",
|
|
7308
|
+
f"[easy-coding:cooperate-mode:{state['effective_cooperate_mode']}]",
|
|
7122
7309
|
]
|
|
7123
7310
|
if state.get("concrete_workflow_mode"):
|
|
7124
7311
|
lines.append(f"[easy-coding:workflow-mode:{state['concrete_workflow_mode']}]")
|
|
@@ -7130,10 +7317,15 @@ def build_machine_breadcrumbs(
|
|
|
7130
7317
|
|
|
7131
7318
|
if task_id:
|
|
7132
7319
|
lines.append(f"[current-task:{task_id}]")
|
|
7320
|
+
continuation = state.get("continuation") or {}
|
|
7321
|
+
if continuation.get("next_action"):
|
|
7322
|
+
lines.append(f"[easy-coding:next-action:{continuation['next_action']}]")
|
|
7323
|
+
if continuation.get("stop_after"):
|
|
7324
|
+
lines.append(f"[easy-coding:stop-after:{continuation['stop_after']}]")
|
|
7133
7325
|
if task and isinstance(task.get("spec_source"), dict):
|
|
7134
7326
|
source = task["spec_source"]
|
|
7135
7327
|
lines.append(f"[easy-coding:spec:{source.get('spec_id')}:revision:{source.get('revision')}]")
|
|
7136
|
-
lines.append("[easy-coding:spec-context:
|
|
7328
|
+
lines.append("[easy-coding:spec-context:reuse-current-session-or-resume-if-missing]")
|
|
7137
7329
|
if task.get("spec_change"):
|
|
7138
7330
|
lines.append("[easy-coding:spec-change:pending-sync-spec-design]")
|
|
7139
7331
|
if (task.get("spec_writeback_progress") or {}).get("pending_action"):
|
|
@@ -7207,6 +7399,7 @@ def build_status_context(
|
|
|
7207
7399
|
session: dict,
|
|
7208
7400
|
agent: str | None = None,
|
|
7209
7401
|
session_file: str | Path | None = None,
|
|
7402
|
+
state: dict | None = None,
|
|
7210
7403
|
) -> str:
|
|
7211
7404
|
if session.get("harness_disabled") is True:
|
|
7212
7405
|
session_path = resolve_session_path(root, session_file)
|
|
@@ -7227,10 +7420,11 @@ def build_status_context(
|
|
|
7227
7420
|
if isinstance(proposal, dict):
|
|
7228
7421
|
lines.append(f"[easy-coding:lite-proposal:{proposal.get('digest', 'missing')}]")
|
|
7229
7422
|
return "\n".join(lines)
|
|
7423
|
+
state = state if state is not None else snapshot_state(root, session_file, session)
|
|
7230
7424
|
return "\n".join(
|
|
7231
7425
|
[
|
|
7232
|
-
build_status_line(root, session, agent, session_file),
|
|
7233
|
-
*build_machine_breadcrumbs(root, session, agent, session_file),
|
|
7426
|
+
build_status_line(root, session, agent, session_file, state),
|
|
7427
|
+
*build_machine_breadcrumbs(root, session, agent, session_file, state),
|
|
7234
7428
|
]
|
|
7235
7429
|
)
|
|
7236
7430
|
|
|
@@ -7245,7 +7439,8 @@ def attach_status_context(
|
|
|
7245
7439
|
session = load_session(root, resolved_session_file)
|
|
7246
7440
|
if session is None:
|
|
7247
7441
|
session = default_session()
|
|
7248
|
-
|
|
7442
|
+
state = data if "current_task" in data and "effective_approval_mode" in data else None
|
|
7443
|
+
context = build_status_context(root, session, agent, resolved_session_file, state)
|
|
7249
7444
|
first_line = context.splitlines()[0] if context.startswith("> ") else ""
|
|
7250
7445
|
enriched = dict(data)
|
|
7251
7446
|
enriched["status_line"] = first_line
|
|
@@ -7852,6 +8047,7 @@ def handoff_task(
|
|
|
7852
8047
|
summary: str,
|
|
7853
8048
|
task_id: str | None = None,
|
|
7854
8049
|
session_file: str | Path | None = None,
|
|
8050
|
+
continuation: dict | None = None,
|
|
7855
8051
|
) -> dict:
|
|
7856
8052
|
if not summary.strip():
|
|
7857
8053
|
raise StateError("Handoff summary is required.")
|
|
@@ -7865,6 +8061,24 @@ def handoff_task(
|
|
|
7865
8061
|
stage = str(task.get("status") or "PENDING")
|
|
7866
8062
|
if stage in TERMINAL_STATUSES:
|
|
7867
8063
|
raise StateError(f"Cannot hand off terminal task: {resolved_task_id}")
|
|
8064
|
+
if continuation is not None:
|
|
8065
|
+
if not isinstance(continuation, dict) or set(continuation) - {
|
|
8066
|
+
"next_action", "unit_ids", "evidence_refs", "stop_after", "repair_id"
|
|
8067
|
+
}:
|
|
8068
|
+
raise StateError("Unknown handoff continuation fields.")
|
|
8069
|
+
if continuation.get("next_action") not in {"implement", "repair", "quality", "continue"}:
|
|
8070
|
+
raise StateError("Handoff must name the next action.")
|
|
8071
|
+
plan = latest_execution_plan(root, str(resolved_task_id)) or {}
|
|
8072
|
+
unit_ids = continuation.get("unit_ids", [])
|
|
8073
|
+
if not is_string_list(unit_ids) or not set(unit_ids) <= {u["id"] for u in plan.get("units", [])}:
|
|
8074
|
+
raise StateError("Handoff units must belong to the existing plan.")
|
|
8075
|
+
refs = continuation.get("evidence_refs", [])
|
|
8076
|
+
count = len(execution_records(root, str(resolved_task_id)))
|
|
8077
|
+
if not isinstance(refs, list) or any(type(i) is not int or i < 0 or i >= count for i in refs):
|
|
8078
|
+
raise StateError("Handoff evidence must reference existing execution records.")
|
|
8079
|
+
if continuation.get("stop_after") not in {None, "IMPLEMENT", "repair"}:
|
|
8080
|
+
raise StateError("Unknown handoff stop point.")
|
|
8081
|
+
task["continuation"] = continuation
|
|
7868
8082
|
|
|
7869
8083
|
record = {
|
|
7870
8084
|
"type": "handoff",
|
|
@@ -7872,6 +8086,7 @@ def handoff_task(
|
|
|
7872
8086
|
"stage": stage,
|
|
7873
8087
|
"summary": summary.strip(),
|
|
7874
8088
|
"timestamp": now_iso(),
|
|
8089
|
+
**(continuation or {}),
|
|
7875
8090
|
}
|
|
7876
8091
|
append_execution_record(root, str(resolved_task_id), record)
|
|
7877
8092
|
task["last_agent"] = agent
|
|
@@ -7907,6 +8122,9 @@ def claim_task(root: Path, task_id: str, agent: str, session_file: str | Path |
|
|
|
7907
8122
|
else "takeover"
|
|
7908
8123
|
)
|
|
7909
8124
|
latest_handoff = latest_handoff_record(root, task_id)
|
|
8125
|
+
already_current = (session.get("current_task") == task_id
|
|
8126
|
+
and agents_equivalent(previous_agent, agent)
|
|
8127
|
+
and pending_handoff_record(root, task_id) is None)
|
|
7910
8128
|
task["last_agent"] = agent
|
|
7911
8129
|
write_task(root, task_id, task)
|
|
7912
8130
|
|
|
@@ -7923,7 +8141,8 @@ def claim_task(root: Path, task_id: str, agent: str, session_file: str | Path |
|
|
|
7923
8141
|
"action": action,
|
|
7924
8142
|
"timestamp": now_iso(),
|
|
7925
8143
|
}
|
|
7926
|
-
|
|
8144
|
+
if not already_current:
|
|
8145
|
+
append_execution_record(root, task_id, claim)
|
|
7927
8146
|
|
|
7928
8147
|
context = restore_spec_context(root, task_id, task, agent, session_file)
|
|
7929
8148
|
snapshot = snapshot_state(root, session_file, session)
|
|
@@ -7934,9 +8153,32 @@ def claim_task(root: Path, task_id: str, agent: str, session_file: str | Path |
|
|
|
7934
8153
|
snapshot["previous_agent"] = previous_agent
|
|
7935
8154
|
snapshot["latest_handoff"] = latest_handoff
|
|
7936
8155
|
snapshot["claim"] = claim
|
|
8156
|
+
snapshot["claim_recorded"] = not already_current
|
|
7937
8157
|
return snapshot
|
|
7938
8158
|
|
|
7939
8159
|
|
|
8160
|
+
def set_cooperate_mode(root: Path, mode: str | None, agent: str,
|
|
8161
|
+
session_file: str | Path | None = None) -> dict:
|
|
8162
|
+
if mode not in {None, "default", "dispatch"}:
|
|
8163
|
+
raise StateError("cooperate_mode must be default or dispatch.")
|
|
8164
|
+
session = ensure_session(root, session_file)
|
|
8165
|
+
if mode is None:
|
|
8166
|
+
session.pop("cooperate_mode", None)
|
|
8167
|
+
else:
|
|
8168
|
+
session["cooperate_mode"] = mode
|
|
8169
|
+
write_session(root, session, session_file)
|
|
8170
|
+
task_id = session.get("current_task")
|
|
8171
|
+
task = load_task(root, task_id)
|
|
8172
|
+
if task and pending_handoff_record(root, task_id) is None:
|
|
8173
|
+
task["cooperation"] = {
|
|
8174
|
+
"mode": behavior_layers(root, session)["cooperate_mode"]["value"],
|
|
8175
|
+
"coordinator": (task.get("cooperation") or {}).get("coordinator") or {
|
|
8176
|
+
"agent": agent, "session_file": display_path(root, resolve_session_path(root, session_file))},
|
|
8177
|
+
}
|
|
8178
|
+
write_task(root, task_id, task)
|
|
8179
|
+
return snapshot_state(root, session_file, session)
|
|
8180
|
+
|
|
8181
|
+
|
|
7940
8182
|
def create_task(
|
|
7941
8183
|
root: Path,
|
|
7942
8184
|
task_id: str,
|
|
@@ -7967,6 +8209,8 @@ def create_task(
|
|
|
7967
8209
|
"created_by": agent,
|
|
7968
8210
|
"last_agent": agent,
|
|
7969
8211
|
"stage_history": [{"stage": "INIT", "agent": agent, "entered_at": timestamp}],
|
|
8212
|
+
"cooperation": {"mode": behavior_layers(root, session)["cooperate_mode"]["value"],
|
|
8213
|
+
"coordinator": {"agent": agent, "session_file": display_path(root, resolve_session_path(root, session_file))}},
|
|
7970
8214
|
"context": {},
|
|
7971
8215
|
"spawned_from": None,
|
|
7972
8216
|
"spawned_tasks": [],
|
|
@@ -9069,6 +9313,7 @@ def sync_spec_design_state(
|
|
|
9069
9313
|
progress.pop("pending_action", None)
|
|
9070
9314
|
task.pop("spec_change", None)
|
|
9071
9315
|
task.pop("spec_context", None)
|
|
9316
|
+
task.pop("spec_contexts", None)
|
|
9072
9317
|
already_acknowledged = any(
|
|
9073
9318
|
record.get("type") == "spec-design-sync"
|
|
9074
9319
|
and record.get("idempotency_key") == idempotency_key
|
|
@@ -9177,6 +9422,7 @@ def writeback_ready_tasks_for_implement(
|
|
|
9177
9422
|
agent: str,
|
|
9178
9423
|
restart_statuses: set[str] | None = None,
|
|
9179
9424
|
source_task_ids: set[str] | None = None,
|
|
9425
|
+
*, repair_id: str | None = None,
|
|
9180
9426
|
) -> None:
|
|
9181
9427
|
inspection, _ = inspect_task_spec(root, task)
|
|
9182
9428
|
implement_attempt = 1 + sum(
|
|
@@ -9204,11 +9450,15 @@ def writeback_ready_tasks_for_implement(
|
|
|
9204
9450
|
f"{harness_task_id}:{source_task_id}:enter-implement:"
|
|
9205
9451
|
f"{task['spec_source']['revision']}:attempt-{implement_attempt}"
|
|
9206
9452
|
)
|
|
9453
|
+
if repair_id:
|
|
9454
|
+
key = f"{harness_task_id}:{source_task_id}:quality-repair:{repair_id}:start"
|
|
9455
|
+
summary = ("Harness started an approved repair within QUALITY" if repair_id else
|
|
9456
|
+
"Harness entered IMPLEMENT for a dependency-ready Canonical task")
|
|
9207
9457
|
action = {
|
|
9208
9458
|
"kind": "task",
|
|
9209
9459
|
"source_task_id": source_task_id,
|
|
9210
9460
|
"status": "in_progress",
|
|
9211
|
-
"summary":
|
|
9461
|
+
"summary": summary,
|
|
9212
9462
|
"evidence": [],
|
|
9213
9463
|
"idempotency_key": key,
|
|
9214
9464
|
"agent": agent,
|
|
@@ -9223,7 +9473,7 @@ def writeback_ready_tasks_for_implement(
|
|
|
9223
9473
|
stored_spec_path(root, task),
|
|
9224
9474
|
str(source_task_id),
|
|
9225
9475
|
"in_progress",
|
|
9226
|
-
|
|
9476
|
+
summary,
|
|
9227
9477
|
SPEC_WRITEBACK_APP,
|
|
9228
9478
|
spec_writeback_agent(agent),
|
|
9229
9479
|
design_digest,
|
|
@@ -10706,7 +10956,7 @@ def main() -> int:
|
|
|
10706
10956
|
if name == "prepare-check":
|
|
10707
10957
|
check_parser.add_argument("--record", required=True)
|
|
10708
10958
|
else:
|
|
10709
|
-
check_parser.add_argument("--prepared-id"
|
|
10959
|
+
check_parser.add_argument("--prepared-id")
|
|
10710
10960
|
check_parser.add_argument("--result", required=True)
|
|
10711
10961
|
correction_parser = subcommands.add_parser("begin-correction", parents=[common])
|
|
10712
10962
|
correction_parser.add_argument("--file", action="append", required=True)
|
|
@@ -10715,6 +10965,21 @@ def main() -> int:
|
|
|
10715
10965
|
correction_parser.add_argument("--agent", required=True)
|
|
10716
10966
|
correction_parser.add_argument("--task-id")
|
|
10717
10967
|
|
|
10968
|
+
for name in ("start-quality-repair", "complete-quality-repair"):
|
|
10969
|
+
repair_parser = subcommands.add_parser(name, parents=[common])
|
|
10970
|
+
repair_parser.add_argument("--repair-id", required=True)
|
|
10971
|
+
repair_parser.add_argument("--agent", required=True)
|
|
10972
|
+
repair_parser.add_argument("--task-id")
|
|
10973
|
+
if name == "start-quality-repair":
|
|
10974
|
+
repair_parser.add_argument("--executor", choices=["current", "other"], required=True)
|
|
10975
|
+
repair_parser.add_argument("--confirmed", action="store_true")
|
|
10976
|
+
|
|
10977
|
+
for name in ("set-cooperate-mode", "clear-cooperate-mode"):
|
|
10978
|
+
cooperate_parser = subcommands.add_parser(name, parents=[common])
|
|
10979
|
+
cooperate_parser.add_argument("--agent", required=True)
|
|
10980
|
+
if name == "set-cooperate-mode":
|
|
10981
|
+
cooperate_parser.add_argument("--mode", choices=["default", "dispatch"], required=True)
|
|
10982
|
+
|
|
10718
10983
|
finalize_quality_parser = subcommands.add_parser(
|
|
10719
10984
|
"finalize-quality", parents=[common]
|
|
10720
10985
|
)
|
|
@@ -10792,6 +11057,7 @@ def main() -> int:
|
|
|
10792
11057
|
handoff.add_argument("--agent", required=True)
|
|
10793
11058
|
handoff.add_argument("--summary", required=True)
|
|
10794
11059
|
handoff.add_argument("--task-id")
|
|
11060
|
+
handoff.add_argument("--continuation", help="JSON next_action, unit_ids, evidence_refs and stop_after")
|
|
10795
11061
|
|
|
10796
11062
|
claim = subcommands.add_parser("claim-task", parents=[common])
|
|
10797
11063
|
claim.add_argument("--task-id", required=True)
|
|
@@ -11289,12 +11555,26 @@ def main() -> int:
|
|
|
11289
11555
|
session, task_id, task = resolve_current_task(root, args.task_id, session_file)
|
|
11290
11556
|
require_spec_context(root, task, agent, session_file)
|
|
11291
11557
|
if command == "prepare-check":
|
|
11292
|
-
|
|
11558
|
+
descriptors = json.loads(args.record)
|
|
11559
|
+
result = ([prepare_check(root, task_id, task, item, agent) for item in descriptors]
|
|
11560
|
+
if isinstance(descriptors, list) else prepare_check(root, task_id, task, descriptors, agent))
|
|
11293
11561
|
elif command == "record-check":
|
|
11294
|
-
|
|
11562
|
+
results = json.loads(args.result)
|
|
11563
|
+
result = ([record_check(root, task_id, task, item["prepared_id"], item["result"], agent) for item in results]
|
|
11564
|
+
if isinstance(results, list) else record_check(root, task_id, task, args.prepared_id, results, agent))
|
|
11295
11565
|
else:
|
|
11296
11566
|
result = begin_correction(root, task_id, task, args.file, args.summary, args.risk, agent)
|
|
11297
11567
|
emit(result)
|
|
11568
|
+
elif command in {"start-quality-repair", "complete-quality-repair"}:
|
|
11569
|
+
if command == "start-quality-repair":
|
|
11570
|
+
result = start_quality_repair(root, args.repair_id, args.executor, args.confirmed,
|
|
11571
|
+
agent, args.task_id, session_file)
|
|
11572
|
+
else:
|
|
11573
|
+
result = complete_quality_repair(root, args.repair_id, agent, args.task_id, session_file)
|
|
11574
|
+
emit(attach_status_context(root, result, agent, session_file))
|
|
11575
|
+
elif command in {"set-cooperate-mode", "clear-cooperate-mode"}:
|
|
11576
|
+
emit(attach_status_context(root, set_cooperate_mode(root,
|
|
11577
|
+
args.mode if command == "set-cooperate-mode" else None, agent, session_file), agent, session_file))
|
|
11298
11578
|
elif command == "finalize-quality":
|
|
11299
11579
|
emit(
|
|
11300
11580
|
attach_status_context(
|
|
@@ -11411,7 +11691,8 @@ def main() -> int:
|
|
|
11411
11691
|
emit(
|
|
11412
11692
|
attach_status_context(
|
|
11413
11693
|
root,
|
|
11414
|
-
handoff_task(root, agent, args.summary, args.task_id, session_file
|
|
11694
|
+
handoff_task(root, agent, args.summary, args.task_id, session_file,
|
|
11695
|
+
json.loads(args.continuation) if args.continuation else None),
|
|
11415
11696
|
agent,
|
|
11416
11697
|
session_file,
|
|
11417
11698
|
)
|