@hunter-harness/workflow-harness 0.2.63 → 0.2.65
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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +6 -5
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +11 -5
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +4 -4
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +320 -35
- package/harness/bundles/general/claude-code/scripts/harness_context.py +43 -25
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +22 -27
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +155 -16
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +31 -5
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +6 -5
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +11 -5
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +4 -4
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +320 -35
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +43 -25
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +22 -27
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +155 -16
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +31 -5
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +6 -5
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +11 -5
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +320 -35
- package/harness/bundles/general/codex/scripts/harness_context.py +43 -25
- package/harness/bundles/general/codex/scripts/harness_gate.py +22 -27
- package/harness/bundles/general/codex/scripts/harness_ledger.py +155 -16
- package/harness/bundles/general/codex/scripts/harness_runtime.py +31 -5
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +6 -5
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +11 -5
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +320 -35
- package/harness/bundles/general/cursor/scripts/harness_context.py +43 -25
- package/harness/bundles/general/cursor/scripts/harness_gate.py +22 -27
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +155 -16
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +31 -5
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +6 -5
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +11 -5
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +320 -35
- package/harness/bundles/java/claude-code/scripts/harness_context.py +43 -25
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +22 -27
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +155 -16
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +31 -5
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +6 -5
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +11 -5
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +320 -35
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +43 -25
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +22 -27
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +155 -16
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +31 -5
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +6 -5
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +11 -5
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +320 -35
- package/harness/bundles/java/codex/scripts/harness_context.py +43 -25
- package/harness/bundles/java/codex/scripts/harness_gate.py +22 -27
- package/harness/bundles/java/codex/scripts/harness_ledger.py +155 -16
- package/harness/bundles/java/codex/scripts/harness_runtime.py +31 -5
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +6 -5
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +11 -5
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +320 -35
- package/harness/bundles/java/cursor/scripts/harness_context.py +43 -25
- package/harness/bundles/java/cursor/scripts/harness_gate.py +22 -27
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +155 -16
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +31 -5
- package/harness/manifests/general/claude-code.json +19 -19
- package/harness/manifests/general/codebuddy.json +19 -19
- package/harness/manifests/general/codex.json +19 -19
- package/harness/manifests/general/cursor.json +19 -19
- package/harness/manifests/java/claude-code.json +21 -21
- package/harness/manifests/java/codebuddy.json +21 -21
- package/harness/manifests/java/codex.json +21 -21
- package/harness/manifests/java/cursor.json +21 -21
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -55,6 +55,7 @@ MANIFEST_COMPARE_EXCLUDE = frozenset(
|
|
|
55
55
|
"logs/execution-log.md",
|
|
56
56
|
"execution-log.md",
|
|
57
57
|
"events.ndjson",
|
|
58
|
+
"evidence/archive-manifest-after.json",
|
|
58
59
|
}
|
|
59
60
|
)
|
|
60
61
|
|
|
@@ -66,6 +67,21 @@ _RE_UNIT_COUNTS = re.compile(
|
|
|
66
67
|
r"Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)"
|
|
67
68
|
)
|
|
68
69
|
_RE_API_PASSED = re.compile(r"(\d+)/(\d+)\s*passed", re.I)
|
|
70
|
+
_RE_RESULT_FRACTION = re.compile(
|
|
71
|
+
r"(\d{1,7})\s*/\s*(\d{1,7})(?=\s*(?:passed|通过|[((]|$))",
|
|
72
|
+
re.I,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _result_fraction(value: str) -> tuple[int, int] | None:
|
|
77
|
+
match = _RE_RESULT_FRACTION.search(value)
|
|
78
|
+
if match is None:
|
|
79
|
+
return None
|
|
80
|
+
passed = int(match.group(1))
|
|
81
|
+
total = int(match.group(2))
|
|
82
|
+
if total <= 0 or passed > total:
|
|
83
|
+
return None
|
|
84
|
+
return passed, total
|
|
69
85
|
|
|
70
86
|
# Ensure sibling harness_events is importable.
|
|
71
87
|
if str(SCRIPTS_DIR) not in sys.path:
|
|
@@ -1831,12 +1847,14 @@ def verify_manifest_byte_coverage(
|
|
|
1831
1847
|
}
|
|
1832
1848
|
mismatches: list[dict[str, str]] = []
|
|
1833
1849
|
checked = 0
|
|
1850
|
+
manifest_paths: set[str] = set()
|
|
1834
1851
|
for item in manifest.get("files") or []:
|
|
1835
1852
|
if not isinstance(item, dict):
|
|
1836
1853
|
continue
|
|
1837
1854
|
rel = str(item.get("path") or "").replace("\\", "/")
|
|
1838
1855
|
if not rel or _manifest_path_excluded(rel):
|
|
1839
1856
|
continue
|
|
1857
|
+
manifest_paths.add(rel)
|
|
1840
1858
|
if rel in excluded:
|
|
1841
1859
|
reasons.setdefault(rel, "excluded from checksum coverage")
|
|
1842
1860
|
continue
|
|
@@ -1856,6 +1874,14 @@ def verify_manifest_byte_coverage(
|
|
|
1856
1874
|
"actual": actual,
|
|
1857
1875
|
}
|
|
1858
1876
|
)
|
|
1877
|
+
for path in sorted(root.rglob("*")):
|
|
1878
|
+
if not path.is_file() or path.name.endswith(".lock"):
|
|
1879
|
+
continue
|
|
1880
|
+
rel = path.relative_to(root).as_posix()
|
|
1881
|
+
if _manifest_path_excluded(rel) or rel in excluded:
|
|
1882
|
+
continue
|
|
1883
|
+
if rel not in manifest_paths:
|
|
1884
|
+
mismatches.append({"path": rel, "reason": "unexpected"})
|
|
1859
1885
|
ok = not mismatches
|
|
1860
1886
|
if ok and reasons:
|
|
1861
1887
|
checksum_status = "OK_WITH_EXCLUSIONS"
|
|
@@ -2907,6 +2933,19 @@ def execute_archive(
|
|
|
2907
2933
|
except OSError:
|
|
2908
2934
|
pass
|
|
2909
2935
|
if not gate.get("ok"):
|
|
2936
|
+
if state_root.is_dir():
|
|
2937
|
+
try:
|
|
2938
|
+
append_event(
|
|
2939
|
+
state_root,
|
|
2940
|
+
phase="archive",
|
|
2941
|
+
type_="phase.end",
|
|
2942
|
+
status="BLOCKED",
|
|
2943
|
+
code=str(gate.get("reasonCode") or "ARCHIVE_PREPARE_BLOCKED"),
|
|
2944
|
+
message=str(gate.get("nextAction") or "归档条件未满足"),
|
|
2945
|
+
note="归档未开始,流程已在预检处结束",
|
|
2946
|
+
)
|
|
2947
|
+
except OSError:
|
|
2948
|
+
pass
|
|
2910
2949
|
status = gate.get("status") if isinstance(gate.get("status"), dict) else {}
|
|
2911
2950
|
blocked_payload = {
|
|
2912
2951
|
"ok": False,
|
|
@@ -2923,6 +2962,13 @@ def execute_archive(
|
|
|
2923
2962
|
}
|
|
2924
2963
|
if candidate_certification is not None:
|
|
2925
2964
|
blocked_payload["candidateCertification"] = candidate_certification
|
|
2965
|
+
_best_effort_archive_terminal_sync(
|
|
2966
|
+
find_project_root(change_dir),
|
|
2967
|
+
state_root,
|
|
2968
|
+
change_key=change_dir.name,
|
|
2969
|
+
run_id=hes.run_id_for(change_dir),
|
|
2970
|
+
payload=blocked_payload,
|
|
2971
|
+
)
|
|
2926
2972
|
return 1, blocked_payload
|
|
2927
2973
|
code, payload = cmd_finalize(
|
|
2928
2974
|
change_dir,
|
|
@@ -3057,6 +3103,15 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
3057
3103
|
skipped = int(m.group(4))
|
|
3058
3104
|
source = "evidence-text"
|
|
3059
3105
|
counted = True
|
|
3106
|
+
else:
|
|
3107
|
+
fraction = _result_fraction(evidence_text)
|
|
3108
|
+
if fraction is not None:
|
|
3109
|
+
passed_count, run = fraction
|
|
3110
|
+
failures = max(run - passed_count, 0)
|
|
3111
|
+
errors = 0
|
|
3112
|
+
skipped = 0
|
|
3113
|
+
source = "evidence-text"
|
|
3114
|
+
counted = True
|
|
3060
3115
|
|
|
3061
3116
|
if not counted:
|
|
3062
3117
|
run = int(unit.get("run", unit.get("testsRun", 0)) or 0)
|
|
@@ -3146,6 +3201,11 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
3146
3201
|
)
|
|
3147
3202
|
or 0
|
|
3148
3203
|
)
|
|
3204
|
+
if attempt_total == 0 and isinstance(attempt.get("evidence"), str):
|
|
3205
|
+
fraction = _result_fraction(str(attempt["evidence"]))
|
|
3206
|
+
if fraction is not None:
|
|
3207
|
+
attempt_passed, attempt_total = fraction
|
|
3208
|
+
attempt_failed = max(attempt_total - attempt_passed, 0)
|
|
3149
3209
|
raw_identities = (
|
|
3150
3210
|
attempt.get("testIdentities")
|
|
3151
3211
|
or attempt_metrics.get("testIdentities")
|
|
@@ -3258,6 +3318,14 @@ def _ledger_api_tests(
|
|
|
3258
3318
|
blocked = 0
|
|
3259
3319
|
source = "evidence-text"
|
|
3260
3320
|
counted = True
|
|
3321
|
+
else:
|
|
3322
|
+
fraction = _result_fraction(evidence_text)
|
|
3323
|
+
if fraction is not None:
|
|
3324
|
+
passed, total = fraction
|
|
3325
|
+
failed = max(total - passed, 0)
|
|
3326
|
+
blocked = 0
|
|
3327
|
+
source = "evidence-text"
|
|
3328
|
+
counted = True
|
|
3261
3329
|
|
|
3262
3330
|
if not counted and change_dir is not None:
|
|
3263
3331
|
results_path = change_dir / "runtime" / "api-test-results.json"
|
|
@@ -3845,6 +3913,11 @@ def _compute_final_status(
|
|
|
3845
3913
|
browser_status = str(browser.get("status") or "")
|
|
3846
3914
|
reasons: list[str] = []
|
|
3847
3915
|
for phase, v in stage_status.items():
|
|
3916
|
+
# Archive is closeout/transport state, not a product-quality gate. Keep
|
|
3917
|
+
# its failed attempt visible in stageStatus and the command payload,
|
|
3918
|
+
# but do not let it poison a later finalize retry.
|
|
3919
|
+
if phase == "archive":
|
|
3920
|
+
continue
|
|
3848
3921
|
if v == "FAIL":
|
|
3849
3922
|
return "FAIL", [f"stage {phase}=FAIL"]
|
|
3850
3923
|
unit = verification.get("unitTests") or {}
|
|
@@ -3871,6 +3944,8 @@ def _compute_final_status(
|
|
|
3871
3944
|
if reasons:
|
|
3872
3945
|
return "CONDITIONAL_OK", reasons
|
|
3873
3946
|
for phase, v in stage_status.items():
|
|
3947
|
+
if phase == "archive":
|
|
3948
|
+
continue
|
|
3874
3949
|
if v == "WARN":
|
|
3875
3950
|
return "WARN", [f"stage {phase}=WARN"]
|
|
3876
3951
|
if v in conditional:
|
|
@@ -4615,6 +4690,11 @@ def _final_commit_from_sources(
|
|
|
4615
4690
|
|
|
4616
4691
|
|
|
4617
4692
|
def _business_goal_from_sources(change_dir: Path, events: list[dict[str, Any]]) -> str:
|
|
4693
|
+
generic_plan_heading = re.compile(
|
|
4694
|
+
r"(?:(?:任务|实施|执行|变更)?计划|(?:implementation|execution|task)?\s*plan)"
|
|
4695
|
+
r"(?:\s*[—–-]\s*\S+)?",
|
|
4696
|
+
re.IGNORECASE,
|
|
4697
|
+
)
|
|
4618
4698
|
plans_root = change_dir / "plans"
|
|
4619
4699
|
primary = sorted(plans_root.glob("*-plan.md"))
|
|
4620
4700
|
secondary = [
|
|
@@ -4653,11 +4733,23 @@ def _business_goal_from_sources(change_dir: Path, events: list[dict[str, Any]])
|
|
|
4653
4733
|
break
|
|
4654
4734
|
if lines:
|
|
4655
4735
|
return lines[0]
|
|
4736
|
+
heading = re.search(r"(?m)^#\s+(.+?)\s*$", body)
|
|
4737
|
+
if heading:
|
|
4738
|
+
title = heading.group(1).strip()
|
|
4739
|
+
title = re.sub(
|
|
4740
|
+
r"\s*[—–-]\s*(?:任务拆分|任务表|实施计划|执行计划|计划)\s*$",
|
|
4741
|
+
"",
|
|
4742
|
+
title,
|
|
4743
|
+
).strip()
|
|
4744
|
+
if title and not generic_plan_heading.fullmatch(title):
|
|
4745
|
+
return title
|
|
4656
4746
|
first_task = re.search(r"(?m)^\s*\|\s*1\s*\|\s*([^|]+?)\s*\|", body)
|
|
4657
4747
|
if first_task:
|
|
4658
4748
|
return first_task.group(1).strip()
|
|
4659
4749
|
for line in body.splitlines():
|
|
4660
4750
|
clean = line.strip().lstrip("#").strip()
|
|
4751
|
+
if generic_plan_heading.fullmatch(clean):
|
|
4752
|
+
continue
|
|
4661
4753
|
if clean and not clean.startswith(("---", ">", "|")) and len(clean) > 8:
|
|
4662
4754
|
return clean
|
|
4663
4755
|
for event in events:
|
|
@@ -5328,10 +5420,47 @@ def _append_finalize_failure_terminal(
|
|
|
5328
5420
|
message=f"归档条件未满足:{message}",
|
|
5329
5421
|
note=f"归档操作 {operation_id} 未进入正式执行",
|
|
5330
5422
|
)
|
|
5423
|
+
append_event(
|
|
5424
|
+
authoritative_change_dir,
|
|
5425
|
+
phase="archive",
|
|
5426
|
+
type_="phase.end",
|
|
5427
|
+
status="BLOCKED",
|
|
5428
|
+
code="ARCHIVE_PREPARE_BLOCKED",
|
|
5429
|
+
message=f"归档条件未满足:{message}",
|
|
5430
|
+
note="归档未开始,流程已在预检处结束",
|
|
5431
|
+
)
|
|
5331
5432
|
except OSError:
|
|
5332
5433
|
pass
|
|
5333
5434
|
|
|
5334
5435
|
|
|
5436
|
+
def _best_effort_archive_terminal_sync(
|
|
5437
|
+
project_root: Path,
|
|
5438
|
+
event_dir: Path,
|
|
5439
|
+
*,
|
|
5440
|
+
change_key: str,
|
|
5441
|
+
run_id: str | None,
|
|
5442
|
+
payload: dict[str, Any],
|
|
5443
|
+
) -> None:
|
|
5444
|
+
"""Publish terminal archive state before any slow post-archive networking."""
|
|
5445
|
+
try:
|
|
5446
|
+
monitor = hes.auto_events_sync(
|
|
5447
|
+
project_root,
|
|
5448
|
+
event_dir,
|
|
5449
|
+
run_id=run_id,
|
|
5450
|
+
change_key=change_key,
|
|
5451
|
+
)
|
|
5452
|
+
payload.setdefault("steps", {})["platform_events_sync"] = monitor
|
|
5453
|
+
if monitor.get("warning"):
|
|
5454
|
+
payload.setdefault("warnings", []).append(str(monitor["warning"]))
|
|
5455
|
+
except Exception as exc: # noqa: BLE001 — monitoring cannot roll back archive
|
|
5456
|
+
warning = f"events-sync terminal hook failed: {exc}"
|
|
5457
|
+
payload.setdefault("warnings", []).append(warning)
|
|
5458
|
+
payload.setdefault("steps", {})["platform_events_sync"] = {
|
|
5459
|
+
"ok": False,
|
|
5460
|
+
"warning": warning,
|
|
5461
|
+
}
|
|
5462
|
+
|
|
5463
|
+
|
|
5335
5464
|
def _freeze_evidence_cutoff(work_dir: Path) -> dict[str, Any]:
|
|
5336
5465
|
"""Freeze the events cutoff: fsync events, write evidence-cutoff.json.
|
|
5337
5466
|
|
|
@@ -5599,7 +5728,11 @@ def validate_summary_data(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5599
5728
|
)
|
|
5600
5729
|
stage = summary.get("stageStatus") or {}
|
|
5601
5730
|
stage = stage if isinstance(stage, dict) else {}
|
|
5602
|
-
has_failed_stage = any(
|
|
5731
|
+
has_failed_stage = any(
|
|
5732
|
+
str(value).upper() == "FAIL"
|
|
5733
|
+
for phase, value in stage.items()
|
|
5734
|
+
if str(phase).lower() != "archive"
|
|
5735
|
+
)
|
|
5603
5736
|
if (has_skip or has_failed_verification or has_failed_stage) and final_status == "OK":
|
|
5604
5737
|
issues.append(
|
|
5605
5738
|
{
|
|
@@ -7374,6 +7507,7 @@ def cmd_finalize(
|
|
|
7374
7507
|
if split_state_dir is not None and split_state_dir.is_dir()
|
|
7375
7508
|
else original_change_dir
|
|
7376
7509
|
)
|
|
7510
|
+
monitoring_run_id = hes.run_id_for(original_change_dir)
|
|
7377
7511
|
operation_id = f"a-{uuid.uuid4().hex[:12]}"
|
|
7378
7512
|
operation_root = project_root / ".harness" / "archive-operations"
|
|
7379
7513
|
operation_temp_dir = operation_root / "staging" / operation_id / change_dir.name
|
|
@@ -7399,6 +7533,13 @@ def cmd_finalize(
|
|
|
7399
7533
|
)
|
|
7400
7534
|
except OSError:
|
|
7401
7535
|
pass
|
|
7536
|
+
_best_effort_archive_terminal_sync(
|
|
7537
|
+
project_root,
|
|
7538
|
+
authoritative_event_dir,
|
|
7539
|
+
change_key=change_name,
|
|
7540
|
+
run_id=monitoring_run_id,
|
|
7541
|
+
payload=payload,
|
|
7542
|
+
)
|
|
7402
7543
|
try:
|
|
7403
7544
|
write_json(
|
|
7404
7545
|
operation_record,
|
|
@@ -7479,12 +7620,17 @@ def cmd_finalize(
|
|
|
7479
7620
|
payload["original_preserved"] = original_change_dir.is_dir()
|
|
7480
7621
|
payload["finalStatus"] = "BLOCKED"
|
|
7481
7622
|
_append_finalize_failure_terminal(
|
|
7482
|
-
|
|
7483
|
-
if split_state_dir is not None and split_state_dir.is_dir()
|
|
7484
|
-
else original_change_dir,
|
|
7623
|
+
authoritative_event_dir,
|
|
7485
7624
|
payload["error"],
|
|
7486
7625
|
operation_id=operation_id,
|
|
7487
7626
|
)
|
|
7627
|
+
_best_effort_archive_terminal_sync(
|
|
7628
|
+
project_root,
|
|
7629
|
+
authoritative_event_dir,
|
|
7630
|
+
change_key=change_name,
|
|
7631
|
+
run_id=monitoring_run_id,
|
|
7632
|
+
payload=payload,
|
|
7633
|
+
)
|
|
7488
7634
|
return 1, payload
|
|
7489
7635
|
|
|
7490
7636
|
# The formal archive attempt starts after the one-shot preflight succeeds,
|
|
@@ -8253,7 +8399,6 @@ def cmd_finalize(
|
|
|
8253
8399
|
# as mutable staging, so a failed attempt cannot poison the next retry.
|
|
8254
8400
|
# Capture monitoring run_id from the live change path before it is deleted
|
|
8255
8401
|
# so archive finalize can report terminal status on the same platform run.
|
|
8256
|
-
monitoring_run_id = hes.run_id_for(original_change_dir)
|
|
8257
8402
|
try:
|
|
8258
8403
|
archive_root.mkdir(parents=True, exist_ok=True)
|
|
8259
8404
|
shutil.move(str(operation_temp_dir), str(archive_dir))
|
|
@@ -8300,11 +8445,56 @@ def cmd_finalize(
|
|
|
8300
8445
|
}
|
|
8301
8446
|
payload["knowledgeMaintenance"] = "REMOTE_PENDING"
|
|
8302
8447
|
|
|
8448
|
+
# Freeze the platform run immediately after the local archive is published.
|
|
8449
|
+
# Service shutdown, ZIP upload, and managed snapshot synchronization are
|
|
8450
|
+
# independent post-phase work and may wait on process/network deadlines.
|
|
8451
|
+
_best_effort_archive_terminal_sync(
|
|
8452
|
+
project_root,
|
|
8453
|
+
archive_dir,
|
|
8454
|
+
change_key=change_name,
|
|
8455
|
+
run_id=monitoring_run_id,
|
|
8456
|
+
payload=payload,
|
|
8457
|
+
)
|
|
8458
|
+
|
|
8303
8459
|
service_result = run_service_stop(work_dir)
|
|
8304
8460
|
payload["steps"]["service_stop"] = service_result
|
|
8305
8461
|
if service_result.get("warning"):
|
|
8306
8462
|
warnings.append(str(service_result["warning"]))
|
|
8307
8463
|
|
|
8464
|
+
# When remote credentials exist, upload one deterministic core-only ZIP.
|
|
8465
|
+
push_result = auto_push_archive_core(
|
|
8466
|
+
project_root,
|
|
8467
|
+
archive_dir,
|
|
8468
|
+
change_key=change_name,
|
|
8469
|
+
)
|
|
8470
|
+
payload["steps"]["archive_push"] = push_result
|
|
8471
|
+
payload["knowledgeMaintenance"] = _knowledge_maintenance_from_archive_push(
|
|
8472
|
+
push_result
|
|
8473
|
+
)
|
|
8474
|
+
if push_result.get("archiveStatus") == "durable":
|
|
8475
|
+
payload["archiveDurability"] = "ARCHIVED_REMOTE_DURABLE"
|
|
8476
|
+
if push_result.get("warning"):
|
|
8477
|
+
warnings.append(str(push_result["warning"]))
|
|
8478
|
+
|
|
8479
|
+
managed_snapshot = auto_push_managed_snapshot(project_root)
|
|
8480
|
+
payload["steps"]["managed_snapshot_push"] = managed_snapshot
|
|
8481
|
+
if managed_snapshot.get("warning"):
|
|
8482
|
+
warnings.append(str(managed_snapshot["warning"]))
|
|
8483
|
+
|
|
8484
|
+
archive_remote = {
|
|
8485
|
+
key: push_result.get(key)
|
|
8486
|
+
for key in (
|
|
8487
|
+
"archiveId",
|
|
8488
|
+
"archiveStatus",
|
|
8489
|
+
"knowledgeStatus",
|
|
8490
|
+
"uploadStatus",
|
|
8491
|
+
"packageSha256",
|
|
8492
|
+
"manifestSha256",
|
|
8493
|
+
"reasonCode",
|
|
8494
|
+
"remoteReceipt",
|
|
8495
|
+
)
|
|
8496
|
+
if push_result.get(key) is not None
|
|
8497
|
+
}
|
|
8308
8498
|
try:
|
|
8309
8499
|
write_json(
|
|
8310
8500
|
operation_record,
|
|
@@ -8321,41 +8511,14 @@ def cmd_finalize(
|
|
|
8321
8511
|
archive_dir / "evidence" / "archive-manifest-after.json"
|
|
8322
8512
|
),
|
|
8323
8513
|
"archiveDurability": payload.get("archiveDurability"),
|
|
8514
|
+
"knowledgeMaintenance": payload["knowledgeMaintenance"],
|
|
8515
|
+
"archiveRemote": archive_remote,
|
|
8516
|
+
"managedSnapshot": managed_snapshot,
|
|
8324
8517
|
},
|
|
8325
8518
|
)
|
|
8326
8519
|
except OSError as exc:
|
|
8327
8520
|
warnings.append(f"could not persist completed archive operation: {exc}")
|
|
8328
8521
|
|
|
8329
|
-
# When remote credentials exist, upload one deterministic core-only ZIP.
|
|
8330
|
-
push_result = auto_push_archive_core(
|
|
8331
|
-
project_root,
|
|
8332
|
-
archive_dir,
|
|
8333
|
-
change_key=change_name,
|
|
8334
|
-
)
|
|
8335
|
-
payload["steps"]["archive_push"] = push_result
|
|
8336
|
-
payload["knowledgeMaintenance"] = _knowledge_maintenance_from_archive_push(
|
|
8337
|
-
push_result
|
|
8338
|
-
)
|
|
8339
|
-
if push_result.get("warning"):
|
|
8340
|
-
warnings.append(str(push_result["warning"]))
|
|
8341
|
-
|
|
8342
|
-
# C3: best-effort terminal events-sync so platform run ends (succeeded/failed).
|
|
8343
|
-
# Use the pre-delete change-path run_id + original change_key for continuity.
|
|
8344
|
-
try:
|
|
8345
|
-
monitor = hes.auto_events_sync(
|
|
8346
|
-
project_root,
|
|
8347
|
-
archive_dir,
|
|
8348
|
-
run_id=monitoring_run_id,
|
|
8349
|
-
change_key=change_name,
|
|
8350
|
-
)
|
|
8351
|
-
payload["steps"]["platform_events_sync"] = monitor
|
|
8352
|
-
if monitor.get("warning"):
|
|
8353
|
-
warnings.append(str(monitor["warning"]))
|
|
8354
|
-
except Exception as exc: # noqa: BLE001 — never roll back archive
|
|
8355
|
-
warn = f"events-sync terminal hook failed: {exc}"
|
|
8356
|
-
warnings.append(warn)
|
|
8357
|
-
payload["steps"]["platform_events_sync"] = {"ok": False, "warning": warn}
|
|
8358
|
-
|
|
8359
8522
|
payload["ok"] = True
|
|
8360
8523
|
payload["finalStatus"] = "OK"
|
|
8361
8524
|
payload["warnings"] = warnings
|
|
@@ -8877,6 +9040,82 @@ def build_archive_package(
|
|
|
8877
9040
|
}
|
|
8878
9041
|
|
|
8879
9042
|
|
|
9043
|
+
def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
9044
|
+
"""Best-effort upload of rules and Codebase Map through the normal push protocol."""
|
|
9045
|
+
project_root = project_root.resolve()
|
|
9046
|
+
credentials = _resolve_archive_remote_credentials(project_root, os.environ)
|
|
9047
|
+
if not credentials["configured"]:
|
|
9048
|
+
return {
|
|
9049
|
+
"ok": False,
|
|
9050
|
+
"skipped": True,
|
|
9051
|
+
"reasonCode": "MANAGED_SNAPSHOT_CREDENTIALS_MISSING",
|
|
9052
|
+
"missingCredentials": credentials["missing"],
|
|
9053
|
+
}
|
|
9054
|
+
try:
|
|
9055
|
+
launcher = resolve_npx_launcher()
|
|
9056
|
+
except OSError as exc:
|
|
9057
|
+
return {
|
|
9058
|
+
"ok": False,
|
|
9059
|
+
"skipped": False,
|
|
9060
|
+
"reasonCode": "MANAGED_SNAPSHOT_UPLOAD_DEFERRED",
|
|
9061
|
+
"warning": f"项目规则与架构地图上传已延后:{exc}",
|
|
9062
|
+
}
|
|
9063
|
+
command = [
|
|
9064
|
+
*launcher,
|
|
9065
|
+
"--yes",
|
|
9066
|
+
"hunter-harness",
|
|
9067
|
+
"push",
|
|
9068
|
+
"--yes",
|
|
9069
|
+
"--non-interactive",
|
|
9070
|
+
"--json",
|
|
9071
|
+
"--server-url",
|
|
9072
|
+
str(credentials["serverUrl"]),
|
|
9073
|
+
]
|
|
9074
|
+
if credentials["tokenEnv"] is not None:
|
|
9075
|
+
command.extend(["--token-env", str(credentials["tokenEnv"])])
|
|
9076
|
+
try:
|
|
9077
|
+
completed = subprocess.run(
|
|
9078
|
+
command,
|
|
9079
|
+
cwd=str(project_root),
|
|
9080
|
+
text=True,
|
|
9081
|
+
stdout=subprocess.PIPE,
|
|
9082
|
+
stderr=subprocess.PIPE,
|
|
9083
|
+
timeout=180,
|
|
9084
|
+
shell=False,
|
|
9085
|
+
)
|
|
9086
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
9087
|
+
return {
|
|
9088
|
+
"ok": False,
|
|
9089
|
+
"skipped": False,
|
|
9090
|
+
"reasonCode": "MANAGED_SNAPSHOT_UPLOAD_DEFERRED",
|
|
9091
|
+
"warning": f"项目规则与架构地图上传已延后:{exc}",
|
|
9092
|
+
}
|
|
9093
|
+
try:
|
|
9094
|
+
output = json.loads(completed.stdout)
|
|
9095
|
+
except (json.JSONDecodeError, TypeError):
|
|
9096
|
+
output = None
|
|
9097
|
+
if completed.returncode != 0 or not isinstance(output, dict):
|
|
9098
|
+
return {
|
|
9099
|
+
"ok": False,
|
|
9100
|
+
"skipped": False,
|
|
9101
|
+
"exitCode": completed.returncode,
|
|
9102
|
+
"reasonCode": "MANAGED_SNAPSHOT_UPLOAD_FAILED",
|
|
9103
|
+
"warning": (
|
|
9104
|
+
"项目规则与架构地图未能同步到平台;归档不受影响,"
|
|
9105
|
+
"稍后可运行 npx hunter-harness push --yes --non-interactive 重试。"
|
|
9106
|
+
),
|
|
9107
|
+
}
|
|
9108
|
+
summary = output.get("summary")
|
|
9109
|
+
submitted = summary.get("submitted", 0) if isinstance(summary, dict) else 0
|
|
9110
|
+
return {
|
|
9111
|
+
"ok": bool(output.get("ok", True)),
|
|
9112
|
+
"skipped": False,
|
|
9113
|
+
"projectId": output.get("project_id"),
|
|
9114
|
+
"submitted": submitted if isinstance(submitted, int) else 0,
|
|
9115
|
+
"unchanged": "MANAGED_SNAPSHOT_UNCHANGED" in output.get("warnings", []),
|
|
9116
|
+
}
|
|
9117
|
+
|
|
9118
|
+
|
|
8880
9119
|
def auto_push_archive_core(
|
|
8881
9120
|
project_root: Path,
|
|
8882
9121
|
archive_dir: Path,
|
|
@@ -8913,6 +9152,12 @@ def auto_push_archive_core(
|
|
|
8913
9152
|
label="archive upload receipt",
|
|
8914
9153
|
must_exist=False,
|
|
8915
9154
|
)
|
|
9155
|
+
remote_receipt_path = _require_archive_project_path(
|
|
9156
|
+
project_root,
|
|
9157
|
+
package_path.with_suffix(".remote.json"),
|
|
9158
|
+
label="durable archive receipt",
|
|
9159
|
+
must_exist=False,
|
|
9160
|
+
)
|
|
8916
9161
|
except (OSError, ValueError) as exc:
|
|
8917
9162
|
return {
|
|
8918
9163
|
"skipped": False,
|
|
@@ -9153,6 +9398,45 @@ def auto_push_archive_core(
|
|
|
9153
9398
|
else:
|
|
9154
9399
|
upload_status = "pending"
|
|
9155
9400
|
reason_code = "ARCHIVE_KNOWLEDGE_INDEXING"
|
|
9401
|
+
durable_receipt = {
|
|
9402
|
+
"schemaVersion": 1,
|
|
9403
|
+
"recordedAt": now_iso(),
|
|
9404
|
+
"changeKey": effective_change_key,
|
|
9405
|
+
"archiveId": cli_payload.get("archive_id"),
|
|
9406
|
+
"archiveStatus": archive_status,
|
|
9407
|
+
"knowledgeStatus": knowledge_status,
|
|
9408
|
+
"uploadStatus": upload_status,
|
|
9409
|
+
"packageSha256": package["packageSha256"],
|
|
9410
|
+
"manifestSha256": package["manifestSha256"],
|
|
9411
|
+
"fileCount": package["fileCount"],
|
|
9412
|
+
}
|
|
9413
|
+
try:
|
|
9414
|
+
remote_receipt_path.parent.mkdir(parents=True, exist_ok=True)
|
|
9415
|
+
write_json(remote_receipt_path, durable_receipt)
|
|
9416
|
+
except OSError as exc:
|
|
9417
|
+
try:
|
|
9418
|
+
persist_receipt(
|
|
9419
|
+
upload_status=upload_status,
|
|
9420
|
+
reason_code="ARCHIVE_DURABLE_RECEIPT_WRITE_FAILED",
|
|
9421
|
+
archive_status=archive_status,
|
|
9422
|
+
knowledge_status=knowledge_status,
|
|
9423
|
+
exit_code=completed.returncode,
|
|
9424
|
+
)
|
|
9425
|
+
except OSError:
|
|
9426
|
+
pass
|
|
9427
|
+
return {
|
|
9428
|
+
**base_result,
|
|
9429
|
+
"ok": True,
|
|
9430
|
+
"uploadStatus": upload_status,
|
|
9431
|
+
"archiveId": cli_payload.get("archive_id"),
|
|
9432
|
+
"archiveStatus": archive_status,
|
|
9433
|
+
"knowledgeStatus": knowledge_status,
|
|
9434
|
+
"reasonCode": "ARCHIVE_DURABLE_RECEIPT_WRITE_FAILED",
|
|
9435
|
+
"warning": (
|
|
9436
|
+
"服务端归档已持久化,但无法写入本地归档状态回执;"
|
|
9437
|
+
f"已保留重试文件:{exc}"
|
|
9438
|
+
),
|
|
9439
|
+
}
|
|
9156
9440
|
try:
|
|
9157
9441
|
persist_receipt(
|
|
9158
9442
|
upload_status=upload_status,
|
|
@@ -9180,6 +9464,7 @@ def auto_push_archive_core(
|
|
|
9180
9464
|
"archiveStatus": archive_status,
|
|
9181
9465
|
"knowledgeStatus": knowledge_status,
|
|
9182
9466
|
"reasonCode": reason_code,
|
|
9467
|
+
"remoteReceipt": str(remote_receipt_path),
|
|
9183
9468
|
}
|
|
9184
9469
|
if fully_ready:
|
|
9185
9470
|
cleanup_warnings: list[str] = []
|
|
@@ -9,12 +9,19 @@ import hashlib
|
|
|
9
9
|
import json
|
|
10
10
|
import os
|
|
11
11
|
import subprocess
|
|
12
|
+
import sys
|
|
12
13
|
import tempfile
|
|
13
14
|
import time
|
|
14
15
|
from contextlib import contextmanager
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Any
|
|
17
18
|
|
|
19
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
20
|
+
if str(SCRIPTS_DIR) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
22
|
+
|
|
23
|
+
import harness_ledger as hl # noqa: E402
|
|
24
|
+
|
|
18
25
|
|
|
19
26
|
PHASE_GRAPH = {
|
|
20
27
|
"plan": ("run",),
|
|
@@ -309,6 +316,14 @@ def configure_phase_plan(
|
|
|
309
316
|
"ok": False,
|
|
310
317
|
"code": "PHASE_PLAN_JUSTIFICATION_REQUIRED",
|
|
311
318
|
}
|
|
319
|
+
worktree_path = contract_root / "meta" / "worktree.json"
|
|
320
|
+
if worktree_path.is_file():
|
|
321
|
+
try:
|
|
322
|
+
worktree = _read_json(worktree_path)
|
|
323
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
324
|
+
worktree = {}
|
|
325
|
+
if bool(worktree.get("requested") or worktree.get("created")) and "merge" not in normalized:
|
|
326
|
+
normalized.insert(normalized.index("archive"), "merge")
|
|
312
327
|
policy_path = contract_root / "meta" / "gate-policy.json"
|
|
313
328
|
policy = _read_json(policy_path) if policy_path.is_file() else {"schemaVersion": 1}
|
|
314
329
|
skipped = [
|
|
@@ -382,28 +397,16 @@ def _parse_time(value: Any) -> dt.datetime | None:
|
|
|
382
397
|
|
|
383
398
|
|
|
384
399
|
def _execution_root(project: Path, contract_root: Path, state_root: Path) -> Path:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
):
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
path = worktree.get("path")
|
|
396
|
-
if not path and worktree.get("worktreeRoot"):
|
|
397
|
-
path = Path(str(worktree["worktreeRoot"])) / contract_root.name
|
|
398
|
-
if path:
|
|
399
|
-
unresolved = Path(str(path))
|
|
400
|
-
resolved = (
|
|
401
|
-
unresolved.resolve()
|
|
402
|
-
if unresolved.is_absolute()
|
|
403
|
-
else (project / unresolved).resolve()
|
|
404
|
-
)
|
|
405
|
-
if resolved.is_dir() and _same_repository(project, resolved):
|
|
406
|
-
return resolved
|
|
400
|
+
declared = False
|
|
401
|
+
# Context、ledger 与 gate 必须以同一份可信 worktree 契约为准。split
|
|
402
|
+
# state 可能携带运行元数据,因此两个根都交给统一解析器检查。
|
|
403
|
+
for change_root in (state_root, contract_root):
|
|
404
|
+
inferred = hl.infer_execution_project_root(change_root)
|
|
405
|
+
if inferred is not None:
|
|
406
|
+
return inferred
|
|
407
|
+
declared = declared or hl.declares_execution_worktree(change_root)
|
|
408
|
+
if declared:
|
|
409
|
+
raise ValueError("EXECUTION_WORKTREE_INVALID")
|
|
407
410
|
return project.resolve()
|
|
408
411
|
|
|
409
412
|
|
|
@@ -595,7 +598,14 @@ def _claim_prepared_context(
|
|
|
595
598
|
"toPhase": phase,
|
|
596
599
|
}
|
|
597
600
|
|
|
598
|
-
|
|
601
|
+
try:
|
|
602
|
+
execution_root = _execution_root(project, contract_root, state_root)
|
|
603
|
+
except ValueError as exc:
|
|
604
|
+
return {
|
|
605
|
+
"ok": False,
|
|
606
|
+
"code": "EXECUTION_WORKTREE_INVALID",
|
|
607
|
+
"error": str(exc),
|
|
608
|
+
}
|
|
599
609
|
now = _now()
|
|
600
610
|
lease: dict[str, Any] = {
|
|
601
611
|
"schemaVersion": 1,
|
|
@@ -950,10 +960,18 @@ def _begin_transition_unlocked(
|
|
|
950
960
|
) -> dict[str, Any]:
|
|
951
961
|
project = Path(project).resolve()
|
|
952
962
|
try:
|
|
953
|
-
|
|
963
|
+
contract_root, _contract_data, state_root = _contract(project, change)
|
|
954
964
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
955
965
|
return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
|
|
956
966
|
paths = _paths(state_root)
|
|
967
|
+
try:
|
|
968
|
+
execution_root = _execution_root(project, contract_root, state_root)
|
|
969
|
+
except ValueError as exc:
|
|
970
|
+
return {
|
|
971
|
+
"ok": False,
|
|
972
|
+
"code": "EXECUTION_WORKTREE_INVALID",
|
|
973
|
+
"error": str(exc),
|
|
974
|
+
}
|
|
957
975
|
if preparation_id is not None:
|
|
958
976
|
current = _read_json(paths["current"]) if paths["current"].is_file() else None
|
|
959
977
|
lease = _read_json(paths["lease"]) if paths["lease"].is_file() else None
|
|
@@ -1027,7 +1045,7 @@ def _begin_transition_unlocked(
|
|
|
1027
1045
|
"changeName": change,
|
|
1028
1046
|
"phase": phase,
|
|
1029
1047
|
"executor": executor,
|
|
1030
|
-
"executionRoot": str(
|
|
1048
|
+
"executionRoot": str(execution_root),
|
|
1031
1049
|
"receiptHash": receipt["receiptHash"],
|
|
1032
1050
|
"begunAt": acknowledgment["begunAt"],
|
|
1033
1051
|
}
|