@hunter-harness/workflow-harness 0.2.69 → 0.2.70

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 +1 -1
  3. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +3 -1
  7. package/harness/bundles/general/claude-code/harness-plan/reference.md +31 -1
  8. package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +3 -1
  12. package/harness/bundles/general/claude-code/harness-run/reference.md +20 -0
  13. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -1
  16. package/harness/bundles/general/claude-code/harness-test/pitfalls.md +34 -3
  17. package/harness/bundles/general/claude-code/harness-test/reference.md +9 -0
  18. package/harness/bundles/general/claude-code/scripts/harness_context.py +27 -7
  19. package/harness/bundles/general/claude-code/scripts/harness_events.py +12 -3
  20. package/harness/bundles/general/claude-code/scripts/harness_gate.py +127 -11
  21. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +244 -5
  22. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +254 -19
  23. package/harness/bundles/general/claude-code/scripts/harness_service.py +69 -15
  24. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +48 -1
  25. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  26. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  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-query/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +3 -1
  31. package/harness/bundles/general/codebuddy/harness-plan/reference.md +31 -1
  32. package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +3 -1
  36. package/harness/bundles/general/codebuddy/harness-run/reference.md +20 -0
  37. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -1
  40. package/harness/bundles/general/codebuddy/harness-test/pitfalls.md +34 -3
  41. package/harness/bundles/general/codebuddy/harness-test/reference.md +9 -0
  42. package/harness/bundles/general/codebuddy/scripts/harness_context.py +27 -7
  43. package/harness/bundles/general/codebuddy/scripts/harness_events.py +12 -3
  44. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +127 -11
  45. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +244 -5
  46. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +254 -19
  47. package/harness/bundles/general/codebuddy/scripts/harness_service.py +69 -15
  48. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +48 -1
  49. package/harness/bundles/general/codex/.harness-build.json +1 -1
  50. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  52. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  53. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  54. package/harness/bundles/general/codex/harness-plan/SKILL.md +3 -1
  55. package/harness/bundles/general/codex/harness-plan/reference.md +31 -1
  56. package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
  58. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  59. package/harness/bundles/general/codex/harness-run/SKILL.md +3 -1
  60. package/harness/bundles/general/codex/harness-run/reference.md +20 -0
  61. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  62. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  63. package/harness/bundles/general/codex/harness-test/SKILL.md +3 -1
  64. package/harness/bundles/general/codex/harness-test/pitfalls.md +34 -3
  65. package/harness/bundles/general/codex/harness-test/reference.md +9 -0
  66. package/harness/bundles/general/codex/scripts/harness_context.py +27 -7
  67. package/harness/bundles/general/codex/scripts/harness_events.py +12 -3
  68. package/harness/bundles/general/codex/scripts/harness_gate.py +127 -11
  69. package/harness/bundles/general/codex/scripts/harness_ledger.py +244 -5
  70. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +254 -19
  71. package/harness/bundles/general/codex/scripts/harness_service.py +69 -15
  72. package/harness/bundles/general/codex/scripts/harness_test_guard.py +48 -1
  73. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  74. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  75. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  76. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  77. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  78. package/harness/bundles/general/cursor/harness-plan/SKILL.md +3 -1
  79. package/harness/bundles/general/cursor/harness-plan/reference.md +31 -1
  80. package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
  81. package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
  82. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  83. package/harness/bundles/general/cursor/harness-run/SKILL.md +3 -1
  84. package/harness/bundles/general/cursor/harness-run/reference.md +20 -0
  85. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  86. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  87. package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -1
  88. package/harness/bundles/general/cursor/harness-test/pitfalls.md +34 -3
  89. package/harness/bundles/general/cursor/harness-test/reference.md +9 -0
  90. package/harness/bundles/general/cursor/scripts/harness_context.py +27 -7
  91. package/harness/bundles/general/cursor/scripts/harness_events.py +12 -3
  92. package/harness/bundles/general/cursor/scripts/harness_gate.py +127 -11
  93. package/harness/bundles/general/cursor/scripts/harness_ledger.py +244 -5
  94. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +254 -19
  95. package/harness/bundles/general/cursor/scripts/harness_service.py +69 -15
  96. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +48 -1
  97. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  98. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  99. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  100. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  101. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  102. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  103. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  104. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +3 -1
  105. package/harness/bundles/java/claude-code/harness-plan/reference.md +31 -1
  106. package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
  107. package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
  108. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  109. package/harness/bundles/java/claude-code/harness-run/SKILL.md +3 -1
  110. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  111. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  112. package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -1
  113. package/harness/bundles/java/claude-code/harness-test/pitfalls.md +34 -3
  114. package/harness/bundles/java/claude-code/scripts/harness_context.py +27 -7
  115. package/harness/bundles/java/claude-code/scripts/harness_events.py +12 -3
  116. package/harness/bundles/java/claude-code/scripts/harness_gate.py +127 -11
  117. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +244 -5
  118. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +254 -19
  119. package/harness/bundles/java/claude-code/scripts/harness_service.py +69 -15
  120. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +48 -1
  121. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  122. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  123. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  124. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  125. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  126. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  127. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  128. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +3 -1
  129. package/harness/bundles/java/codebuddy/harness-plan/reference.md +31 -1
  130. package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
  131. package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
  132. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  133. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +3 -1
  134. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  135. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  136. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -1
  137. package/harness/bundles/java/codebuddy/harness-test/pitfalls.md +34 -3
  138. package/harness/bundles/java/codebuddy/scripts/harness_context.py +27 -7
  139. package/harness/bundles/java/codebuddy/scripts/harness_events.py +12 -3
  140. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +127 -11
  141. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +244 -5
  142. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +254 -19
  143. package/harness/bundles/java/codebuddy/scripts/harness_service.py +69 -15
  144. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +48 -1
  145. package/harness/bundles/java/codex/.harness-build.json +1 -1
  146. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  147. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  148. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  149. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  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 +3 -1
  153. package/harness/bundles/java/codex/harness-plan/reference.md +31 -1
  154. package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
  155. package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
  156. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  157. package/harness/bundles/java/codex/harness-run/SKILL.md +3 -1
  158. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  159. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  160. package/harness/bundles/java/codex/harness-test/SKILL.md +3 -1
  161. package/harness/bundles/java/codex/harness-test/pitfalls.md +34 -3
  162. package/harness/bundles/java/codex/scripts/harness_context.py +27 -7
  163. package/harness/bundles/java/codex/scripts/harness_events.py +12 -3
  164. package/harness/bundles/java/codex/scripts/harness_gate.py +127 -11
  165. package/harness/bundles/java/codex/scripts/harness_ledger.py +244 -5
  166. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +254 -19
  167. package/harness/bundles/java/codex/scripts/harness_service.py +69 -15
  168. package/harness/bundles/java/codex/scripts/harness_test_guard.py +48 -1
  169. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  170. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  171. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  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-query/SKILL.md +1 -1
  175. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  176. package/harness/bundles/java/cursor/harness-plan/SKILL.md +3 -1
  177. package/harness/bundles/java/cursor/harness-plan/reference.md +31 -1
  178. package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
  179. package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
  180. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  181. package/harness/bundles/java/cursor/harness-run/SKILL.md +3 -1
  182. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  183. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  184. package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -1
  185. package/harness/bundles/java/cursor/harness-test/pitfalls.md +34 -3
  186. package/harness/bundles/java/cursor/scripts/harness_context.py +27 -7
  187. package/harness/bundles/java/cursor/scripts/harness_events.py +12 -3
  188. package/harness/bundles/java/cursor/scripts/harness_gate.py +127 -11
  189. package/harness/bundles/java/cursor/scripts/harness_ledger.py +244 -5
  190. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +254 -19
  191. package/harness/bundles/java/cursor/scripts/harness_service.py +69 -15
  192. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +48 -1
  193. package/harness/manifests/general/claude-code.json +26 -26
  194. package/harness/manifests/general/codebuddy.json +26 -26
  195. package/harness/manifests/general/codex.json +26 -26
  196. package/harness/manifests/general/cursor.json +26 -26
  197. package/harness/manifests/java/claude-code.json +26 -26
  198. package/harness/manifests/java/codebuddy.json +26 -26
  199. package/harness/manifests/java/codex.json +26 -26
  200. package/harness/manifests/java/cursor.json +26 -26
  201. package/hunter-workflow-family.json +5 -5
  202. package/package.json +1 -1
@@ -1462,6 +1462,10 @@ def _job_id_path(change_dir: Path) -> Path:
1462
1462
  return _runtime_dir(change_dir) / "_harness_service.job.id"
1463
1463
 
1464
1464
 
1465
+ def _breakaway_note_path(change_dir: Path) -> Path:
1466
+ return _runtime_dir(change_dir) / "_harness_service.breakaway.json"
1467
+
1468
+
1465
1469
  def _child_pid_path(change_dir: Path) -> Path:
1466
1470
  return _runtime_dir(change_dir) / "_harness_service.child.pid"
1467
1471
 
@@ -1574,20 +1578,56 @@ def _start_detached_service_windows(
1574
1578
  str(pid_path),
1575
1579
  job_id,
1576
1580
  ]
1577
- win_flags = (
1578
- _DETACHED_PROCESS
1579
- | _CREATE_NEW_PROCESS_GROUP
1580
- | _CREATE_NO_WINDOW
1581
- | _CREATE_BREAKAWAY_FROM_JOB
1582
- )
1583
- launcher_proc = subprocess.Popen(
1584
- launcher_args,
1585
- cwd=str(cwd),
1586
- stdin=subprocess.DEVNULL,
1587
- stdout=subprocess.DEVNULL,
1588
- stderr=subprocess.DEVNULL,
1589
- creationflags=win_flags,
1590
- close_fds=False,
1581
+ base_flags = _DETACHED_PROCESS | _CREATE_NEW_PROCESS_GROUP | _CREATE_NO_WINDOW
1582
+ # CREATE_BREAKAWAY_FROM_JOB fails with ERROR_ACCESS_DENIED (WinError 5)
1583
+ # whenever the *caller* already runs inside a Job Object that was created
1584
+ # without JOB_OBJECT_LIMIT_BREAKAWAY_OK — which is exactly how agent CLIs
1585
+ # (Claude Code / CodeBuddy / Codex) contain their child processes. Breakaway
1586
+ # is an optimisation (the service outlives the agent session), not a
1587
+ # requirement, so fall back to spawning inside the caller's job rather than
1588
+ # failing the whole start.
1589
+ launcher_proc = None
1590
+ breakaway = True
1591
+ try:
1592
+ launcher_proc = subprocess.Popen(
1593
+ launcher_args,
1594
+ cwd=str(cwd),
1595
+ stdin=subprocess.DEVNULL,
1596
+ stdout=subprocess.DEVNULL,
1597
+ stderr=subprocess.DEVNULL,
1598
+ creationflags=base_flags | _CREATE_BREAKAWAY_FROM_JOB,
1599
+ close_fds=False,
1600
+ )
1601
+ except OSError as exc:
1602
+ if getattr(exc, "winerror", None) != 5:
1603
+ raise
1604
+ breakaway = False
1605
+ launcher_proc = subprocess.Popen(
1606
+ launcher_args,
1607
+ cwd=str(cwd),
1608
+ stdin=subprocess.DEVNULL,
1609
+ stdout=subprocess.DEVNULL,
1610
+ stderr=subprocess.DEVNULL,
1611
+ creationflags=base_flags,
1612
+ close_fds=False,
1613
+ )
1614
+ _breakaway_note_path(change_dir).write_text(
1615
+ json.dumps(
1616
+ {
1617
+ "breakawayFromJob": breakaway,
1618
+ "note": (
1619
+ None
1620
+ if breakaway
1621
+ else "spawned inside the caller's Job Object "
1622
+ "(CREATE_BREAKAWAY_FROM_JOB denied); the service is "
1623
+ "terminated when the agent session ends"
1624
+ ),
1625
+ },
1626
+ ensure_ascii=False,
1627
+ )
1628
+ + "\n",
1629
+ encoding="utf-8",
1630
+ newline="\n",
1591
1631
  )
1592
1632
  _launcher_pid_path(change_dir).write_text(str(launcher_proc.pid), encoding="utf-8")
1593
1633
  _wait_for_child_pid(pid_path)
@@ -2400,7 +2440,21 @@ def _start_and_record(
2400
2440
  }
2401
2441
  )
2402
2442
  write_json(mutation_state_path(change_dir), mutation_state)
2403
- return emit_error(f"failed to start service: {exc}", as_json=as_json)
2443
+ if getattr(exc, "winerror", None) == 5:
2444
+ return emit_error(
2445
+ "failed to start service: Windows denied process creation "
2446
+ f"({exc}). The agent shell blocks detached spawns; start the "
2447
+ "service from a terminal the agent does not own (or use the "
2448
+ "documented nohup fallback) and re-run `ensure` so the harness "
2449
+ "adopts the running instance.",
2450
+ as_json=as_json,
2451
+ reasonCode="SERVICE_SPAWN_DENIED",
2452
+ )
2453
+ return emit_error(
2454
+ f"failed to start service: {exc}",
2455
+ as_json=as_json,
2456
+ reasonCode="SERVICE_START_FAILED",
2457
+ )
2404
2458
 
2405
2459
  # Give the OS a moment to register the process before identity/create-time reads
2406
2460
  time.sleep(0.15)
@@ -178,6 +178,32 @@ def _state_project_root(project: Path) -> Path:
178
178
  return resolved.parent if resolved.name == ".git" else project
179
179
 
180
180
 
181
+ def _invalid_project_root(
182
+ action: str, project: Path | str, resolved: Path
183
+ ) -> dict[str, Any] | None:
184
+ """Reject a --project that is not an existing directory.
185
+
186
+ Passing the *project name* (``--project udp``) instead of its path used to
187
+ resolve to ``<cwd>/udp`` and then surface as SNAPSHOT_MISSING — an error
188
+ that points at the wrong thing entirely. Fail here with the resolved path
189
+ so the real mistake is visible.
190
+ """
191
+ if resolved.is_dir():
192
+ return None
193
+ return _result(
194
+ False,
195
+ action,
196
+ "PROJECT_ROOT_INVALID",
197
+ [],
198
+ project=str(project),
199
+ resolvedProject=str(resolved),
200
+ hint=(
201
+ "--project takes a filesystem path to the project root "
202
+ "(use '.' when running from it), not the project name"
203
+ ),
204
+ )
205
+
206
+
181
207
  def _change_dir(project: Path, change_dir: Path | str) -> Path | None:
182
208
  state_project = _state_project_root(project)
183
209
  candidate = Path(change_dir)
@@ -556,6 +582,9 @@ def record(
556
582
  ) -> dict[str, Any]:
557
583
  action = "record"
558
584
  project_root = Path(project).resolve()
585
+ invalid_root = _invalid_project_root(action, project, project_root)
586
+ if invalid_root is not None:
587
+ return invalid_root
559
588
  if not files:
560
589
  return _result(False, action, "EMPTY_FILES", [])
561
590
  if reason not in REASONS:
@@ -1452,6 +1481,9 @@ def _reconcile_close_manifest(
1452
1481
  def begin(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
1453
1482
  action = "begin"
1454
1483
  project_root = Path(project).resolve()
1484
+ invalid_root = _invalid_project_root(action, project, project_root)
1485
+ if invalid_root is not None:
1486
+ return invalid_root
1455
1487
  change_root = _change_dir(project_root, change_dir)
1456
1488
  if change_root is None:
1457
1489
  return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
@@ -1545,12 +1577,24 @@ def begin(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
1545
1577
  def close(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
1546
1578
  action = "close"
1547
1579
  project_root = Path(project).resolve()
1580
+ invalid_root = _invalid_project_root(action, project, project_root)
1581
+ if invalid_root is not None:
1582
+ return invalid_root
1548
1583
  change_root = _change_dir(project_root, change_dir)
1549
1584
  if change_root is None:
1550
1585
  return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
1551
1586
  snapshot_path = _state_root(change_root) / SNAPSHOT_REL
1552
1587
  if not snapshot_path.is_file():
1553
- return _result(False, action, "SNAPSHOT_MISSING", [])
1588
+ # Name the exact path: this snapshot is evidence/test-guard-snapshot.json,
1589
+ # not meta/state-snapshot.json, and the two are easy to confuse.
1590
+ return _result(
1591
+ False,
1592
+ action,
1593
+ "SNAPSHOT_MISSING",
1594
+ [],
1595
+ expectedSnapshot=str(snapshot_path),
1596
+ hint="run `harness_test_guard.py begin` for this change first",
1597
+ )
1554
1598
  try:
1555
1599
  snapshot = _read_json(snapshot_path)
1556
1600
  except (OSError, json.JSONDecodeError) as exc:
@@ -1752,6 +1796,9 @@ def mark(
1752
1796
  def stage(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
1753
1797
  action = "stage"
1754
1798
  project_root = Path(project).resolve()
1799
+ invalid_root = _invalid_project_root(action, project, project_root)
1800
+ if invalid_root is not None:
1801
+ return invalid_root
1755
1802
  change_root = _change_dir(project_root, change_dir)
1756
1803
  if change_root is None:
1757
1804
  return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "java",
4
4
  "agent": "codebuddy",
5
- "coreHash": "9f91fcbcac46e102"
5
+ "coreHash": "86f5a6b27795a615"
6
6
  }
@@ -3,7 +3,7 @@ name: harness-apidoc
3
3
  description: 从 Java 后端代码生成结构化前端 API 文档(Controller/VO/ErrorCode)。仅当用户显式调用
4
4
  /harness-apidoc 时使用,不得自动触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-apidoc — 前端接口文档(Java overlay)
8
8
 
9
9
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-archive
3
3
  description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到 .harness/archive/,含归档元数据。仅当用户显式调用
4
4
  /harness-archive 时使用;不得被其他阶段 skill 自动接续触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-archive — 变更归档
8
8
 
9
9
  ## Purpose
@@ -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=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-codebase-map — 代码库地图
8
8
 
9
9
  ## Purpose
@@ -2,7 +2,7 @@
2
2
  name: harness-knowledge-ingest
3
3
  description: 确认归档 ZIP 已上传并由 Hunter Platform 在服务端解包、校验和入库。客户端不再构建或维护本地知识索引。
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
6
6
  # harness-knowledge-ingest
7
7
 
8
8
  知识 ingest 完全由 Hunter Platform 负责。归档完成时,客户端生成一个确定性 ZIP;
@@ -2,7 +2,7 @@
2
2
  name: harness-knowledge-query
3
3
  description: 在规划、实现或排查前,通过 hunter-harness CLI 查询远端项目知识库。远端不可用时直接报告不可用,不建立本地索引或离线回退。
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
6
6
  # harness-knowledge-query
7
7
 
8
8
  项目知识以 Hunter Platform 的服务端索引为唯一真源。客户端只提交查询并消费结果:
@@ -2,7 +2,7 @@
2
2
  name: harness-package
3
3
  description: 增量模块打包:拉取最新→编译验证→mvn package→汇总 jar/war。仅当用户显式调用 /harness-package 时使用,不得自动触发。
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
6
6
  # harness-package — 增量模块打包(Java overlay)
7
7
 
8
8
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-plan
3
3
  description: 将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。仅当用户显式调用 /harness-plan 或明确要求进入
4
4
  Harness 规划阶段时使用;不得因用户描述需求就自动触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-plan — 需求规划
8
8
 
9
9
  ## Purpose
@@ -102,6 +102,8 @@ change-name 范围变更 → 提示重命名或记 🟡WARN(→ `reference.md`
102
102
  | 空目录 | 不得为“预留目录”生成 `.gitkeep`;只有产品明确需要跟踪空目录时才能创建,并在计划中说明业务原因 |
103
103
  | 设计审批包 | 一次 blocking user confirmation 含 worktree(读 `harness.json` `defaultWorktree`) |
104
104
  | 阶段 8 | 二选一且不得混用:**v2** = `hunter-harness plan finalize`(证据包 → 八 target + journal committed + plan-events.ndjson);**legacy** = 六项标准产物先进入 staging,仅 finalizer 校验成功后发布并写唯一 `phase.end`/log,随后 `verify` 确认 start/end、收据完整覆盖六项标准产物、哈希、全部任务表和非空场景清单一致。失败均不得手工补终态 |
105
+ | 发布后改产物 | 用 `harness_plan_finalize.py republish --run-id <全新> --reason "<why>"` 一次完成(新 attempt + 换收据 + 重新派生 manifest)。重跑 `finalize` 会报 `PLAN_FINALIZATION_HASH_CONFLICT`;**绝不手改 `meta/scenario-manifest.json`**(派生物,手改必致 `ARTIFACT_HASH_DRIFT`)→ `reference.md`「发布后修订计划」 |
106
+ | v2 输入骨架 | 不要猜 `plan-evidence-input.json` 结构,也不要去翻 TS 接口/npx 缓存:`npx hunter-harness plan evidence-pack --print-template` 直接给带占位符的完整骨架 |
105
107
  | Plan 结束 | **禁止**询问执行模式;只提示 `/harness-run` |
106
108
  | 知识查询 | 阶段 1 失败不得假装已读历史,也不得改用本地索引或其他执行入口 |
107
109
  | 歧义优先检查 | 否定、对比、动作对象或范围存在多种合理解释时,最小取证后先给推荐理解并一次一问;确认前不深挖错误方向 |
@@ -357,7 +357,14 @@ npx hunter-harness plan evidence-pack --input .harness/changes/<cn>/meta/plan-ev
357
357
  npx hunter-harness plan finalize --input .harness/changes/<cn>/meta/plan-evidence.json
358
358
  ```
359
359
 
360
- **自然输入文件**(`meta/plan-evidence-input.json`,权威定义 `packages/cli/src/commands/plan-evidence-pack.ts` 的 `EvidencePackInputFile`)由规划阶段逐步沉淀,各字段定稿时点不得倒置:
360
+ **自然输入文件**(`meta/plan-evidence-input.json`,权威定义 `packages/cli/src/commands/plan-evidence-pack.ts` 的 `EvidencePackInputFile`)由规划阶段逐步沉淀,各字段定稿时点不得倒置。
361
+
362
+ > 📋 **先取骨架,别猜结构**:CLI 自带模板输出,不需要去找 TS 接口或翻 npx 缓存——
363
+ > ```bash
364
+ > npx hunter-harness plan evidence-pack --print-template > .harness/changes/<cn>/meta/plan-evidence-input.json
365
+ > ```
366
+ > 输出是带 `<...>` 占位符的完整骨架,逐项替换即可;替换完 grep 一次 `<` 自检有无遗漏。
367
+ > `--print-template` 不读写任何文件,只打到 stdout。
361
368
 
362
369
  | 字段 | 内容 | 定稿阶段 |
363
370
  |------|------|:---:|
@@ -379,6 +386,29 @@ npx hunter-harness plan finalize --input .harness/changes/<cn>/meta/plan-evidenc
379
386
  - **legacy 收据**:过渡期(阶段 14 前)v2 路径不写 `plan-finalization.json`;消费方若仍读 legacy receipt,由兼容投影单独提供,不得反向要求 v2 双写。
380
387
  - **回退**:v2 自然输入不完整(如缺真实审批记录)时才允许走 Python finalizer(legacy 路径);同一次发布不得两条路径混用。
381
388
 
389
+ ### 发布后修订计划(republish)
390
+
391
+ 计划发布后又要改产物,是**正常且高频**的情况——用户看完计划补一个回归场景、修正一条任务、调整验收标准。这时不要与哈希守卫搏斗:
392
+
393
+ ```bash
394
+ python <skills-root>/scripts/harness_plan_finalize.py republish \
395
+ --change-dir ".harness/changes/<cn>" --staging-dir ".harness/changes/<cn>/runtime/plan-staging" \
396
+ --change <cn> --run-id "plan_$(uuidgen)" --reason "<为什么要改>" --json
397
+ ```
398
+
399
+ 它一次性完成整套动作:校验 staging → 分配新 attempt(自动取已用最大值 +1)→ 追加 `phase.start` → 替换收据 → 重新派生 `scenario-manifest.json` 与 `implementation-checkpoints.json` → 写 `phase.end`。收据里保留 `supersedes`(被取代的 hash/runId/attempt)与 `amendReason`,修订全程可审计。
400
+
401
+ | 约束 | 说明 |
402
+ |------|------|
403
+ | `--reason` 必填 | 修订已发布计划必须留下理由,否则 `PLAN_AMEND_REASON_REQUIRED` |
404
+ | `--run-id` 必须全新 | 复用旧 run-id 报 `PLAN_AMEND_RUN_ID_IN_USE`(attempt 与 run-id 一一绑定) |
405
+ | 首次发布不能用它 | 无收据时报 `PLAN_NOT_FINALIZED`,首次发布走 `finalize` |
406
+ | 内容没变则空操作 | 返回 `idempotent:true`,不写事件、不消耗 attempt |
407
+
408
+ ⚠️ **绝对不要手改 `meta/scenario-manifest.json`**。它是 finalizer 从 `test-scenarios.md` 派生的产物,手改会造成真实漂移:`verify` 报 `ARTIFACT_HASH_DRIFT`,run 阶段 `validate_plan_handoff` 也会记 WARN。`republish` 会重新派生它,这才是唯一正确入口。
409
+
410
+ > 直接重跑 `finalize` 会报 `PLAN_FINALIZATION_HASH_CONFLICT`——这是守卫在防止发布后产物被悄悄改动,不是 bug。报错信息里已经给出 `republish` 命令行。
411
+
382
412
  ### Plan 结束行为规则
383
413
 
384
414
  - **禁止询问执行模式**:Subagent-Driven / Inline Execution 属于 /harness-run 阶段
@@ -3,7 +3,7 @@ name: harness-pull
3
3
  description: 从 Hunter Platform 下拉配置/规则/架构/指令(及显式来源分支的分支文件恢复)。仅当用户显式调用
4
4
  /harness-pull 或明确说'从平台拉取/恢复'时使用;不得自动触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-pull — 从 Hunter Platform 下拉与恢复
8
8
 
9
9
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-push
3
3
  description: 上传本地配置/规则/架构/指令(及显式归档)到 Hunter Platform。仅当用户显式调用 /harness-push
4
4
  或明确说'上传到平台'时使用;不得因存在本地修改就自动触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-push — 上传到 Hunter Platform
8
8
 
9
9
  ## Purpose
@@ -4,7 +4,7 @@ description: 6维度代码审查(架构/安全/规范/兼容/测试/性能)
4
4
  .harness/context-index.json)和测试场景表,在隔离上下文运行。仅当用户显式调用 /harness-review 时使用;不得在
5
5
  test 结束后自动接续执行。
6
6
  ---
7
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
7
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
8
8
  # harness-review — 代码审查
9
9
 
10
10
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-run
3
3
  description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。仅当用户显式调用
4
4
  /harness-run 时使用;不得因用户提到编码/实现就自动触发,也不得被其他阶段 skill 自动接续。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-run — 需求编码
8
8
 
9
9
  ## Purpose
@@ -68,6 +68,8 @@ description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编
68
68
 
69
69
  **阶段归属规则**:只用 `ownerPhase=run` 的任务和场景判定编码阶段结果。`ownerPhase=test` 的任务或场景按计划留给测试阶段属于正常移交,必须记录为“待测试阶段执行”,不得将编码阶段降级为 WARN;只有 run 自身负责的工作未完成、验证降级或证据异常时才使用 WARN。
70
70
 
71
+ > 关门脚本与本规则一致:`harness_gate.py close --phase run` 的 C9 场景覆盖只要求 `ownerPhase` 为 `plan`/`run` 的必需场景有通过 receipt,`ownerPhase=test` 的场景出现在返回值的 `deferred` 里,不阻断 run。若 run 关门报 `REQUIRED_SCENARIO_NOT_EXECUTED` 且缺的是接口/端到端场景,那是 `meta/scenario-manifest.json` 里 `ownerPhase` 标错了(或老清单没声明),应当修清单——**不要**为了过门在 run 阶段起服务补跑本属 test 的验证。
72
+
71
73
  **Fixback**:入口只用 `launch-review`,后续问题处理通过 `resolve-issue/close` 驱动,不得把修复说明当成新的普通 Run。只读取返回的受影响问题和文件;验证仅失效与 `changedFiles` 相交的目标,其他 Test/Review 证据继续复用。RED 优先;`manual`、`workflow` 或未选用的建议不进入代码批次,使用中文记录处理结论。
72
74
 
73
75
  **执行器边界**:优先使用项目 build profile 和已有测试入口。禁止为了绕过 ESM、路径或参数问题临时生成 `.js`、`require` 脚本;需要文件式 runner 时使用项目已有入口,确需新增时遵循项目模块类型(例如 ESM 使用 `.mjs`)。runner 包装说明写入 `runnerCommand` 元数据,不得拼进账本的规范 `command`。
@@ -3,7 +3,7 @@ name: harness-submit
3
3
  description: 最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff 合并回主分支。仅当用户显式调用
4
4
  /harness-submit(或 /harness-merge 重入合并段)时使用;用户口头说'提交/commit/push'时必须先确认,不得自动触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-submit — 最终提交(含 worktree 合并)
8
8
 
9
9
  ## Purpose
@@ -4,7 +4,7 @@ description: Use when the user asks to synchronize, refresh, or validate Harness
4
4
  metadata, adapters, remote knowledge ownership, instruction entrypoints,
5
5
  config origins, or CodeGraph status.
6
6
  ---
7
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
7
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
8
8
  # harness-sync
9
9
 
10
10
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-test
3
3
  description: 测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。仅当用户显式调用 /harness-test
4
4
  时使用;不得在 run 结束后自动接续执行。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f91fcbcac46e102; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=86f5a6b27795a615; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-test — 测试执行
8
8
 
9
9
  ## Purpose
@@ -195,6 +195,8 @@ python <skills-root>/scripts/harness_test_guard.py record --project . --change-d
195
195
 
196
196
  先读 `.harness/changes/<change-name>/runtime/credential-cache.json`(认证凭证缓存,按项目认证机制;token/SSO 为常见实现),本地轻量接口验证通过则复用,失败才走远程认证。接口测试执行器用 request context / 原生 HTTP 客户端直连本地 baseURL,**不得依赖浏览器当前页面 origin**。同一次流程内凭证刷新计数 > 1 → 🟡 WARN。**不得在报告/日志/对话总结中输出明文凭证**。详见 `reference.md`「认证凭证缓存与复用」。
197
197
 
198
+ > ⛔ **验证码 = 硬停,不是待解的技术问题。** 登录响应出现 `验证码` / `captcha` / `blockPuzzle` / `slider` / `geetest` 等特征时,**禁止**编写或运行任何求解代码(图像匹配、OCR、打码平台、反编译服务端找容差均在禁止之列)。立即记 `apiTest=BLOCKED`,请用户手工把凭证写入 `runtime/credential-cache.json` 或临时关闭测试环境验证码,并提示 `.harness/config/harness-test-config.md` 的认证方式已过期。详见 `pitfalls.md` 规则 31。
199
+
198
200
  ### 九、测试报告状态规则
199
201
 
200
202
  整体 ✅OK / 🟡WARN / ❌FAIL 三态;API 维度使用 `OK` / `PARTIAL` / `BLOCKED` / `NOT_RUN` / `FAIL` 五态。**不得把「5 PASS + 9 BLOCKED + 1 FAIL」写成 `apiTest=NOT_RUN`**,正确为 `apiTest=PARTIAL`。P0 场景 BLOCKED 不得仍 OK。详见 `reference.md`「结果分级规则」。
@@ -2,16 +2,16 @@
2
2
  description: harness-test 的踩坑规则(来自真实对话日志)。执行测试前必须通读,遇到测试失败时重新参考。
3
3
  ---
4
4
 
5
- # 避坑规则(30 条)
5
+ # 避坑规则(32 条)
6
6
 
7
- > 以下规则来自真实对话日志(2026-06-12 ~ 06-24),每个都有明确的现象和根因。
7
+ > 以下规则来自真实对话日志(2026-06-12 ~ 08-17),每个都有明确的现象和根因。
8
8
  > 执行 `harness-test` 时必须通读,避免重蹈覆辙。
9
9
 
10
10
  | # | 规则 | 现象 | 根因 | 解法 |
11
11
  |:--:|------|------|------|------|
12
12
  | 1 | 凭证可展示但注意安全 | 对话日志记录了凭证 | Bearer 凭证被粘贴到聊天中 | 凭证可以在对话中展示用于调试,但不要写入持久化文件(测试报告、执行日志等) |
13
13
  | 2 | 不用 curl 发中文 | `Invalid UTF-8 start byte 0xb2` | Windows curl 默认 GBK 编码 | 用 PowerShell Invoke-WebRequest 或 Playwright fetch |
14
- | 3 | 必须带租户标识 | 所有接口 500 | 租户上下文为空 | 请求头加租户标识(见 build-profile.httpHeaders |
14
+ | 3 | 必须带租户标识 | 所有接口 500,或 200 但 `data:null` | 租户上下文为空,或请求头租户值与测试数据行的租户不一致 | 请求头加租户标识(见 build-profile.httpHeaders);**取值必须等于测试数据实际所属租户**,先查数据行的租户列再填,不要默认写 0 |
15
15
  | 4 | 认证降级 | 接口 401 "账号未登录" | 本地无认证服务 | 应用配置放行(如 permit-all-urls)或权限降级 |
16
16
  | 5 | 错误码双格式兼容 | 测试断言失败 | 预期 `1_003_002_009`,实际 `1003002009` | 自动兼容下划线和数字两种格式 |
17
17
  | 6 | 编译产物确认 | 修改代码后测试结果不变 | IDE 热重载未触发 | 测试前编译(如 `mvn compile -o`) |
@@ -39,6 +39,8 @@ description: harness-test 的踩坑规则(来自真实对话日志)。执行
39
39
  | 28 | **已有服务未先决策就跑业务接口** | 旧服务不含新代码,接口 500 后才发现版本不匹配 | 检测到已有应用服务(端口被占)后,未先展示服务决策门就跑业务接口 | 先展示服务决策门,询问复用/重启/跳过/停止;询问前只允许 health/meta 检查 |
40
40
  | 29 | **启动命令反复试相对配置叠加 / 中文路径** | 相对路径或中文路径导致应用读不到运行时配置叠加,启动失败 | 默认只用 `C:/temp/harness-test-overlay/<change>/application-harness-test.yml` ASCII 绝对路径,并固化已知良好测试配置 |
41
41
  | 30 | **伪 diffHash 导致错误复用** | `3files-84plus-5minus` 不能证明代码未变 | 用 `git diff --binary` 生成 patch 并计算 SHA-256,ledger 只认 `sha256:<hash>` |
42
+ | 31 | **尝试破解验证码 / 人机验证** | 认证服务返回"验证码不能为空",AI 写图像识别脚本反复试坐标(实测 28 次 / 4 种算法全失败) | 测试配置里的账号密码登录流程被加了滑块/图形验证码,配置未同步 | **硬停**。禁止编写或运行任何验证码求解代码;立即标 `apiTest=BLOCKED`,请用户手工提供凭证写入 `runtime/credential-cache.json`,或临时关闭测试环境验证码 |
43
+ | 32 | **预存环境缺陷被当成本次变更的阻塞** | `unitTestFull` 144 个测试挂 1 个,根因是别人提交引入的缺省配置缺失 | 全量验证把预存缺陷和本次 diff 混在一起判定 | 先用 `git log -S` + diff 触点证明与本次变更无关,再按「预存缺陷」上报并请用户决策(修根因 / 记 🟡WARN / 停止),**不得**擅自改 tracked 配置或排除测试 |
42
44
 
43
45
 
44
46
  ## 详细说明
@@ -257,6 +259,35 @@ $cred = $resp.data.accessToken
257
259
  **后果**:代码已变却复用旧测试结果,掩盖回归问题
258
260
  **正确做法**:用 `git diff --binary` 生成 patch 并计算 SHA-256,ledger 只认 `sha256:<hash>` 格式。diffHash 不一致时必须重跑相关测试。
259
261
 
262
+ ### 规则 31:尝试破解验证码 / 人机验证
263
+ **严重度**:❌FAIL(硬性禁止)
264
+ **场景**:本地服务已起来,但业务接口需要真实登录上下文;认证服务登录接口返回 `验证码不能为空` / 返回滑块拼图(如 AJ-Captcha `blockPuzzle`)/ 图形验证码 / 短信验证码
265
+ **后果**:真实日志里 AI 连续写了 4 版求解算法(边缘求和 → NCC 模板匹配 → 二维边缘匹配 → 一维归一化互相关)、反编译服务端 jar 找容差、共 28 次尝试全部失败,烧掉整段会话,最终仍然 BLOCKED
266
+ **正确做法**:**验证码是安全控制,不是待调试的技术问题。** 一旦识别到验证码/人机验证:
267
+
268
+ 1. **立即停止**,不得编写、下载或运行任何验证码求解代码(图像匹配、OCR、打码平台、反编译校验逻辑找容差都在禁止之列)
269
+ 2. 该维度记 `apiTest=BLOCKED`,受影响场景标 🟡 BLOCKED,**不得**伪造 receipt 或把未执行写成通过
270
+ 3. 向用户输出原文,并给出三条可选出路:
271
+ - 用户手工登录后把凭证写入 `.harness/changes/<change-name>/runtime/credential-cache.json`(推荐,执行器本来就先读 cache)
272
+ - 用户在测试环境临时关闭验证码开关,AI 重跑
273
+ - 本轮跳过接口验证,按 BLOCKED 归档,留待环境就绪
274
+ 4. 顺手提示更新 `.harness/config/harness-test-config.md`——认证方式已变更,配置已过期
275
+
276
+ > 判据:只要登录响应里出现 `验证码` / `captcha` / `blockPuzzle` / `slider` / `geetest` / `secretKey + originalImageBase64` 任一特征,即触发本规则。
277
+
278
+ ### 规则 32:预存环境缺陷被当成本次变更的阻塞
279
+ **严重度**:🟡WARN
280
+ **场景**:模块全量验证(如 `unitTestFull`)失败,但失败点在本次 diff 之外——例如别人提交的类新增了无默认值的配置占位符,导致某个全上下文测试无法加载
281
+ **后果**:要么误判成自己的回归浪费时间排查,要么擅自改 tracked 配置 / 排除测试把问题掩盖掉,污染 diff
282
+ **正确做法**:
283
+
284
+ 1. **先证明无关**:`git log -S '<符号>' --oneline` 定位引入提交 + `git diff` 证明本次触点不涉及该文件/目录,两条证据都要有
285
+ 2. **再上报**:按「预存缺陷」向用户说明——受影响模块、失败文件、引入提交、不阻断依据
286
+ 3. **由用户决策**:修根因(会扩大 diff,需明确点头)/ 记 🟡WARN 继续 / 停止本轮
287
+ 4. **禁止自作主张**:不得改 tracked 配置文件、不得 `-Dtest=` 排除失败测试、不得偏离 build-profile 的规范命令——这些都会让证据与 ledger 命令不一致
288
+
289
+ > 用户批准修复后,该修复文件即进入本次 diff:必须在提交前复核它仍在工作区(真实日志里用户中途手工还原了该修复,AI 直到 staging 才发现)。
290
+
260
291
  > diffHash/ledger 复用规则遵循 `../protocols/ledger-protocol.md`;结果证据要求遵循 `../protocols/evidence-based-reporting-protocol.md`。
261
292
 
262
293
  ## BOM-safe JSON / identifier sanitizer / 凭据扫描 (runtime-helpers.mjs)
@@ -219,10 +219,30 @@ def _same_repository(project: Path, candidate: Path) -> bool:
219
219
  return candidate.is_relative_to(project)
220
220
 
221
221
 
222
+ def _contract_error_code(exc: BaseException) -> str:
223
+ """Keep PROJECT_ROOT_INVALID distinct from a genuinely missing change."""
224
+ if str(exc).startswith("PROJECT_ROOT_INVALID"):
225
+ return "PROJECT_ROOT_INVALID"
226
+ return "CHANGE_NOT_FOUND"
227
+
228
+
222
229
  def _contract(project: Path, change: str) -> tuple[Path, dict[str, Any], Path]:
223
230
  root = project.resolve()
224
- contract_root = (root / ".harness" / "changes" / change).resolve()
231
+ # A bare project *name* (--project udp) resolves to <cwd>/udp and then
232
+ # reports CHANGE_NOT_FOUND, which sends the caller hunting for the change
233
+ # instead of fixing the argument. Separate the two failures.
234
+ if not root.is_dir():
235
+ raise ValueError(
236
+ f"PROJECT_ROOT_INVALID: {root} is not a directory — --project takes "
237
+ "a path to the project root (use '.'), not the project name"
238
+ )
225
239
  changes_root = (root / ".harness" / "changes").resolve()
240
+ if not changes_root.is_dir():
241
+ raise ValueError(
242
+ f"PROJECT_ROOT_INVALID: {changes_root} does not exist — "
243
+ f"{root} is not a harness project root"
244
+ )
245
+ contract_root = (root / ".harness" / "changes" / change).resolve()
226
246
  if not contract_root.is_relative_to(changes_root) or not contract_root.is_dir():
227
247
  raise ValueError(f"CHANGE_NOT_FOUND: {change}")
228
248
  context_path = contract_root / "meta" / "change-context.json"
@@ -296,7 +316,7 @@ def configure_phase_plan(
296
316
  try:
297
317
  contract_root, _contract_data, _state_root = _contract(project, change)
298
318
  except (OSError, ValueError, json.JSONDecodeError) as exc:
299
- return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
319
+ return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
300
320
  normalized = [str(item).strip() for item in phases if str(item).strip()]
301
321
  if (
302
322
  not normalized
@@ -802,7 +822,7 @@ def close_transition(
802
822
  try:
803
823
  contract_root, _contract_data, state_root = _contract(project, change)
804
824
  except (OSError, ValueError, json.JSONDecodeError) as exc:
805
- return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
825
+ return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
806
826
  allowed_next = _allowed_next_phases(contract_root, from_phase)
807
827
  if to_phase not in allowed_next:
808
828
  planned_phases, source = _phase_plan(contract_root)
@@ -962,7 +982,7 @@ def _begin_transition_unlocked(
962
982
  try:
963
983
  contract_root, _contract_data, state_root = _contract(project, change)
964
984
  except (OSError, ValueError, json.JSONDecodeError) as exc:
965
- return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
985
+ return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
966
986
  paths = _paths(state_root)
967
987
  try:
968
988
  execution_root = _execution_root(project, contract_root, state_root)
@@ -1074,7 +1094,7 @@ def begin_transition(
1074
1094
  try:
1075
1095
  _contract_root, _contract_data, state_root = _contract(project, change)
1076
1096
  except (OSError, ValueError, json.JSONDecodeError) as exc:
1077
- return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
1097
+ return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
1078
1098
  paths = _paths(state_root)
1079
1099
  with _exclusive_state_lock(paths["runtime"] / "branch-selection.lock"):
1080
1100
  return _begin_transition_unlocked(
@@ -1100,7 +1120,7 @@ def cancel_prepared_context(
1100
1120
  try:
1101
1121
  _contract_root, _contract_data, state_root = _contract(project, change)
1102
1122
  except (OSError, ValueError, json.JSONDecodeError) as exc:
1103
- return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
1123
+ return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
1104
1124
  paths = _paths(state_root)
1105
1125
  removed: list[str] = []
1106
1126
  receipt_hash: str | None = None
@@ -1186,7 +1206,7 @@ def context_view(project: Path, change: str) -> dict[str, Any]:
1186
1206
  try:
1187
1207
  contract_root, contract, state_root = _contract(project, change)
1188
1208
  except (OSError, ValueError, json.JSONDecodeError) as exc:
1189
- return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
1209
+ return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
1190
1210
  paths = _paths(state_root)
1191
1211
  transitions = _read_ndjson(paths["transitions"])
1192
1212
  begins = _read_ndjson(paths["begins"])
@@ -704,7 +704,11 @@ def append_event(
704
704
  return {
705
705
  "ok": False,
706
706
  "code": "PHASE_ALREADY_CLOSED",
707
- "message": "PHASE_ALREADY_CLOSED: refusing a second phase.end for the same attempt",
707
+ "message": (
708
+ "PHASE_ALREADY_CLOSED: refusing a second phase.end for the "
709
+ "same attempt. A retry needs a fresh run id AND the next "
710
+ "attempt number (attempts are global per phase, not per run id)."
711
+ ),
708
712
  "event": event,
709
713
  "autoSealed": result.get("autoSealed") or [],
710
714
  }
@@ -1931,7 +1935,9 @@ def cmd_append(args: argparse.Namespace) -> int:
1931
1935
  ):
1932
1936
  attempt_error = (
1933
1937
  "EVENT_ATTEMPT_CONFLICT: run_id is already bound to "
1934
- f"attempt {expected_attempt}, received {explicit_attempt}"
1938
+ f"attempt {expected_attempt}, received "
1939
+ f"{explicit_attempt}. One run id maps to exactly one "
1940
+ "attempt — allocate a fresh run id for the next attempt."
1935
1941
  )
1936
1942
  else:
1937
1943
  event["attempt"] = expected_attempt
@@ -1972,7 +1978,10 @@ def cmd_append(args: argparse.Namespace) -> int:
1972
1978
  return emit_error(f"append failed: {exc}", as_json=as_json)
1973
1979
  if phase_closed:
1974
1980
  return emit_error(
1975
- "PHASE_ALREADY_CLOSED: refusing a second phase.end for the same attempt",
1981
+ "PHASE_ALREADY_CLOSED: refusing a second phase.end for the same "
1982
+ "attempt. A retry needs a fresh run id AND the next attempt number "
1983
+ "(attempts are global per phase, not per run id). For a published "
1984
+ "plan use harness_plan_finalize.py republish, which allocates both.",
1976
1985
  as_json=as_json,
1977
1986
  error_code="PHASE_ALREADY_CLOSED",
1978
1987
  )