@hunter-harness/workflow-harness 0.2.37 → 0.2.39

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 (194) 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 +10 -7
  3. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +24 -4
  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-ingest/scripts/harness_knowledge.py +283 -28
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -2
  10. package/harness/bundles/general/claude-code/harness-review/reference.md +20 -5
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/scripts/harness_archive.py +446 -20
  16. package/harness/bundles/general/claude-code/scripts/harness_events.py +273 -21
  17. package/harness/bundles/general/claude-code/scripts/harness_gate.py +56 -32
  18. package/harness/bundles/general/claude-code/scripts/harness_integration.py +197 -3
  19. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +126 -2
  20. package/harness/bundles/general/claude-code/scripts/harness_report_model.py +28 -6
  21. package/harness/bundles/general/claude-code/scripts/harness_review.py +118 -17
  22. package/harness/bundles/general/claude-code/scripts/harness_service.py +219 -0
  23. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  24. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +10 -7
  25. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
  26. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  29. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
  32. package/harness/bundles/general/codebuddy/harness-review/reference.md +20 -5
  33. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +446 -20
  38. package/harness/bundles/general/codebuddy/scripts/harness_events.py +273 -21
  39. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +56 -32
  40. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +197 -3
  41. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +126 -2
  42. package/harness/bundles/general/codebuddy/scripts/harness_report_model.py +28 -6
  43. package/harness/bundles/general/codebuddy/scripts/harness_review.py +118 -17
  44. package/harness/bundles/general/codebuddy/scripts/harness_service.py +219 -0
  45. package/harness/bundles/general/codex/.harness-build.json +1 -1
  46. package/harness/bundles/general/codex/harness-archive/SKILL.md +10 -7
  47. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +24 -4
  48. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  51. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  52. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  53. package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
  54. package/harness/bundles/general/codex/harness-review/reference.md +20 -5
  55. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  56. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  58. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  59. package/harness/bundles/general/codex/scripts/harness_archive.py +446 -20
  60. package/harness/bundles/general/codex/scripts/harness_events.py +273 -21
  61. package/harness/bundles/general/codex/scripts/harness_gate.py +56 -32
  62. package/harness/bundles/general/codex/scripts/harness_integration.py +197 -3
  63. package/harness/bundles/general/codex/scripts/harness_ledger.py +126 -2
  64. package/harness/bundles/general/codex/scripts/harness_report_model.py +28 -6
  65. package/harness/bundles/general/codex/scripts/harness_review.py +118 -17
  66. package/harness/bundles/general/codex/scripts/harness_service.py +219 -0
  67. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  68. package/harness/bundles/general/cursor/harness-archive/SKILL.md +10 -7
  69. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +24 -4
  70. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  73. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  74. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  75. package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
  76. package/harness/bundles/general/cursor/harness-review/reference.md +20 -5
  77. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  78. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  79. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  80. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  81. package/harness/bundles/general/cursor/scripts/harness_archive.py +446 -20
  82. package/harness/bundles/general/cursor/scripts/harness_events.py +273 -21
  83. package/harness/bundles/general/cursor/scripts/harness_gate.py +56 -32
  84. package/harness/bundles/general/cursor/scripts/harness_integration.py +197 -3
  85. package/harness/bundles/general/cursor/scripts/harness_ledger.py +126 -2
  86. package/harness/bundles/general/cursor/scripts/harness_report_model.py +28 -6
  87. package/harness/bundles/general/cursor/scripts/harness_review.py +118 -17
  88. package/harness/bundles/general/cursor/scripts/harness_service.py +219 -0
  89. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  90. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  91. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +10 -7
  92. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +24 -4
  93. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  94. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  95. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  96. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  97. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  98. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  99. package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
  100. package/harness/bundles/java/claude-code/harness-review/reference.md +20 -5
  101. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  102. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  103. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  104. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  105. package/harness/bundles/java/claude-code/scripts/harness_archive.py +446 -20
  106. package/harness/bundles/java/claude-code/scripts/harness_events.py +273 -21
  107. package/harness/bundles/java/claude-code/scripts/harness_gate.py +56 -32
  108. package/harness/bundles/java/claude-code/scripts/harness_integration.py +197 -3
  109. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +126 -2
  110. package/harness/bundles/java/claude-code/scripts/harness_report_model.py +28 -6
  111. package/harness/bundles/java/claude-code/scripts/harness_review.py +118 -17
  112. package/harness/bundles/java/claude-code/scripts/harness_service.py +219 -0
  113. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  114. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  115. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +10 -7
  116. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
  117. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  118. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  119. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  120. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  121. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  122. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  123. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
  124. package/harness/bundles/java/codebuddy/harness-review/reference.md +20 -5
  125. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  126. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  127. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  128. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  129. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +446 -20
  130. package/harness/bundles/java/codebuddy/scripts/harness_events.py +273 -21
  131. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +56 -32
  132. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +197 -3
  133. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +126 -2
  134. package/harness/bundles/java/codebuddy/scripts/harness_report_model.py +28 -6
  135. package/harness/bundles/java/codebuddy/scripts/harness_review.py +118 -17
  136. package/harness/bundles/java/codebuddy/scripts/harness_service.py +219 -0
  137. package/harness/bundles/java/codex/.harness-build.json +1 -1
  138. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  139. package/harness/bundles/java/codex/harness-archive/SKILL.md +10 -7
  140. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +24 -4
  141. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  142. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  143. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  144. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  145. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  146. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  147. package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
  148. package/harness/bundles/java/codex/harness-review/reference.md +20 -5
  149. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  150. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  151. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  152. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  153. package/harness/bundles/java/codex/scripts/harness_archive.py +446 -20
  154. package/harness/bundles/java/codex/scripts/harness_events.py +273 -21
  155. package/harness/bundles/java/codex/scripts/harness_gate.py +56 -32
  156. package/harness/bundles/java/codex/scripts/harness_integration.py +197 -3
  157. package/harness/bundles/java/codex/scripts/harness_ledger.py +126 -2
  158. package/harness/bundles/java/codex/scripts/harness_report_model.py +28 -6
  159. package/harness/bundles/java/codex/scripts/harness_review.py +118 -17
  160. package/harness/bundles/java/codex/scripts/harness_service.py +219 -0
  161. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  162. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  163. package/harness/bundles/java/cursor/harness-archive/SKILL.md +10 -7
  164. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +24 -4
  165. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  166. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  167. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  168. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  169. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  170. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  171. package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
  172. package/harness/bundles/java/cursor/harness-review/reference.md +20 -5
  173. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  174. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  175. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  176. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  177. package/harness/bundles/java/cursor/scripts/harness_archive.py +446 -20
  178. package/harness/bundles/java/cursor/scripts/harness_events.py +273 -21
  179. package/harness/bundles/java/cursor/scripts/harness_gate.py +56 -32
  180. package/harness/bundles/java/cursor/scripts/harness_integration.py +197 -3
  181. package/harness/bundles/java/cursor/scripts/harness_ledger.py +126 -2
  182. package/harness/bundles/java/cursor/scripts/harness_report_model.py +28 -6
  183. package/harness/bundles/java/cursor/scripts/harness_review.py +118 -17
  184. package/harness/bundles/java/cursor/scripts/harness_service.py +219 -0
  185. package/harness/manifests/general/claude-code.json +24 -24
  186. package/harness/manifests/general/codebuddy.json +24 -24
  187. package/harness/manifests/general/codex.json +24 -24
  188. package/harness/manifests/general/cursor.json +24 -24
  189. package/harness/manifests/java/claude-code.json +26 -26
  190. package/harness/manifests/java/codebuddy.json +26 -26
  191. package/harness/manifests/java/codex.json +26 -26
  192. package/harness/manifests/java/cursor.json +26 -26
  193. package/hunter-workflow-family.json +4 -4
  194. package/package.json +1 -1
@@ -19,6 +19,7 @@ import hashlib
19
19
  import json
20
20
  import os
21
21
  import re
22
+ import subprocess
22
23
  import sys
23
24
  import uuid
24
25
  from pathlib import Path
@@ -351,43 +352,121 @@ def degradation_matrix(
351
352
  return {"fallback": "advisory", "status": "ADVISORY"}
352
353
 
353
354
 
355
+ def _canonical_root(value: str | Path) -> str:
356
+ """Canonicalize a root for an identity comparison across Windows worktrees."""
357
+ return os.path.normcase(str(Path(value).expanduser().resolve()))
358
+
359
+
360
+ def codegraph_worktree_id(execution_root: Path) -> str | None:
361
+ """Return a stable id for a linked worktree, or ``None`` for the main checkout."""
362
+ root = Path(execution_root).resolve()
363
+ try:
364
+ git_dir = subprocess.run(
365
+ ["git", "rev-parse", "--git-dir"],
366
+ cwd=str(root),
367
+ capture_output=True,
368
+ text=True,
369
+ encoding="utf-8",
370
+ errors="replace",
371
+ check=False,
372
+ )
373
+ common_dir = subprocess.run(
374
+ ["git", "rev-parse", "--git-common-dir"],
375
+ cwd=str(root),
376
+ capture_output=True,
377
+ text=True,
378
+ encoding="utf-8",
379
+ errors="replace",
380
+ check=False,
381
+ )
382
+ except OSError:
383
+ return None
384
+ if git_dir.returncode != 0 or common_dir.returncode != 0:
385
+ return None
386
+ git_dir_path = Path(git_dir.stdout.strip())
387
+ common_dir_path = Path(common_dir.stdout.strip())
388
+ if not git_dir_path.is_absolute():
389
+ git_dir_path = (root / git_dir_path).resolve()
390
+ if not common_dir_path.is_absolute():
391
+ common_dir_path = (root / common_dir_path).resolve()
392
+ if _canonical_root(git_dir_path) == _canonical_root(common_dir_path):
393
+ return None
394
+ return "sha256:" + hashlib.sha256(
395
+ _canonical_root(root).encode("utf-8")
396
+ ).hexdigest()
397
+
398
+
399
+ def codegraph_expected_identity(
400
+ execution_root: Path,
401
+ *,
402
+ expected_repository_id: str,
403
+ expected_head: str,
404
+ ) -> dict[str, str | None]:
405
+ """Build the identity contract supplied with every CodeGraph query."""
406
+ root = Path(execution_root).resolve()
407
+ return {
408
+ "rootPath": str(root),
409
+ "worktreeId": codegraph_worktree_id(root),
410
+ "repositoryId": expected_repository_id,
411
+ "head": expected_head,
412
+ "indexSnapshotAt": None,
413
+ }
414
+
415
+
354
416
  def validate_codegraph_identity(
355
417
  *,
356
418
  response: dict[str, Any],
357
419
  expected_repository_id: str,
358
420
  expected_head: str,
421
+ expected_root: str | Path,
422
+ expected_worktree_id: str | None,
359
423
  ) -> dict[str, Any]:
360
- """C12: validate CodeGraph identity repositoryId/indexedHead must match.
424
+ """Validate CodeGraph evidence before Review adopts its source snippets.
361
425
 
362
- On mismatch, signal CODEGRAPH_IDENTITY_MISMATCH with fallback to Grep/Glob + Read.
426
+ A repository id is shared by the main checkout and linked worktrees, so it
427
+ cannot by itself prevent main-checkout evidence from being used for feature
428
+ worktree review. Root and worktree identity are therefore mandatory.
363
429
  """
364
430
  repo_id = response.get("repositoryId")
365
- indexed_head = response.get("indexedHead")
366
- indexed_at = response.get("indexedAt")
431
+ indexed_head = response.get("head", response.get("indexedHead"))
432
+ indexed_at = response.get("indexSnapshotAt", response.get("indexedAt"))
433
+ root_path = response.get("rootPath", response.get("indexedRoot"))
434
+ worktree_id = response.get("worktreeId")
435
+ expected_root_path = _canonical_root(expected_root)
436
+ actual_root_path = (
437
+ _canonical_root(root_path) if isinstance(root_path, str) and root_path.strip() else None
438
+ )
439
+ actual = {
440
+ "rootPath": root_path,
441
+ "worktreeId": worktree_id,
442
+ "repositoryId": repo_id,
443
+ "head": indexed_head,
444
+ "indexSnapshotAt": indexed_at,
445
+ }
446
+ expected = {
447
+ "rootPath": str(Path(expected_root).resolve()),
448
+ "worktreeId": expected_worktree_id,
449
+ "repositoryId": expected_repository_id,
450
+ "head": expected_head,
451
+ "indexSnapshotAt": None,
452
+ }
367
453
  if (
368
454
  repo_id == expected_repository_id
369
455
  and indexed_head == expected_head
456
+ and actual_root_path == expected_root_path
457
+ and worktree_id == expected_worktree_id
370
458
  and indexed_at
371
459
  ):
372
460
  return {
373
461
  "ok": True,
374
462
  "code": "IDENTITY_OK",
375
- "repositoryId": repo_id,
376
- "indexedHead": indexed_head,
377
- "indexedAt": indexed_at,
463
+ "evidence": actual,
378
464
  }
379
465
  return {
380
466
  "ok": False,
381
- "code": "CODEGRAPH_IDENTITY_MISMATCH",
382
- "expected": {
383
- "repositoryId": expected_repository_id,
384
- "indexedHead": expected_head,
385
- },
386
- "actual": {
387
- "repositoryId": repo_id,
388
- "indexedHead": indexed_head,
389
- "indexedAt": indexed_at,
390
- },
467
+ "code": "IDENTITY_MISMATCH",
468
+ "expected": expected,
469
+ "actual": actual,
391
470
  "fallback": "grep-glob-read",
392
471
  }
393
472
 
@@ -399,6 +478,20 @@ def cmd_validate_findings(args: argparse.Namespace) -> int:
399
478
  return _emit(payload, as_json=True)
400
479
 
401
480
 
481
+ def cmd_validate_codegraph_identity(args: argparse.Namespace) -> int:
482
+ response = _read_json(Path(args.input))
483
+ if not isinstance(response, dict):
484
+ raise ValueError("CodeGraph response must be an object")
485
+ payload = validate_codegraph_identity(
486
+ response=response,
487
+ expected_repository_id=args.repository_id,
488
+ expected_head=args.head,
489
+ expected_root=args.execution_root,
490
+ expected_worktree_id=args.worktree_id,
491
+ )
492
+ return _emit(payload, as_json=True)
493
+
494
+
402
495
  def cmd_write_findings(args: argparse.Namespace) -> int:
403
496
  doc = _read_json(Path(args.input))
404
497
  return _emit(write_findings(Path(args.change_dir), doc), as_json=True)
@@ -421,6 +514,14 @@ def build_parser() -> argparse.ArgumentParser:
421
514
  p_validate.add_argument("--input", required=True)
422
515
  p_validate.set_defaults(func=cmd_validate_findings)
423
516
 
517
+ p_codegraph = sub.add_parser("validate-codegraph-identity")
518
+ p_codegraph.add_argument("--input", required=True)
519
+ p_codegraph.add_argument("--execution-root", required=True)
520
+ p_codegraph.add_argument("--repository-id", required=True)
521
+ p_codegraph.add_argument("--head", required=True)
522
+ p_codegraph.add_argument("--worktree-id")
523
+ p_codegraph.set_defaults(func=cmd_validate_codegraph_identity)
524
+
424
525
  p_findings = sub.add_parser("write-findings")
425
526
  p_findings.add_argument("--change-dir", required=True)
426
527
  p_findings.add_argument("--input", required=True)
@@ -726,6 +726,173 @@ def write_session(change_dir: Path, session: dict[str, Any]) -> Path:
726
726
  return path
727
727
 
728
728
 
729
+ def _same_path(left: Any, right: Path | None) -> bool:
730
+ if right is None or not isinstance(left, str) or not left.strip():
731
+ return False
732
+ try:
733
+ return Path(left).resolve() == right.resolve()
734
+ except OSError:
735
+ return False
736
+
737
+
738
+ def session_is_owned(
739
+ session: dict[str, Any],
740
+ *,
741
+ change_id: str | None = None,
742
+ execution_root: Path | None = None,
743
+ worktree_root: Path | None = None,
744
+ ) -> bool:
745
+ """Return true for an AI session owned by the supplied change/root filters.
746
+
747
+ ``change_id`` is required when provided. Root filters match if the session's
748
+ ``executionRoot`` or ``worktreeRoot`` equals any supplied root (OR).
749
+ """
750
+ if session.get("startedBy") != "AI":
751
+ return False
752
+ if change_id is not None and session.get("changeId") != change_id:
753
+ return False
754
+ root_matches: list[bool] = []
755
+ if execution_root is not None:
756
+ root_matches.append(_same_path(session.get("executionRoot"), execution_root))
757
+ root_matches.append(_same_path(session.get("worktreeRoot"), execution_root))
758
+ if worktree_root is not None:
759
+ root_matches.append(_same_path(session.get("worktreeRoot"), worktree_root))
760
+ root_matches.append(_same_path(session.get("executionRoot"), worktree_root))
761
+ if root_matches and not any(root_matches):
762
+ return False
763
+ return True
764
+
765
+
766
+ def find_owned_sessions(
767
+ project_root: Path,
768
+ *,
769
+ change_id: str | None = None,
770
+ execution_root: Path | None = None,
771
+ worktree_root: Path | None = None,
772
+ ) -> dict[str, list[dict[str, Any]]]:
773
+ """Discover persisted sessions and classify them without stopping anything."""
774
+ project = resolve_path(project_root)
775
+ candidate_dirs: list[Path] = []
776
+ if change_id:
777
+ candidate_dirs.extend(
778
+ [
779
+ project / ".harness" / "changes" / change_id,
780
+ project / ".harness" / "state" / "changes" / change_id,
781
+ ]
782
+ )
783
+ else:
784
+ for root in (
785
+ project / ".harness" / "changes",
786
+ project / ".harness" / "state" / "changes",
787
+ ):
788
+ if root.is_dir():
789
+ candidate_dirs.extend(path for path in root.iterdir() if path.is_dir())
790
+
791
+ owned: list[dict[str, Any]] = []
792
+ reported: list[dict[str, Any]] = []
793
+ seen: set[Path] = set()
794
+ for change_dir in candidate_dirs:
795
+ path = session_path(change_dir)
796
+ try:
797
+ path = path.resolve()
798
+ except OSError:
799
+ continue
800
+ if path in seen or not path.is_file():
801
+ continue
802
+ seen.add(path)
803
+ try:
804
+ session = load_session(change_dir)
805
+ except SessionCorrupt as exc:
806
+ reported.append(
807
+ {
808
+ "sessionPath": str(path),
809
+ "reason": "session-corrupt",
810
+ "detail": str(exc),
811
+ }
812
+ )
813
+ continue
814
+ if session is None:
815
+ continue
816
+ item = {"changeDir": str(change_dir.resolve()), "session": session}
817
+ if session_is_owned(
818
+ session,
819
+ change_id=change_id,
820
+ execution_root=execution_root,
821
+ worktree_root=worktree_root,
822
+ ):
823
+ owned.append(item)
824
+ else:
825
+ reported.append(
826
+ {
827
+ "sessionPath": str(path),
828
+ "reason": "not-owned",
829
+ "startedBy": session.get("startedBy"),
830
+ "pid": session.get("pid"),
831
+ "leasedPort": session.get("leasedPort"),
832
+ }
833
+ )
834
+ return {"owned": owned, "reported": reported}
835
+
836
+
837
+ def stop_owned_sessions(
838
+ project_root: Path,
839
+ *,
840
+ change_id: str | None = None,
841
+ execution_root: Path | None = None,
842
+ worktree_root: Path | None = None,
843
+ ) -> dict[str, Any]:
844
+ """Stop only discovered AI sessions whose persisted ownership still matches."""
845
+ discovered = find_owned_sessions(
846
+ project_root,
847
+ change_id=change_id,
848
+ execution_root=execution_root,
849
+ worktree_root=worktree_root,
850
+ )
851
+ stopped: list[dict[str, Any]] = []
852
+ blocked: list[dict[str, Any]] = []
853
+ ports_still_in_use: list[int] = []
854
+ for item in discovered["owned"]:
855
+ change_dir = Path(item["changeDir"])
856
+ session = item["session"]
857
+ if not session_is_owned(
858
+ session,
859
+ change_id=change_id,
860
+ execution_root=execution_root,
861
+ worktree_root=worktree_root,
862
+ ):
863
+ blocked.append(
864
+ {"sessionPath": str(session_path(change_dir)), "reason": "ownership-changed"}
865
+ )
866
+ continue
867
+ result = stop_ai_session(change_dir, session, require_identity=True)
868
+ result["sessionPath"] = str(session_path(change_dir))
869
+ if result.get("action") == "needs-user-decision":
870
+ blocked.append(result)
871
+ continue
872
+ pid = session.get("pid")
873
+ if isinstance(pid, int) and is_pid_alive(pid):
874
+ blocked.append(
875
+ {
876
+ "sessionPath": str(session_path(change_dir)),
877
+ "reason": "owned-process-still-alive",
878
+ "pid": pid,
879
+ }
880
+ )
881
+ continue
882
+ stopped.append(result)
883
+ port = session.get("leasedPort")
884
+ if isinstance(port, int) and is_port_in_use(port):
885
+ ports_still_in_use.append(port)
886
+ return {
887
+ "ok": not blocked,
888
+ "ownedSessions": len(discovered["owned"]),
889
+ "stopped": stopped,
890
+ "blocked": blocked,
891
+ "reported": discovered["reported"],
892
+ "portsStillInUse": ports_still_in_use,
893
+ }
894
+
895
+
729
896
  # ---------------------------------------------------------------------------
730
897
  # Start / stop helpers
731
898
  # ---------------------------------------------------------------------------
@@ -924,6 +1091,10 @@ def build_session(
924
1091
  command: str,
925
1092
  service_start: dict[str, Any],
926
1093
  started_at: str | None = None,
1094
+ worktree_root: Path | None = None,
1095
+ execution_root: Path | None = None,
1096
+ change_id: str | None = None,
1097
+ attempt_id: str | None = None,
927
1098
  ) -> dict[str, Any]:
928
1099
  profile_name = service_start.get("profile") or "local-dev"
929
1100
  overlay = service_start.get("overlayPath") or ""
@@ -941,6 +1112,14 @@ def build_session(
941
1112
  if isinstance(service_start.get("leasedPort"), int):
942
1113
  session["leasedPort"] = service_start["leasedPort"]
943
1114
  session["leaseOwner"] = service_start.get("leaseOwner")
1115
+ if worktree_root is not None:
1116
+ session["worktreeRoot"] = str(worktree_root.resolve())
1117
+ if execution_root is not None:
1118
+ session["executionRoot"] = str(execution_root.resolve())
1119
+ if change_id:
1120
+ session["changeId"] = change_id
1121
+ if attempt_id:
1122
+ session["attemptId"] = attempt_id
944
1123
  return session
945
1124
 
946
1125
 
@@ -1031,6 +1210,15 @@ def cmd_ensure(args: argparse.Namespace) -> int:
1031
1210
  # 持久 profile 只保存模板;runtime overlay/profile 注入到 session,不写回持久 profile。
1032
1211
  # 含 worktree/change 陈旧持久值时拒绝(修复输入端陈旧 profile)。
1033
1212
  change_name = getattr(args, "change_name", None) or change_dir.name
1213
+ worktree_root_raw = getattr(args, "worktree_root", None)
1214
+ execution_root_raw = getattr(args, "execution_root", None)
1215
+ attempt_id = str(getattr(args, "attempt_id", None) or "").strip() or None
1216
+ worktree_root = (
1217
+ resolve_path(worktree_root_raw) if worktree_root_raw else project
1218
+ )
1219
+ execution_root = (
1220
+ resolve_path(execution_root_raw) if execution_root_raw else None
1221
+ )
1034
1222
  overlay = getattr(args, "overlay", None)
1035
1223
  leased_port = getattr(args, "leased_port", None)
1036
1224
  lease_owner = str(getattr(args, "lease_owner", None) or "").strip() or None
@@ -1135,6 +1323,10 @@ def cmd_ensure(args: argparse.Namespace) -> int:
1135
1323
  as_json=as_json,
1136
1324
  action="restarted",
1137
1325
  previousPid=pid,
1326
+ worktree_root=worktree_root,
1327
+ execution_root=execution_root,
1328
+ change_id=change_name,
1329
+ attempt_id=attempt_id,
1138
1330
  )
1139
1331
 
1140
1332
  # pid dead → stale session; clear and fall through
@@ -1161,6 +1353,10 @@ def cmd_ensure(args: argparse.Namespace) -> int:
1161
1353
  files=files,
1162
1354
  as_json=as_json,
1163
1355
  action="started",
1356
+ worktree_root=worktree_root,
1357
+ execution_root=execution_root,
1358
+ change_id=change_name,
1359
+ attempt_id=attempt_id,
1164
1360
  )
1165
1361
 
1166
1362
 
@@ -1173,6 +1369,10 @@ def _start_and_record(
1173
1369
  files: list[str],
1174
1370
  as_json: bool,
1175
1371
  action: str,
1372
+ worktree_root: Path | None = None,
1373
+ execution_root: Path | None = None,
1374
+ change_id: str | None = None,
1375
+ attempt_id: str | None = None,
1176
1376
  **extra: Any,
1177
1377
  ) -> int:
1178
1378
  if not files:
@@ -1224,6 +1424,10 @@ def _start_and_record(
1224
1424
  command=command,
1225
1425
  service_start=service_start,
1226
1426
  started_at=started_at,
1427
+ worktree_root=worktree_root,
1428
+ execution_root=execution_root,
1429
+ change_id=change_id,
1430
+ attempt_id=attempt_id,
1227
1431
  )
1228
1432
  write_session(change_dir, session)
1229
1433
 
@@ -1423,6 +1627,21 @@ def build_parser() -> argparse.ArgumentParser:
1423
1627
  default=None,
1424
1628
  help="run id that owns --leased-port",
1425
1629
  )
1630
+ p_ensure.add_argument(
1631
+ "--worktree-root",
1632
+ default=None,
1633
+ help="worktree that owns the service (default: --project)",
1634
+ )
1635
+ p_ensure.add_argument(
1636
+ "--execution-root",
1637
+ default=None,
1638
+ help="ephemeral execution worktree that owns the service",
1639
+ )
1640
+ p_ensure.add_argument(
1641
+ "--attempt-id",
1642
+ default=None,
1643
+ help="phase or transaction attempt that started the service",
1644
+ )
1426
1645
  p_ensure.add_argument("--json", action="store_true")
1427
1646
  p_ensure.set_defaults(func=cmd_ensure)
1428
1647
 
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "java",
4
4
  "agent": "codebuddy",
5
- "coreHash": "e47155d4f364fc9e"
5
+ "coreHash": "5756e5211caf00c1"
6
6
  }
@@ -2,7 +2,7 @@
2
2
  name: harness-apidoc
3
3
  description: 从 Java 后端代码生成结构化前端 API 文档(Controller/VO/ErrorCode)。使用场景:接口文档、前端接口文档、API 文档生成
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=e47155d4f364fc9e; overlay=java; agent=codebuddy; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=5756e5211caf00c1; overlay=java; agent=codebuddy; do not edit -->
6
6
  # harness-apidoc — 前端接口文档(Java overlay)
7
7
 
8
8
  ## Purpose
@@ -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=e47155d4f364fc9e; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=5756e5211caf00c1; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-archive — 变更归档
8
8
 
9
9
  ## Purpose
@@ -26,7 +26,7 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
26
26
  自动调用边界:
27
27
 
28
28
  - 归档涉及移动/删除原变更目录,支持被其他 skill 调用,但调用前必须确保前置条件已满足;也支持用户显式 `/harness-archive` 调用。
29
- - 归档前确认是强制阻断检查点,用户拒绝 终止流程,不执行任何操作。
29
+ - 默认归档前确认是阻断检查点。若 `harness_archive.py auto-gate` 返回 `ARCHIVE_AUTO_GATE_SATISFIED`(Submit 或 Merge 已终态,archive-boundary snapshot 存在且全部归档门禁满足),后续 Archive 可无 AskQuestion 自动运行;其他情况仍须确认。
30
30
 
31
31
  ## 前置条件
32
32
 
@@ -89,9 +89,12 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
89
89
  - **Read `checklist.md`** — 归档前检查项
90
90
  - 发现多个未归档变更 → 让用户选择或终止
91
91
 
92
- ### Phase 2:确认归档 ⚠️ 强制阻断
92
+ ### Phase 2:确认归档 / 自动门禁
93
93
 
94
- blocking user confirmation 让用户确认归档操作。**用户拒绝 终止流程,不执行任何操作。**
94
+ 先运行 `harness_archive.py auto-gate --change-dir ".harness/changes/<change-name>" --intent <intent> --json`。
95
+
96
+ - `autoArchiveAllowed=true`:记录 gate receipt,直接进入 Phase 3;无需 AskQuestion。
97
+ - 否则:blocking user confirmation 让用户确认归档操作。**用户拒绝 → 终止流程,不执行任何操作。**
95
98
 
96
99
  - **Read `reference.md`** — 确认对话框的内容格式
97
100
 
@@ -126,9 +129,9 @@ blocking user confirmation 让用户确认归档操作。**用户拒绝 → 终
126
129
 
127
130
  扫描排除 `.harness/archive/`;多个未归档变更 → 让用户选择或终止,不批量归档。
128
131
 
129
- ### 二、归档前确认是强制阻断检查点
132
+ ### 二、归档前确认或自动门禁
130
133
 
131
- blocking user confirmation 确认;用户拒绝 → 终止,不执行任何操作。
134
+ `auto-gate` 只有在 Submit/Merge 终态、`meta/state-snapshot.json` 含 archive-boundary base、且 `status.archivable=true` 时才允许无确认执行。未满足时 blocking user confirmation;用户拒绝 → 终止,不执行任何操作。
132
135
 
133
136
  ### 三、文件移动只用内置工具或 PowerShell
134
137
 
@@ -201,7 +204,7 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 fi
201
204
 
202
205
  ## 交互白名单
203
206
 
204
- **仅允许**归档确认(Phase 2 blocking user confirmation);拒绝 → 终止,不执行任何移动。
207
+ 仅当 `auto-gate` 未满足时,允许归档确认(Phase 2 blocking user confirmation);拒绝 → 终止,不执行任何移动。
205
208
 
206
209
  ## 执行日志
207
210
 
@@ -69,7 +69,7 @@ const risks = list(current.findings).length
69
69
  const statusTone = (raw) => {
70
70
  const status = String(raw || "UNKNOWN").toUpperCase();
71
71
  if (/FAIL|ERROR|BLOCKED/.test(status)) return "danger";
72
- if (/WARN|CONDITIONAL|PARTIAL|NOT_RUN|UNKNOWN|SKIP/.test(status)) return "warning";
72
+ if (/WARN|CONDITIONAL|PARTIAL|NOT_RUN|UNKNOWN|SKIP|EVIDENCE_MISSING/.test(status)) return "warning";
73
73
  if (/ADVISORY|REUSED|NOT_APPLICABLE/.test(status)) return "neutral";
74
74
  return "success";
75
75
  };
@@ -77,7 +77,7 @@ const STATUS_LABELS = {
77
77
  OK: "通过", PASS: "通过", PASSED: "通过", CONDITIONAL_OK: "有条件通过",
78
78
  WARN: "警告", ADVISORY: "建议", FAIL: "失败", FAILED: "失败", ERROR: "错误",
79
79
  BLOCKED: "阻塞", NOT_RUN: "未运行", SKIPPED: "已跳过",
80
- NOT_APPLICABLE: "不适用", UNKNOWN: "未知"
80
+ NOT_APPLICABLE: "不适用", UNKNOWN: "未知", EVIDENCE_MISSING: "证据缺失"
81
81
  };
82
82
  const pill = (raw) => {
83
83
  const status = String(raw || "UNKNOWN").toUpperCase();
@@ -115,7 +115,17 @@ const verificationStatus = (value) => {
115
115
  };
116
116
  const verificationNote = (key, raw) => {
117
117
  const value = record(raw);
118
- if (key === "unitTests") return `${value.passRate || `${value.run ?? 0} 项`} · 失败 ${value.failures ?? 0}`;
118
+ if (key === "unitTests") {
119
+ const unique = numeric(value.uniqueTestCount);
120
+ const uniqueLabel = unique === null ? "唯一用例未记录" : `唯一 ${unique} 项`;
121
+ return `${value.passRate || `${value.run ?? 0} 次执行`} · ${uniqueLabel} · 重跑 ${value.rerunCount ?? 0}`;
122
+ }
123
+ if (key === "dbCompatibility") {
124
+ const evidence = record(verification.dbCompatibilityEvidence);
125
+ return evidence.status === "EVIDENCE_MISSING"
126
+ ? "缺少可验证的类型化账本/收据"
127
+ : evidence.reason || evidence.evidenceHash || "类型化兼容性证据";
128
+ }
119
129
  if (key === "apiTests") return `${value.passRate || `${value.passed ?? 0}/${value.total ?? 0}`} · 阻塞 ${value.blocked ?? 0}`;
120
130
  if (key === "browserE2E") return `${value.passed ?? 0}/${value.total ?? 0} 通过 · ${value.failed ?? 0} 失败 · 重试 ${value.retries ?? 0}`;
121
131
  if (value.checks !== undefined) return `${value.checks} 项检查`;
@@ -135,6 +145,8 @@ const groups = [
135
145
  const statuses = values.map(verificationStatus);
136
146
  const groupStatus = statuses.some((status) => /FAIL|ERROR|BLOCKED/.test(String(status)))
137
147
  ? "FAIL"
148
+ : statuses.some((status) => /EVIDENCE_MISSING/.test(String(status)))
149
+ ? "EVIDENCE_MISSING"
138
150
  : statuses.some((status) => /WARN|CONDITIONAL|PARTIAL/.test(String(status)))
139
151
  ? "WARN"
140
152
  : statuses.length && statuses.every((status) => /NOT_APPLICABLE/.test(String(status)))
@@ -147,6 +159,13 @@ const groups = [
147
159
  });
148
160
  const passedGroups = groups.filter((group) => group.status === "OK").length;
149
161
  const productCommit = identity.productCommit || data.productCommit || data.finalCommit;
162
+ const identityChain = [
163
+ ["检查点", identity.checkpointCommit || data.checkpointCommit],
164
+ ["产品", productCommit],
165
+ ["功能尖端", identity.featureTip || data.featureTip],
166
+ ["合并", identity.mergeCommit || data.mergeCommit || identity.featureMergeHash],
167
+ ["发布尖端", identity.releaseTip || data.releaseTip || identity.releaseTipHash]
168
+ ].filter(([, value]) => value).map(([label, value]) => `${label} ${shortHash(value)}`).join(" → ") || "N/A";
150
169
  const wallClock = timing.workflowWallClockMs ?? record(data.durations).totalMs;
151
170
 
152
171
  const groupHtml = groups.map((group) => `
@@ -201,7 +220,7 @@ const html = `<!doctype html>
201
220
  ${recordOnly ? '<div class="record-only">归档意图:仅记录 · 未请求发布</div>' : ""}
202
221
  </section>
203
222
  <section class="metrics">
204
- <article class="metric"><small>产品提交</small><strong><code title="${esc(productCommit || "N/A")}">${esc(shortHash(productCommit))}</code></strong></article>
223
+ <article class="metric"><small>产品提交链</small><strong><code title="${esc(identityChain)}">${esc(identityChain)}</code></strong></article>
205
224
  <article class="metric"><small>验证概览</small><strong>${passedGroups}/${groups.length} 组通过</strong><small>按后端 / Geo / 前端 / 浏览器 / API</small></article>
206
225
  <article class="metric"><small>风险与动作</small><strong>${risks.length} / ${actions.length}</strong><small>当前风险 / 人工动作</small></article>
207
226
  <article class="metric"><small>全流程耗时</small><strong>${esc(duration(wallClock))}</strong><small>活动 ${esc(duration(timing.stageActiveExecutionMs))}</small></article>
@@ -234,6 +253,7 @@ const html = `<!doctype html>
234
253
  <details><summary>命令证据(${commands.length})</summary><div class="table-wrap"><table><thead><tr><th>阶段</th><th>命令</th><th>结果</th></tr></thead><tbody>${commandHtml}</tbody></table></div></details>
235
254
  <details><summary>技术元数据</summary><div><dl>
236
255
  <dt>产品提交</dt><dd><code>${esc(productCommit || "N/A")}</code></dd>
256
+ <dt>检查点 → 产品 → 功能尖端 → 合并 → 发布尖端</dt><dd><code>${esc(identityChain)}</code></dd>
237
257
  <dt>产品树哈希</dt><dd><code>${esc(identity.productTreeHash || data.productTreeHash || "N/A")}</code></dd>
238
258
  <dt>环境哈希</dt><dd><code>${esc(identity.environmentHash || data.environmentHash || "N/A")}</code></dd>
239
259
  <dt>基线提交</dt><dd><code>${esc(identity.baseCommit || data.baseCommit || "N/A")}</code></dd>
@@ -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=e47155d4f364fc9e; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=5756e5211caf00c1; overlay=java; 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=e47155d4f364fc9e; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=5756e5211caf00c1; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-knowledge-ingest
8
8
 
9
9
  从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。