@hunter-harness/workflow-harness 0.2.18 → 0.2.20

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.
Files changed (170) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +7 -1
  3. package/harness/bundles/general/claude-code/harness-archive/checklist.md +6 -0
  4. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +11 -2
  11. package/harness/bundles/general/claude-code/harness-submit/checklist.md +20 -2
  12. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +2 -0
  15. package/harness/bundles/general/claude-code/scripts/harness_archive.py +199 -30
  16. package/harness/bundles/general/claude-code/scripts/harness_events.py +5 -18
  17. package/harness/bundles/general/claude-code/scripts/harness_integration.py +153 -0
  18. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +37 -11
  19. package/harness/bundles/general/claude-code/scripts/harness_paths.py +170 -1
  20. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  21. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +7 -1
  22. package/harness/bundles/general/codebuddy/harness-archive/checklist.md +6 -0
  23. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  24. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +11 -2
  30. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +20 -2
  31. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +2 -0
  34. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +199 -30
  35. package/harness/bundles/general/codebuddy/scripts/harness_events.py +5 -18
  36. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +153 -0
  37. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +37 -11
  38. package/harness/bundles/general/codebuddy/scripts/harness_paths.py +170 -1
  39. package/harness/bundles/general/codex/.harness-build.json +1 -1
  40. package/harness/bundles/general/codex/harness-archive/SKILL.md +7 -1
  41. package/harness/bundles/general/codex/harness-archive/checklist.md +6 -0
  42. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  43. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  44. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  45. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  46. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-submit/SKILL.md +11 -2
  49. package/harness/bundles/general/codex/harness-submit/checklist.md +20 -2
  50. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  52. package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +2 -0
  53. package/harness/bundles/general/codex/scripts/harness_archive.py +199 -30
  54. package/harness/bundles/general/codex/scripts/harness_events.py +5 -18
  55. package/harness/bundles/general/codex/scripts/harness_integration.py +153 -0
  56. package/harness/bundles/general/codex/scripts/harness_ledger.py +37 -11
  57. package/harness/bundles/general/codex/scripts/harness_paths.py +170 -1
  58. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  59. package/harness/bundles/general/cursor/harness-archive/SKILL.md +7 -1
  60. package/harness/bundles/general/cursor/harness-archive/checklist.md +6 -0
  61. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  62. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  63. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  64. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  65. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  66. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  67. package/harness/bundles/general/cursor/harness-submit/SKILL.md +11 -2
  68. package/harness/bundles/general/cursor/harness-submit/checklist.md +20 -2
  69. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  70. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +2 -0
  72. package/harness/bundles/general/cursor/scripts/harness_archive.py +199 -30
  73. package/harness/bundles/general/cursor/scripts/harness_events.py +5 -18
  74. package/harness/bundles/general/cursor/scripts/harness_integration.py +153 -0
  75. package/harness/bundles/general/cursor/scripts/harness_ledger.py +37 -11
  76. package/harness/bundles/general/cursor/scripts/harness_paths.py +170 -1
  77. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  78. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  79. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +7 -1
  80. package/harness/bundles/java/claude-code/harness-archive/checklist.md +6 -0
  81. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  82. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  83. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  84. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  85. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  86. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  87. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  88. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +11 -2
  89. package/harness/bundles/java/claude-code/harness-submit/checklist.md +20 -2
  90. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  91. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  92. package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +2 -0
  93. package/harness/bundles/java/claude-code/scripts/harness_archive.py +199 -30
  94. package/harness/bundles/java/claude-code/scripts/harness_events.py +5 -18
  95. package/harness/bundles/java/claude-code/scripts/harness_integration.py +153 -0
  96. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +37 -11
  97. package/harness/bundles/java/claude-code/scripts/harness_paths.py +170 -1
  98. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  99. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  100. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +7 -1
  101. package/harness/bundles/java/codebuddy/harness-archive/checklist.md +6 -0
  102. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  103. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  104. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  105. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  106. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  107. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  108. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  109. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +11 -2
  110. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +20 -2
  111. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  112. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  113. package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +2 -0
  114. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +199 -30
  115. package/harness/bundles/java/codebuddy/scripts/harness_events.py +5 -18
  116. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +153 -0
  117. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +37 -11
  118. package/harness/bundles/java/codebuddy/scripts/harness_paths.py +170 -1
  119. package/harness/bundles/java/codex/.harness-build.json +1 -1
  120. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  121. package/harness/bundles/java/codex/harness-archive/SKILL.md +7 -1
  122. package/harness/bundles/java/codex/harness-archive/checklist.md +6 -0
  123. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  124. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  125. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  126. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  127. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  128. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  129. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  130. package/harness/bundles/java/codex/harness-submit/SKILL.md +11 -2
  131. package/harness/bundles/java/codex/harness-submit/checklist.md +20 -2
  132. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  133. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  134. package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +2 -0
  135. package/harness/bundles/java/codex/scripts/harness_archive.py +199 -30
  136. package/harness/bundles/java/codex/scripts/harness_events.py +5 -18
  137. package/harness/bundles/java/codex/scripts/harness_integration.py +153 -0
  138. package/harness/bundles/java/codex/scripts/harness_ledger.py +37 -11
  139. package/harness/bundles/java/codex/scripts/harness_paths.py +170 -1
  140. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  141. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  142. package/harness/bundles/java/cursor/harness-archive/SKILL.md +7 -1
  143. package/harness/bundles/java/cursor/harness-archive/checklist.md +6 -0
  144. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  145. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  146. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  147. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  148. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  149. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  150. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  151. package/harness/bundles/java/cursor/harness-submit/SKILL.md +11 -2
  152. package/harness/bundles/java/cursor/harness-submit/checklist.md +20 -2
  153. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  154. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  155. package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +2 -0
  156. package/harness/bundles/java/cursor/scripts/harness_archive.py +199 -30
  157. package/harness/bundles/java/cursor/scripts/harness_events.py +5 -18
  158. package/harness/bundles/java/cursor/scripts/harness_integration.py +153 -0
  159. package/harness/bundles/java/cursor/scripts/harness_ledger.py +37 -11
  160. package/harness/bundles/java/cursor/scripts/harness_paths.py +170 -1
  161. package/harness/manifests/general/claude-code.json +19 -19
  162. package/harness/manifests/general/codebuddy.json +19 -19
  163. package/harness/manifests/general/codex.json +19 -19
  164. package/harness/manifests/general/cursor.json +19 -19
  165. package/harness/manifests/java/claude-code.json +21 -21
  166. package/harness/manifests/java/codebuddy.json +21 -21
  167. package/harness/manifests/java/codex.json +21 -21
  168. package/harness/manifests/java/cursor.json +21 -21
  169. package/hunter-workflow-family.json +1 -1
  170. package/package.json +1 -1
@@ -109,6 +109,10 @@ class CleanupResidualError(IntegrationError):
109
109
  )
110
110
 
111
111
 
112
+ class AbandonRefusedError(IntegrationError):
113
+ code = "ABANDON_REFUSED"
114
+
115
+
112
116
  class JournalConflictError(IntegrationError):
113
117
  code = "JOURNAL_CONFLICT"
114
118
 
@@ -740,6 +744,8 @@ class IntegrationTransaction:
740
744
  str(intg_root),
741
745
  self.temp_branch,
742
746
  )
747
+ # H-5: LF-stable hashing in the integration worktree.
748
+ self.runner.run(intg_root, "config", "core.autocrlf", "false")
743
749
 
744
750
  return self._run_step("prepare", action)
745
751
 
@@ -1195,6 +1201,148 @@ class IntegrationTransaction:
1195
1201
  harness_change.integration_lock_release(self.project_root, run_id=self.run_id)
1196
1202
  return journal
1197
1203
 
1204
+ # ------------------------------------------------------------ abandon
1205
+
1206
+ def _remote_contains_merge(self, journal: dict[str, Any]) -> bool:
1207
+ merge_commit = str(journal.get("mergeCommit") or "")
1208
+ if not merge_commit:
1209
+ return False
1210
+ remote_line = self.runner.text(
1211
+ self.project_root, "ls-remote", "origin", self.target_branch
1212
+ )
1213
+ observed_remote = (
1214
+ remote_line.split()[0] if remote_line and remote_line.split() else ""
1215
+ )
1216
+ return bool(observed_remote and observed_remote == merge_commit)
1217
+
1218
+ def _force_remove_integration_root(
1219
+ self, journal: dict[str, Any], intg_root: Path
1220
+ ) -> dict[str, Any]:
1221
+ """Best-effort remove of the integration worktree for abandon only."""
1222
+ allowed = Path(str(journal.get("allowedCleanupRoot") or "")).resolve()
1223
+ resolved = harness_paths.assert_path_within(intg_root, self.temp_root)
1224
+ if not allowed or resolved != allowed:
1225
+ raise CleanupRefusedError(
1226
+ f"refusing to abandon non-transaction path: {resolved}"
1227
+ )
1228
+ self.runner.run(
1229
+ self.project_root,
1230
+ "worktree",
1231
+ "remove",
1232
+ "--force",
1233
+ str(resolved),
1234
+ check=False,
1235
+ )
1236
+ residual: list[str] = []
1237
+ if resolved.exists():
1238
+ for name in HEAVY_WORKTREE_ROOTS:
1239
+ heavy = resolved / name
1240
+ if heavy.exists():
1241
+ harness_paths.assert_path_within(heavy, resolved)
1242
+ try:
1243
+ if heavy.is_dir() and not heavy.is_symlink():
1244
+ shutil.rmtree(heavy)
1245
+ else:
1246
+ heavy.unlink()
1247
+ except OSError:
1248
+ residual.append(name)
1249
+ if resolved.exists():
1250
+ try:
1251
+ shutil.rmtree(resolved)
1252
+ except OSError:
1253
+ residual.append(str(resolved))
1254
+ listed = self.runner.text(self.project_root, "worktree", "list", "--porcelain")
1255
+ still_registered = str(resolved) in {
1256
+ str(Path(line.removeprefix("worktree ")).resolve())
1257
+ for line in (listed or "").splitlines()
1258
+ if line.startswith("worktree ")
1259
+ }
1260
+ ok = not still_registered and not residual and not resolved.exists()
1261
+ return {
1262
+ "ok": ok,
1263
+ "code": "CLEANUP_COMPLETE" if ok else "CLEANUP_RESIDUAL",
1264
+ "target": str(resolved),
1265
+ "worktreeRegistered": still_registered,
1266
+ "diskPathPresent": resolved.exists(),
1267
+ "residualPaths": residual,
1268
+ }
1269
+
1270
+ def abandon(self) -> dict[str, Any]:
1271
+ """Official recovery for a failed integration transaction (H-6).
1272
+
1273
+ Allowed only when push is not DONE and the remote does not already
1274
+ contain ``mergeCommit``. Removes the integration worktree, temp branch,
1275
+ protection refs, and integration lock. Never deletes the feature
1276
+ worktree or feature branch.
1277
+ """
1278
+ try:
1279
+ journal = self._load()
1280
+ except FileNotFoundError as exc:
1281
+ raise AbandonRefusedError(
1282
+ f"no journal for transaction {self.transaction_id}"
1283
+ ) from exc
1284
+
1285
+ push_status = str(self._step(journal, "push").get("status") or "")
1286
+ push_intent = journal.get("pushIntent") or {}
1287
+ push_confirmed = (
1288
+ push_status == "DONE"
1289
+ or (
1290
+ isinstance(push_intent, dict)
1291
+ and push_intent.get("status") == "REMOTE_CONFIRMED"
1292
+ )
1293
+ or bool(journal.get("pushedHead"))
1294
+ or bool(journal.get("mergeFinalHash"))
1295
+ )
1296
+ remote_has_merge = self._remote_contains_merge(journal)
1297
+ if push_confirmed or remote_has_merge:
1298
+ raise AbandonRefusedError(
1299
+ "refuse abandon: push succeeded or remote already contains "
1300
+ "the merge commit; use cleanup instead"
1301
+ )
1302
+
1303
+ intg_root = Path(str(journal.get("integrationRoot") or ""))
1304
+ cleanup_result: dict[str, Any] = {
1305
+ "ok": True,
1306
+ "code": "ALREADY_ABSENT",
1307
+ "target": str(intg_root) if intg_root else "",
1308
+ "worktreeRegistered": False,
1309
+ "diskPathPresent": False,
1310
+ "residualPaths": [],
1311
+ }
1312
+ prepare_done = self._step(journal, "prepare").get("status") == "DONE"
1313
+ if prepare_done and str(journal.get("integrationRoot") or "").strip():
1314
+ try:
1315
+ cleanup_result = self.cleanup_target(intg_root)
1316
+ except CleanupRefusedError:
1317
+ cleanup_result = self._force_remove_integration_root(journal, intg_root)
1318
+
1319
+ self.runner.run(
1320
+ self.project_root, "branch", "-D", self.temp_branch, check=False
1321
+ )
1322
+ refs = journal.get("protectionRefs") or {}
1323
+ if isinstance(refs, dict):
1324
+ for ref in (refs.get("base"), refs.get("head")):
1325
+ if ref:
1326
+ self.runner.run(
1327
+ self.project_root, "update-ref", "-d", str(ref), check=False
1328
+ )
1329
+
1330
+ harness_change.integration_lock_release(self.project_root, run_id=self.run_id)
1331
+
1332
+ journal = self._load()
1333
+ journal["abandonedAt"] = now_iso()
1334
+ journal["abandonResult"] = cleanup_result
1335
+ self._save(journal)
1336
+
1337
+ return {
1338
+ "ok": True,
1339
+ "code": "ABANDON_COMPLETE",
1340
+ "transactionId": self.transaction_id,
1341
+ "cleanup": cleanup_result,
1342
+ "featureBranchRetained": True,
1343
+ "featureWorktreeRetained": True,
1344
+ }
1345
+
1198
1346
  # ------------------------------------------------------------ recover
1199
1347
 
1200
1348
  def recover(
@@ -1282,6 +1430,10 @@ def cmd_cleanup(args: argparse.Namespace) -> int:
1282
1430
  return _emit(_txn_from_args(args).cleanup())
1283
1431
 
1284
1432
 
1433
+ def cmd_abandon(args: argparse.Namespace) -> int:
1434
+ return _emit(_txn_from_args(args).abandon())
1435
+
1436
+
1285
1437
  def cmd_recover(args: argparse.Namespace) -> int:
1286
1438
  return _emit(_txn_from_args(args).recover())
1287
1439
 
@@ -1333,6 +1485,7 @@ def build_parser() -> argparse.ArgumentParser:
1333
1485
  add("merge", cmd_merge)
1334
1486
  add("push", cmd_push)
1335
1487
  add("cleanup", cmd_cleanup)
1488
+ add("abandon", cmd_abandon)
1336
1489
  add("recover", cmd_recover)
1337
1490
  add("status", cmd_status)
1338
1491
 
@@ -28,6 +28,7 @@ if str(SCRIPTS_DIR) not in sys.path:
28
28
  sys.path.insert(0, str(SCRIPTS_DIR))
29
29
 
30
30
  import harness_paths # noqa: E402
31
+ import harness_profile # noqa: E402
31
32
 
32
33
 
33
34
  if hasattr(sys.stdout, "reconfigure"):
@@ -131,8 +132,19 @@ def emit_compact_or_verbose(
131
132
  emit_json(out, as_json=as_json)
132
133
 
133
134
 
134
- def emit_error(message: str, *, as_json: bool, code: int = 1) -> int:
135
- payload = {"ok": False, "error": message}
135
+ def emit_error(
136
+ message: str,
137
+ *,
138
+ as_json: bool,
139
+ code: int = 1,
140
+ error_code: str | None = None,
141
+ extra: dict[str, Any] | None = None,
142
+ ) -> int:
143
+ payload: dict[str, Any] = {"ok": False, "error": message}
144
+ if error_code:
145
+ payload["code"] = error_code
146
+ if extra:
147
+ payload.update(extra)
136
148
  if as_json:
137
149
  sys.stderr.write(json.dumps(payload, ensure_ascii=False) + "\n")
138
150
  else:
@@ -523,18 +535,30 @@ def expand_profile_input_files(
523
535
  ) -> tuple[list[str], str | None]:
524
536
  """Expand verificationInputs[profile_input] globs from build-profile.json.
525
537
 
526
- Globs are relative to project; only files inside project are kept
527
- (deduped, path-sorted). Returns (files, error); error is None on success.
538
+ Profile is loaded via ``harness_profile.load_profile`` (C7: common_root then
539
+ execution overlay) so linked worktrees without a local build-profile still
540
+ reuse the main checkout profile. Globs remain relative to the execution
541
+ ``project`` root (not common_root) so inputsHash tracks the tree under test.
542
+
543
+ Returns (files, error); error is None on success.
528
544
  profile 缺失 / key 缺失 / glob 无匹配 / 结果为空 → 返回 ([], "<reason>"),
529
545
  调用方据此返回 insufficient-evidence,执行全量测试但不允许缓存复用。
530
546
  """
531
- profile_path = project / ".harness" / "config" / "build-profile.json"
532
- if not profile_path.is_file():
547
+ profile = harness_profile.load_profile(Path(project))
548
+ if profile is None:
549
+ # load_profile swallows JSON errors; restore actionable unreadable diag
550
+ # when a profile file exists but cannot be parsed (review YELLOW-1).
551
+ project_root = Path(project).resolve()
552
+ common = harness_paths.common_root(project_root)
553
+ for root in (common, project_root):
554
+ candidate = root / ".harness" / "config" / "build-profile.json"
555
+ if not candidate.is_file():
556
+ continue
557
+ try:
558
+ json.loads(candidate.read_text(encoding="utf-8-sig"))
559
+ except (OSError, json.JSONDecodeError) as exc:
560
+ return [], f"build-profile.json unreadable: {exc}"
533
561
  return [], "build-profile.json missing; run harness_preflight.py detect"
534
- try:
535
- profile = json.loads(profile_path.read_text(encoding="utf-8-sig"))
536
- except (OSError, json.JSONDecodeError) as exc:
537
- return [], f"build-profile.json unreadable: {exc}"
538
562
  if not isinstance(profile, dict):
539
563
  return [], "build-profile.json is not an object"
540
564
  inputs = profile.get("verificationInputs")
@@ -544,7 +568,7 @@ def expand_profile_input_files(
544
568
  if not isinstance(patterns, list) or not patterns:
545
569
  return [], f"verificationInputs.{profile_input} is empty or invalid"
546
570
 
547
- base = project.resolve()
571
+ base = Path(project).resolve()
548
572
  seen: set[str] = set()
549
573
  for pat in patterns:
550
574
  if not isinstance(pat, str) or not pat.strip():
@@ -1520,6 +1544,8 @@ def cmd_record(args: argparse.Namespace) -> int:
1520
1544
  return emit_error(
1521
1545
  "ledger identity incomplete: " + ", ".join(missing),
1522
1546
  as_json=as_json,
1547
+ error_code="LEDGER_IDENTITY_INVALID",
1548
+ extra={"missing": missing},
1523
1549
  )
1524
1550
  write_ledger(out_path, ledger)
1525
1551
  except (OSError, ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
@@ -20,7 +20,7 @@ import re
20
20
  import subprocess
21
21
  import sys
22
22
  from pathlib import Path
23
- from typing import Any
23
+ from typing import Any, Sequence
24
24
 
25
25
  SCRIPTS_DIR = Path(__file__).resolve().parent
26
26
  if str(SCRIPTS_DIR) not in sys.path:
@@ -289,6 +289,175 @@ def assert_path_within(
289
289
  return resolved
290
290
 
291
291
 
292
+ class CleanupTopologyError(ValueError):
293
+ """Raised when cleanup would delete or traverse into state/archive roots."""
294
+
295
+ code = "CLEANUP_TOPOLOGY_REFUSED"
296
+
297
+
298
+ # Skip descending into these when scanning for junction/symlink escapes.
299
+ # Matches integration HEAVY_WORKTREE_ROOTS plus .git (topology scan only).
300
+ CLEANUP_WALK_SKIP_DIRS = frozenset(
301
+ {
302
+ "node_modules",
303
+ ".venv",
304
+ "venv",
305
+ "build",
306
+ "dist",
307
+ "target",
308
+ ".cache",
309
+ "__pycache__",
310
+ ".git",
311
+ }
312
+ )
313
+
314
+
315
+ def assert_cleanup_safe(
316
+ cleanup_root: Path,
317
+ state_roots: Sequence[Path] | None = None,
318
+ archive_roots: Sequence[Path] | None = None,
319
+ ) -> Path:
320
+ """Refuse cleanup when state/archive roots resolve inside the cleanup tree.
321
+
322
+ Also refuses when a junction/symlink under ``cleanup_root`` resolves into a
323
+ protected state/archive root (Windows junction / symlink escape).
324
+ """
325
+ cleanup = Path(cleanup_root).resolve()
326
+ protected: list[Path] = []
327
+ for raw in [*(state_roots or ()), *(archive_roots or ())]:
328
+ try:
329
+ protected.append(Path(raw).resolve())
330
+ except OSError as exc:
331
+ raise CleanupTopologyError(
332
+ f"CLEANUP_TOPOLOGY_REFUSED: cannot resolve protected path {raw}: {exc}"
333
+ ) from exc
334
+
335
+ for prot in protected:
336
+ if prot == cleanup or prot.is_relative_to(cleanup):
337
+ raise CleanupTopologyError(
338
+ "CLEANUP_TOPOLOGY_REFUSED: protected path "
339
+ f"{prot} is inside cleanup root {cleanup}"
340
+ )
341
+
342
+ if cleanup.is_dir():
343
+ for dirpath, dirnames, filenames in os.walk(cleanup, followlinks=False):
344
+ # Inspect this level (including heavy names) for link escapes, then
345
+ # prune descent so we do not walk install/cache trees.
346
+ entries = [*dirnames, *filenames]
347
+ dirnames[:] = [d for d in dirnames if d not in CLEANUP_WALK_SKIP_DIRS]
348
+ for name in entries:
349
+ child = Path(dirpath) / name
350
+ try:
351
+ is_link = child.is_symlink()
352
+ except OSError:
353
+ continue
354
+ # Junctions often report as directories; resolve and compare.
355
+ try:
356
+ resolved_child = child.resolve()
357
+ except OSError:
358
+ continue
359
+ escaped = not (
360
+ resolved_child == cleanup or resolved_child.is_relative_to(cleanup)
361
+ )
362
+ if not (is_link or escaped):
363
+ continue
364
+ for prot in protected:
365
+ if (
366
+ resolved_child == prot
367
+ or resolved_child.is_relative_to(prot)
368
+ or prot.is_relative_to(resolved_child)
369
+ ):
370
+ raise CleanupTopologyError(
371
+ "CLEANUP_TOPOLOGY_REFUSED: cleanup path "
372
+ f"{child} resolves to protected {resolved_child}"
373
+ )
374
+ return cleanup
375
+
376
+
377
+ _FORMAL_SNAPSHOT_DIRS = ("plans", "spec", "evidence", "reports", "meta")
378
+ _FORMAL_SNAPSHOT_FILES = ("events.ndjson",)
379
+
380
+
381
+ def snapshot_change_formal_layer(
382
+ project_root: Path,
383
+ change_id: str,
384
+ *,
385
+ change_dir: Path | None = None,
386
+ ) -> dict[str, Any]:
387
+ """Copy change formal layer into ``.harness/cache/change-snapshots/<change>/``.
388
+
389
+ Destination is always under the main project / common harness root — never
390
+ under a feature worktree path alone.
391
+ """
392
+ import shutil
393
+
394
+ project = resolve_main_project_root(Path(project_root))
395
+ src = Path(change_dir).resolve() if change_dir is not None else (
396
+ project / ".harness" / "changes" / change_id
397
+ ).resolve()
398
+ if not src.is_dir():
399
+ raise FileNotFoundError(f"change dir not found for snapshot: {src}")
400
+
401
+ dest = (project / ".harness" / "cache" / "change-snapshots" / change_id).resolve()
402
+ # Hard guard: never write the snapshot inside the source tree itself.
403
+ if dest == src or dest.is_relative_to(src):
404
+ raise ValueError(
405
+ f"snapshot destination must not be inside change dir: {dest}"
406
+ )
407
+ dest.mkdir(parents=True, exist_ok=True)
408
+
409
+ copied: list[str] = []
410
+ for name in _FORMAL_SNAPSHOT_DIRS:
411
+ source_dir = src / name
412
+ if not source_dir.is_dir():
413
+ continue
414
+ target_dir = dest / name
415
+ shutil.copytree(source_dir, target_dir, dirs_exist_ok=True)
416
+ copied.append(name + "/")
417
+ for name in _FORMAL_SNAPSHOT_FILES:
418
+ source_file = src / name
419
+ if not source_file.is_file():
420
+ continue
421
+ shutil.copy2(source_file, dest / name)
422
+ copied.append(name)
423
+
424
+ file_hashes: dict[str, str] = {}
425
+ for path in sorted(dest.rglob("*")):
426
+ if not path.is_file():
427
+ continue
428
+ rel = path.relative_to(dest).as_posix()
429
+ if rel == "manifest.json":
430
+ continue
431
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
432
+ file_hashes[rel] = "sha256:" + digest
433
+
434
+ manifest = {
435
+ "schemaVersion": 1,
436
+ "changeId": change_id,
437
+ "source": str(src),
438
+ "destination": str(dest),
439
+ "projectRoot": str(project),
440
+ "copied": copied,
441
+ "files": file_hashes,
442
+ "fileCount": len(file_hashes),
443
+ }
444
+ manifest_path = dest / "manifest.json"
445
+ manifest_path.write_text(
446
+ json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
447
+ encoding="utf-8",
448
+ newline="\n",
449
+ )
450
+ return {
451
+ "ok": True,
452
+ "code": "CHANGE_SNAPSHOT_WRITTEN",
453
+ "changeId": change_id,
454
+ "destination": str(dest),
455
+ "fileCount": len(file_hashes),
456
+ "copied": copied,
457
+ "manifestPath": str(manifest_path),
458
+ }
459
+
460
+
292
461
  def cmd_resolve_layout(args: argparse.Namespace) -> int:
293
462
  layout = resolve_change_layout(Path.cwd(), args.change)
294
463
  sys.stdout.write(json.dumps(layout, ensure_ascii=False, indent=2) + "\n")
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "codebuddy",
5
- "coreHash": "d5b51ad56d4d4535"
5
+ "coreHash": "5b9a3abb544f8fb2"
6
6
  }
@@ -3,7 +3,7 @@ name: harness-archive
3
3
  description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到
4
4
  .harness/archive/,含归档元数据和可视化最终总结。使用场景:归档、完成归档、收尾、打包产出物
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=d5b51ad56d4d4535; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=5b9a3abb544f8fb2; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-archive — 变更归档
8
8
 
9
9
  ## Purpose
@@ -33,6 +33,12 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
33
33
  - commit 已 push(`git log @{u}..HEAD` 输出为空)
34
34
  - **最终 hash 来源**:worktree 模式(`meta/worktree.json` requested=true)下读 ledger 的 `mergeFinalHash`(由 `/harness-submit` 合并段写入);主目录模式读 submit 的 `final pushed hash`。当前 `git rev-parse HEAD` 须一致
35
35
  - test 报告存在或标记"跳过";review 报告存在或标记"未运行 review"(review 不阻塞归档)
36
+ - **最小必备集(H-4 / `harness_archive.py status` blockers)**:
37
+ - `plans/*-plan.md`
38
+ - 非空 `events.ndjson`
39
+ - `evidence/verification-ledger.json`
40
+ - 至少一个 test 或 review 报告/证据
41
+ - 缺失任一项 → `archivable=false`;可从 `.harness/cache/change-snapshots/<change>/` 恢复正式层后再重试
36
42
 
37
43
  ## Inputs
38
44
 
@@ -20,6 +20,12 @@ description: harness-archive 的归档前检查项和归档后验证项。仅在
20
20
 
21
21
  - [ ] 未在调用 finalize 前手工追加 archive 阶段边界
22
22
  - [ ] 只有一个未归档变更目录(多个时终止或让用户选择)
23
+ - [ ] **最小必备集 blockers**(`harness_archive.py status`):
24
+ - [ ] `plans/*-plan.md` 存在
25
+ - [ ] `events.ndjson` 存在且非空
26
+ - [ ] `evidence/verification-ledger.json` 存在
27
+ - [ ] 至少一个 test 或 review 报告/证据
28
+ - [ ] 若缺失:优先从 `.harness/cache/change-snapshots/<change>/` 恢复,禁止空壳归档
23
29
  - [ ] 变更目录下有 plans/ 子目录(至少有计划文件)
24
30
  - [ ] `events.ndjson` 存在;执行日志允许由 finalize 从事件流重新渲染(旧 archive 才兼容根目录 `execution-log.md`)
25
31
  - [ ] 准备生成 `archive-manifest-before.json`(path/size/sha256)
@@ -3,7 +3,7 @@ name: harness-codebase-map
3
3
  description: 分析现有代码库并生成 .harness/codebase/map/
4
4
  下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=d5b51ad56d4d4535; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=5b9a3abb544f8fb2; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-codebase-map — 代码库地图
8
8
 
9
9
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-knowledge-ingest
3
3
  description: 从 .harness/archive 归档整理、同步和维护项目知识索引。适用场景:ingest knowledge、sync
4
4
  knowledge、rebuild knowledge index、promote knowledge、确认知识条目、检查知识库是否过期。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=d5b51ad56d4d4535; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=5b9a3abb544f8fb2; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-knowledge-ingest
8
8
 
9
9
  从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
@@ -3,7 +3,7 @@ name: harness-knowledge-query
3
3
  description: 根据新需求、设计任务、代码修改请求或问题排查,在规划和编码前查询 .harness/knowledge
4
4
  历史上下文。适用场景:query knowledge、查找历史需求、根据归档理解需求、继续之前类似开发、生成需求上下文包。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=d5b51ad56d4d4535; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=5b9a3abb544f8fb2; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-knowledge-query
8
8
 
9
9
  并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
@@ -2,7 +2,7 @@
2
2
  name: harness-plan
3
3
  description: 将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。使用场景:需求分析、feature plan、技术方案设计、实现方案规划
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=d5b51ad56d4d4535; overlay=none; agent=codebuddy; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=5b9a3abb544f8fb2; overlay=none; agent=codebuddy; do not edit -->
6
6
  # harness-plan — 需求规划
7
7
 
8
8
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-review
3
3
  description: 6维度代码审查(架构/安全/规范/兼容/测试/性能),对照项目规则(见
4
4
  .harness/context-index.json)和测试场景表,在隔离上下文运行。使用场景:代码审查、提交前检查、合并评审
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=d5b51ad56d4d4535; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=5b9a3abb544f8fb2; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-review — 代码审查
8
8
 
9
9
  ## Purpose
@@ -2,7 +2,7 @@
2
2
  name: harness-run
3
3
  description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。使用场景:开始编码、实现功能、写代码、TDD 编码
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=d5b51ad56d4d4535; overlay=none; agent=codebuddy; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=5b9a3abb544f8fb2; overlay=none; agent=codebuddy; do not edit -->
6
6
  # harness-run — 需求编码
7
7
 
8
8
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-submit
3
3
  description: 最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff
4
4
  合并回主分支。使用场景:提交代码、commit、push、合并分支、merge to main、完成开发
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=d5b51ad56d4d4535; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=5b9a3abb544f8fb2; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-submit — 最终提交(含 worktree 合并)
8
8
 
9
9
  ## Purpose
@@ -79,12 +79,17 @@ M3. **merge** — `--no-ff` 合并 feature 分支;merge diff 出现其他 Chan
79
79
  M4. **verify** — 在 integration worktree 内执行组合态验证;他人提交引入或 ledger 不可复用时必跑
80
80
  M5. **push** — 仅在验证身份与远端基线仍匹配时 push;远端漂移 → `TARGET_MOVED` 结构化失败,不继续
81
81
  M6. **cleanup** — `git worktree remove --force` 精确路径 + 临时分支 + (push 成功后)保护 ref;释放 integration lock;失败保留 journal 与诊断证据;更新 `worktree.json`(`created=false` + removedAt)
82
+ M6.5. **formal snapshot** — mergeFinalHash 写入后调用 `harness_paths.snapshot_change_formal_layer(projectRoot, changeId)`,备份到主仓 `.harness/cache/change-snapshots/<change>/`(不得写在 feature worktree 内)
82
83
  M7. **ledger + 收尾** — 经 `harness_ledger.py record` 写入 `mergeFinalHash`(= journal `pushedHead`);**`harness_gate.py close --phase merge`**(禁止手工 phase.end);提示 `/harness-archive`
83
84
 
84
- > Ledger v3(v2 契约 / split-v1 布局起):`record` 强制顶层身份(`schemaVersion=3/repositoryId/baseCommit/currentHead/diffHash/ownershipHash`,缺失非零退出、不写账本)与 typed metrics;legacy 契约行为不变。详见 `../protocols/ledger-protocol.md` 第十节。
85
+ > Ledger v3(v2 契约 / split-v1 布局起):`record` 强制顶层身份(`schemaVersion=3/repositoryId/baseCommit/currentHead/diffHash/ownershipHash`,缺失非零退出、`LEDGER_IDENTITY_INVALID`,不写账本)与 typed metrics;legacy 契约行为不变。详见 `../protocols/ledger-protocol.md` 第十节。
85
86
 
86
87
  正常路径**禁止创建、应用或删除仓库级 stash**。中断恢复:`harness_integration.py status` 读 journal,`recover` 从首个未完成步骤续跑;protection refs 只在 push 成功后的 cleanup 删除。
87
88
 
89
+ **失败 txn 回收(abandon)**:verify/push 未成功且 remote 不含 mergeCommit 时,运行 `harness_integration.py abandon --change … --run-id …`:清理 integrationRoot / temp branch / protection refs / lock;**绝不删除 feature worktree**。push 已成功或 remote 已含 merge → `ABANDON_REFUSED`,改走 cleanup。
90
+
91
+ **清 feature worktree 迁根硬门槛(H-1/H-2)**:删除 feature worktree 前,若 Agent root / cwd 落在待删路径内,必须先 `move_agent_to_root(projectRoot)`(目标为主仓已存在的 `main`/`master`;**禁止**为迁根去 fetch 已删除的 feature 分支)。迁根失败 → **拒绝删除**。清理前调用 `harness_paths.assert_cleanup_safe(cleanupRoot, stateRoots, archiveRoots)`;state/archive(含 junction/symlink 解析后)落在 cleanup 内 → `CLEANUP_TOPOLOGY_REFUSED`。
92
+
88
93
  ## P0 执行可信度规则
89
94
 
90
95
  - 命令结果不得靠猜测;普通 Bash 被拒 → 立即改用等价 PowerShell 重试一次
@@ -116,6 +121,10 @@ test-tracking manifest 是 ignored test 的唯一强制暂存授权:只允许
116
121
  - **`git merge --no-ff` 固定**;禁止 fast-forward
117
122
  - **冲突不自动解** → 停下 → 用户手动解 → 确认后继续
118
123
  - **`mergeFinalHash`** = 主分支 push 后 HEAD;archive 优先读此字段
124
+ - **迁根后再删 feature WT**:Agent 仍在待删 root 内则拒绝删除;目标分支已删时切主仓 `main`/`master`,禁止强行 fetch
125
+ - **snapshot 后再清**:正式层已写入 `.harness/cache/change-snapshots/<change>/`
126
+ - **拓扑安全**:`assert_cleanup_safe` 拒绝 state/archive ⊆ cleanup(含 junction)
127
+ - **abandon ≠ cleanup**:仅失败 txn;永不删 feature worktree
119
128
 
120
129
  ### 五、Shell 安全 / 敏感信息 / 证据化
121
130
 
@@ -369,6 +369,8 @@ powershell.exe -Command "python harness/scripts/harness_check_gate.py --write"
369
369
 
370
370
  ### 步骤 M5:push
371
371
 
372
+ **硬门槛(eslint × feature worktree)**:integration / 主仓 `pre-push` 跑 `npm run check` 时,eslint 不得把 sibling `.worktrees/<change>/` 当成第二工程根(双 `tsconfigRootDir`)。本仓以 `eslint.config.mjs` 的 `.worktrees/**` ignore 为准;若自定义 eslint 未 ignore,须先 `move_agent_to_root(<projectRoot>)` 再清 feature worktree,再 push。
373
+
372
374
  ```powershell
373
375
  python harness/scripts/harness_integration.py push --change <change-name> --run-id <run-id> --feature-branch harness/<change-name> --target-branch <主分支> --temp-root <task-temp>
374
376
  ```
@@ -386,11 +388,26 @@ python harness/scripts/harness_integration.py cleanup --change <change-name> --r
386
388
  - 失败保留 journal 与诊断证据,状态 🟡WARN,不得宣称清理完成
387
389
  - 若存在 test-tracking manifest,确认其路径已由合并后的目标分支跟踪(`git ls-files --error-unmatch -- <exact-path>`),缺失即停止
388
390
 
391
+ **失败 txn 改走 abandon(勿强行 cleanup)**:
392
+
393
+ ```powershell
394
+ python harness/scripts/harness_integration.py abandon --change <change-name> --run-id <run-id> --feature-branch harness/<change-name> --target-branch <主分支> --temp-root <task-temp>
395
+ ```
396
+
397
+ 仅当 push ≠ DONE 且 remote 不含 mergeCommit;清理 integration 产物后可换新 run-id 重开。push 成功 → `ABANDON_REFUSED`。
398
+
389
399
  更新 `meta/worktree.json`:`created=false` + `removedAt`/`removedBy`/`removalNote`。
390
400
 
401
+ **删除 feature worktree 前(硬门槛)**:
402
+
403
+ 1. 若 Agent root / cwd 落在待删 worktree → 先 `move_agent_to_root(<projectRoot>)`;目标为主仓,feature 分支已删则切 `main`/`master`,禁止 fetch 已不存在分支
404
+ 2. 迁根失败 → **停止删除**
405
+ 3. `assert_cleanup_safe(cleanupRoot, [.harness/changes/<id>, .harness/state/...], [.harness/archive])`;`CLEANUP_TOPOLOGY_REFUSED` → 停止
406
+ 4. mergeFinalHash 已写后先 `snapshot_change_formal_layer`,再清 feature WT
407
+
391
408
  ### 步骤 M7:更新 ledger + 收尾
392
409
 
393
- ledger 顶层写入 `"mergeFinalHash": "<journal pushedHead>"`;经 `harness_gate.py close --phase merge` 关闭(禁止手工 phase.end)。
410
+ ledger 顶层写入 `"mergeFinalHash": "<journal pushedHead>"`;经 `harness_gate.py close --phase merge` 关闭(禁止手工 phase.end)。正式层快照确认存在于 `.harness/cache/change-snapshots/<change-name>/`。
394
411
 
395
412
  ```markdown
396
413
  ## 合并完成 — <change-name>
@@ -399,6 +416,7 @@ ledger 顶层写入 `"mergeFinalHash": "<journal pushedHead>"`;经 `harness_ga
399
416
  - 主分支 push: ✅(<old>..<new> → origin/<主分支>)
400
417
  - mergeFinalHash: `<merge-final-hash>`
401
418
  - 验证策略: 🔁REUSED / 🔄已重跑
402
- - worktree: 已清理
419
+ - snapshot: `.harness/cache/change-snapshots/<change-name>/`
420
+ - worktree: 已清理(迁根后)
403
421
  - 下一步: `/harness-archive`
404
422
  ```
@@ -2,7 +2,7 @@
2
2
  name: harness-sync
3
3
  description: 检查并更新项目AI元数据(CodeGraph索引、harness-codebase-map分析、CLAUDE.md、AGENTS.md、.harness/完整性),确保Claude对代码库的理解与最新代码一致。当用户说'同步/更新索引/刷新元数据/检查一致性'时使用
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=d5b51ad56d4d4535; overlay=none; agent=codebuddy; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=5b9a3abb544f8fb2; overlay=none; agent=codebuddy; do not edit -->
6
6
  # harness-sync — 元数据同步
7
7
 
8
8
  ## Purpose