@hunter-harness/workflow-harness 0.2.38 → 0.2.40

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 (202) 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_deploy.py +10 -51
  17. package/harness/bundles/general/claude-code/scripts/harness_events.py +273 -21
  18. package/harness/bundles/general/claude-code/scripts/harness_gate.py +56 -32
  19. package/harness/bundles/general/claude-code/scripts/harness_integration.py +197 -3
  20. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +126 -2
  21. package/harness/bundles/general/claude-code/scripts/harness_report_model.py +28 -6
  22. package/harness/bundles/general/claude-code/scripts/harness_review.py +118 -17
  23. package/harness/bundles/general/claude-code/scripts/harness_service.py +219 -0
  24. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  25. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +10 -7
  26. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
  27. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  30. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
  33. package/harness/bundles/general/codebuddy/harness-review/reference.md +20 -5
  34. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +446 -20
  39. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +10 -51
  40. package/harness/bundles/general/codebuddy/scripts/harness_events.py +273 -21
  41. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +56 -32
  42. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +197 -3
  43. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +126 -2
  44. package/harness/bundles/general/codebuddy/scripts/harness_report_model.py +28 -6
  45. package/harness/bundles/general/codebuddy/scripts/harness_review.py +118 -17
  46. package/harness/bundles/general/codebuddy/scripts/harness_service.py +219 -0
  47. package/harness/bundles/general/codex/.harness-build.json +1 -1
  48. package/harness/bundles/general/codex/harness-archive/SKILL.md +10 -7
  49. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +24 -4
  50. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  52. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  53. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  54. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  55. package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
  56. package/harness/bundles/general/codex/harness-review/reference.md +20 -5
  57. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  58. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  59. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  60. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  61. package/harness/bundles/general/codex/scripts/harness_archive.py +446 -20
  62. package/harness/bundles/general/codex/scripts/harness_deploy.py +10 -51
  63. package/harness/bundles/general/codex/scripts/harness_events.py +273 -21
  64. package/harness/bundles/general/codex/scripts/harness_gate.py +56 -32
  65. package/harness/bundles/general/codex/scripts/harness_integration.py +197 -3
  66. package/harness/bundles/general/codex/scripts/harness_ledger.py +126 -2
  67. package/harness/bundles/general/codex/scripts/harness_report_model.py +28 -6
  68. package/harness/bundles/general/codex/scripts/harness_review.py +118 -17
  69. package/harness/bundles/general/codex/scripts/harness_service.py +219 -0
  70. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  71. package/harness/bundles/general/cursor/harness-archive/SKILL.md +10 -7
  72. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +24 -4
  73. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  74. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  75. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  76. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  77. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  78. package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
  79. package/harness/bundles/general/cursor/harness-review/reference.md +20 -5
  80. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  81. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  82. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  83. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  84. package/harness/bundles/general/cursor/scripts/harness_archive.py +446 -20
  85. package/harness/bundles/general/cursor/scripts/harness_deploy.py +10 -51
  86. package/harness/bundles/general/cursor/scripts/harness_events.py +273 -21
  87. package/harness/bundles/general/cursor/scripts/harness_gate.py +56 -32
  88. package/harness/bundles/general/cursor/scripts/harness_integration.py +197 -3
  89. package/harness/bundles/general/cursor/scripts/harness_ledger.py +126 -2
  90. package/harness/bundles/general/cursor/scripts/harness_report_model.py +28 -6
  91. package/harness/bundles/general/cursor/scripts/harness_review.py +118 -17
  92. package/harness/bundles/general/cursor/scripts/harness_service.py +219 -0
  93. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  94. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  95. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +10 -7
  96. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +24 -4
  97. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  98. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  99. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  100. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  101. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  102. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  103. package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
  104. package/harness/bundles/java/claude-code/harness-review/reference.md +20 -5
  105. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  106. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  107. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  108. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  109. package/harness/bundles/java/claude-code/scripts/harness_archive.py +446 -20
  110. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +10 -51
  111. package/harness/bundles/java/claude-code/scripts/harness_events.py +273 -21
  112. package/harness/bundles/java/claude-code/scripts/harness_gate.py +56 -32
  113. package/harness/bundles/java/claude-code/scripts/harness_integration.py +197 -3
  114. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +126 -2
  115. package/harness/bundles/java/claude-code/scripts/harness_report_model.py +28 -6
  116. package/harness/bundles/java/claude-code/scripts/harness_review.py +118 -17
  117. package/harness/bundles/java/claude-code/scripts/harness_service.py +219 -0
  118. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  119. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  120. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +10 -7
  121. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
  122. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  123. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  124. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  125. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  126. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  127. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  128. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
  129. package/harness/bundles/java/codebuddy/harness-review/reference.md +20 -5
  130. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  131. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  132. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  133. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  134. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +446 -20
  135. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +10 -51
  136. package/harness/bundles/java/codebuddy/scripts/harness_events.py +273 -21
  137. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +56 -32
  138. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +197 -3
  139. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +126 -2
  140. package/harness/bundles/java/codebuddy/scripts/harness_report_model.py +28 -6
  141. package/harness/bundles/java/codebuddy/scripts/harness_review.py +118 -17
  142. package/harness/bundles/java/codebuddy/scripts/harness_service.py +219 -0
  143. package/harness/bundles/java/codex/.harness-build.json +1 -1
  144. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  145. package/harness/bundles/java/codex/harness-archive/SKILL.md +10 -7
  146. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +24 -4
  147. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  148. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  149. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  150. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  151. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  152. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  153. package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
  154. package/harness/bundles/java/codex/harness-review/reference.md +20 -5
  155. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  156. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  157. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  158. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  159. package/harness/bundles/java/codex/scripts/harness_archive.py +446 -20
  160. package/harness/bundles/java/codex/scripts/harness_deploy.py +10 -51
  161. package/harness/bundles/java/codex/scripts/harness_events.py +273 -21
  162. package/harness/bundles/java/codex/scripts/harness_gate.py +56 -32
  163. package/harness/bundles/java/codex/scripts/harness_integration.py +197 -3
  164. package/harness/bundles/java/codex/scripts/harness_ledger.py +126 -2
  165. package/harness/bundles/java/codex/scripts/harness_report_model.py +28 -6
  166. package/harness/bundles/java/codex/scripts/harness_review.py +118 -17
  167. package/harness/bundles/java/codex/scripts/harness_service.py +219 -0
  168. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  169. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  170. package/harness/bundles/java/cursor/harness-archive/SKILL.md +10 -7
  171. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +24 -4
  172. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  173. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  174. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  175. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  176. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  177. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  178. package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
  179. package/harness/bundles/java/cursor/harness-review/reference.md +20 -5
  180. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  181. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  182. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  183. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  184. package/harness/bundles/java/cursor/scripts/harness_archive.py +446 -20
  185. package/harness/bundles/java/cursor/scripts/harness_deploy.py +10 -51
  186. package/harness/bundles/java/cursor/scripts/harness_events.py +273 -21
  187. package/harness/bundles/java/cursor/scripts/harness_gate.py +56 -32
  188. package/harness/bundles/java/cursor/scripts/harness_integration.py +197 -3
  189. package/harness/bundles/java/cursor/scripts/harness_ledger.py +126 -2
  190. package/harness/bundles/java/cursor/scripts/harness_report_model.py +28 -6
  191. package/harness/bundles/java/cursor/scripts/harness_review.py +118 -17
  192. package/harness/bundles/java/cursor/scripts/harness_service.py +219 -0
  193. package/harness/manifests/general/claude-code.json +25 -25
  194. package/harness/manifests/general/codebuddy.json +25 -25
  195. package/harness/manifests/general/codex.json +25 -25
  196. package/harness/manifests/general/cursor.json +25 -25
  197. package/harness/manifests/java/claude-code.json +27 -27
  198. package/harness/manifests/java/codebuddy.json +27 -27
  199. package/harness/manifests/java/codex.json +27 -27
  200. package/harness/manifests/java/cursor.json +27 -27
  201. package/hunter-workflow-family.json +4 -4
  202. 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,9 +2,9 @@
2
2
  "schema_version": 2,
3
3
  "profile": "general",
4
4
  "adapter": "claude-code",
5
- "bundle_version": "0.2.28",
5
+ "bundle_version": "0.2.29",
6
6
  "requires": {
7
- "minimumCliVersion": "0.2.40",
7
+ "minimumCliVersion": "0.2.41",
8
8
  "capabilities": [
9
9
  "sync@1",
10
10
  "rules-sync@1",
@@ -22,7 +22,7 @@
22
22
  "files": [
23
23
  {
24
24
  "path": ".harness-build.json",
25
- "sha256": "6502c319f9300c8756e88bb4d1fa9d71793cef7f87eb84683b5567a237230433"
25
+ "sha256": "1c20c4038c1b683807bfaa3aea3fcefb336c5f5b53527426436929fc0e3bb102"
26
26
  },
27
27
  {
28
28
  "path": "agents/harness-evaluator.md",
@@ -74,11 +74,11 @@
74
74
  },
75
75
  {
76
76
  "path": "harness-archive/SKILL.md",
77
- "sha256": "e322f9da025fd9443b446c27b8bed66fc589386f1bfab6fb4480e029e38ecb95"
77
+ "sha256": "02499e773d75375f3321e002f6c624a5c0bc462be4730306d3083a789f5b301b"
78
78
  },
79
79
  {
80
80
  "path": "harness-archive/templates/render-summary.mjs",
81
- "sha256": "b437798e93e4bf1afc31305de9e448e4f292ed57513b059ffab59bc53f6dc347"
81
+ "sha256": "6f5198906276a80859e1dc17793113c39dea8fb510c51678e0c1f91e40fddfa0"
82
82
  },
83
83
  {
84
84
  "path": "harness-archive/templates/summary-data-template.json",
@@ -94,7 +94,7 @@
94
94
  },
95
95
  {
96
96
  "path": "harness-codebase-map/SKILL.md",
97
- "sha256": "a84c7adcae064a729ce9ab6ab41a2936b197d70a38402f14e388af628b76c4c9"
97
+ "sha256": "9097ce35521ded60ab193306be4ad7495ad33d0a09eff0cc46011dc82d8597ea"
98
98
  },
99
99
  {
100
100
  "path": "harness-codebase-map/templates/ARCHITECTURE.md",
@@ -154,15 +154,15 @@
154
154
  },
155
155
  {
156
156
  "path": "harness-knowledge-ingest/scripts/harness_knowledge.py",
157
- "sha256": "4c83c3282b956143de287e6817f34687457c98f456f71749e2ad6ac4546a2498"
157
+ "sha256": "b451ad12a16758b69b6852e0a4348b3889795c05cbe691d49efdeb78a1abecea"
158
158
  },
159
159
  {
160
160
  "path": "harness-knowledge-ingest/SKILL.md",
161
- "sha256": "56c5a9b22ccf70763fe45dadda515b7b7fb819e792c1445f40dffd96688e232c"
161
+ "sha256": "0cbb6caf5c59c2c41a5458a1f99dcdd196814baa8b78a6f2d5ea29c6f61c724f"
162
162
  },
163
163
  {
164
164
  "path": "harness-knowledge-query/SKILL.md",
165
- "sha256": "6e5d0ed600d479aeda3824710675746e04455b54126eea5fb7573dff958d8901"
165
+ "sha256": "9b581ff1c60cd97326d298c3dca7e097773c217b60307c5867402d314f90bc02"
166
166
  },
167
167
  {
168
168
  "path": "harness-plan/checklist.md",
@@ -178,7 +178,7 @@
178
178
  },
179
179
  {
180
180
  "path": "harness-plan/SKILL.md",
181
- "sha256": "17c38a879581733b674ab637f1bfac299a18d58e741217cc8e754381e7c5d6ba"
181
+ "sha256": "a85cd176d3c076cb797a1a7321acbedc25529b89ff706c9dbb4bceb54c4106c2"
182
182
  },
183
183
  {
184
184
  "path": "harness-review/checklist.md",
@@ -190,11 +190,11 @@
190
190
  },
191
191
  {
192
192
  "path": "harness-review/reference.md",
193
- "sha256": "5e43f753b2189bc8256c04905c079016a31694dad2aa1ad841aceae3670dd6c6"
193
+ "sha256": "94bfb479910abbf393721887c4ce1b7d4259d170a1223c3315330d317ce77510"
194
194
  },
195
195
  {
196
196
  "path": "harness-review/SKILL.md",
197
- "sha256": "0a295bf9e189d5f4664c5df4fb5f6c95b3d489c7e91fbe4ac741722086310aa0"
197
+ "sha256": "a537781cb692a6c96f1c5b1062477a7d81092501d53978a3ca7558e9c08f2a34"
198
198
  },
199
199
  {
200
200
  "path": "harness-run/checklist.md",
@@ -210,7 +210,7 @@
210
210
  },
211
211
  {
212
212
  "path": "harness-run/SKILL.md",
213
- "sha256": "798c96ba1e8ec8ccb180ccbf9f82a49164f5a8869d85ec96a13b93476841717d"
213
+ "sha256": "10ea4152e1085859799b1ebee36e48400e33733efee367917e4b8ffd6092c856"
214
214
  },
215
215
  {
216
216
  "path": "harness-submit/checklist.md",
@@ -222,7 +222,7 @@
222
222
  },
223
223
  {
224
224
  "path": "harness-submit/SKILL.md",
225
- "sha256": "e13c96a448efeb54ac1c1f7239754be351d6a8bbb9959d679fbf1ecf0390aa15"
225
+ "sha256": "c28a19c734242b03460fd0e977212aac40c91add18fb6213c8b182b181c52a50"
226
226
  },
227
227
  {
228
228
  "path": "harness-sync/reference.md",
@@ -230,7 +230,7 @@
230
230
  },
231
231
  {
232
232
  "path": "harness-sync/SKILL.md",
233
- "sha256": "1315123144c3d2c3d79cf52e8a8745cd282fb6e4ded2b3486efabee6981e2f5d"
233
+ "sha256": "9a75623d0ef60b9d17e2bc7752aeb5d50337ac6bb3c01942ce47d84cf6cdb7f2"
234
234
  },
235
235
  {
236
236
  "path": "harness-test/checklist.md",
@@ -254,7 +254,7 @@
254
254
  },
255
255
  {
256
256
  "path": "harness-test/SKILL.md",
257
- "sha256": "02f62492e02e3cf6cbdb2dc3720e43245dc9b067fa9e2ff5dcf3668e8c524ac8"
257
+ "sha256": "64ba3253305174716f8d109c5b3a4e9ddcf1cc0908b2c8e4fe5a7e85af8c9326"
258
258
  },
259
259
  {
260
260
  "path": "protocols/archive-report-protocol.md",
@@ -322,7 +322,7 @@
322
322
  },
323
323
  {
324
324
  "path": "scripts/harness_archive.py",
325
- "sha256": "77b77ccbea2ec201294eb13763f90ca7bb2fc8f1fbdb4456e455d02bbcc78309"
325
+ "sha256": "d9992c4f1a5e3c777ee4d6a8415b1a127dc28d2921110e4bd9fd2726827c0abb"
326
326
  },
327
327
  {
328
328
  "path": "scripts/harness_change.py",
@@ -338,7 +338,7 @@
338
338
  },
339
339
  {
340
340
  "path": "scripts/harness_deploy.py",
341
- "sha256": "c10045fc86f2ac87d6ba60b5bcd31e9698d306a2d94eccfcff4a623cf817bf5c"
341
+ "sha256": "fb4290c2dd5c28a09c378da5ee7be491bdefe0410f277ec5dd2af1952cfddbca"
342
342
  },
343
343
  {
344
344
  "path": "scripts/harness_environment.py",
@@ -346,19 +346,19 @@
346
346
  },
347
347
  {
348
348
  "path": "scripts/harness_events.py",
349
- "sha256": "7f5e9e316c422f7532fe0fcf52beb809125f757af7ccfbdf85fa519c2de4e66d"
349
+ "sha256": "0d4db19d71d5945837c255192f7c915af447f98e2ac66bf60e25b93f1613b391"
350
350
  },
351
351
  {
352
352
  "path": "scripts/harness_gate.py",
353
- "sha256": "37bdb2eaaac2652fe5cdf142572e99379b1b17630f365a01b6e3e71333055a8a"
353
+ "sha256": "d6085f724ea59f0fcd47a190a7c45ce81c77a21e84fba185e47c6b21b84c054e"
354
354
  },
355
355
  {
356
356
  "path": "scripts/harness_integration.py",
357
- "sha256": "88d0a6ffbdec74ce8c3a994c1ec80e5a0c550b3cccad8d46fb8c355ced1a7721"
357
+ "sha256": "19ff557794609d94af780f0055aa0a50d47cfd6775f97c163d542205d2ab0044"
358
358
  },
359
359
  {
360
360
  "path": "scripts/harness_ledger.py",
361
- "sha256": "ffb7aa2f63b86edf74730dcf44cb530266bda2a8064bada11cd8240651b634e5"
361
+ "sha256": "3b00ff15958a1cc650ee32f6a3d8807f4585b75c5bce33707fc9e48c9ec71235"
362
362
  },
363
363
  {
364
364
  "path": "scripts/harness_migration_head.py",
@@ -390,7 +390,7 @@
390
390
  },
391
391
  {
392
392
  "path": "scripts/harness_report_model.py",
393
- "sha256": "fd30968d429aed3a82bddd3203ccfe89807c00fa2d67787b625c67182a289feb"
393
+ "sha256": "08b6df8f053ea3381ca24571056817e0756a06cef01edd1aaee6a7eb492426d6"
394
394
  },
395
395
  {
396
396
  "path": "scripts/harness_retry.py",
@@ -398,7 +398,7 @@
398
398
  },
399
399
  {
400
400
  "path": "scripts/harness_review.py",
401
- "sha256": "f5b9170f5c3b35c87bd340e8368cd08d8940176a67fb11666518107ba3626e86"
401
+ "sha256": "a493efd57d743f7414617ffa2e8cac59d6ddd102b0e45cab60795492bf442e20"
402
402
  },
403
403
  {
404
404
  "path": "scripts/harness_runtime.py",
@@ -406,7 +406,7 @@
406
406
  },
407
407
  {
408
408
  "path": "scripts/harness_service.py",
409
- "sha256": "af0eb7c93d5f856c5ed5a977e3f9a12dc0ced5baca571f7023f6b04ece46baf6"
409
+ "sha256": "a897ccb4ef397d1677e7d45ce55357b37f0083ea7e985685604c40a6da075d12"
410
410
  },
411
411
  {
412
412
  "path": "scripts/harness_state.py",