session-orchestrator 3.22.0 → 3.24.0

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 (316) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -122,6 +122,7 @@
122
122
  import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
123
123
  import { join, isAbsolute, relative } from 'node:path';
124
124
  import { execFileSync } from 'node:child_process';
125
+ import { isGitToplevel } from './repo-files.mjs';
125
126
 
126
127
  // ---------------------------------------------------------------------------
127
128
  // argv
@@ -184,9 +185,32 @@ const CARVE_OUT_MARKER = '// integrity-anchor:';
184
185
  const MAGIC_COMMENT = '// @test-value-bans-allowed';
185
186
  const MAGIC_COMMENT_SCAN_LINES = 5;
186
187
 
187
- /** B2 heuristic: how many toContain/toMatch calls make a .md-reading file suspect. */
188
+ /** B2 heuristic: how many toContain/toMatch calls make a .md read suspect. */
188
189
  const PROSE_ASSERT_THRESHOLD = 3;
189
190
 
191
+ /** A line that reads a `.md` file — the B2 trigger. */
192
+ const MD_READ_CALL = /readFileSync\(/;
193
+ const MD_READ_TARGET = /\.md\b/;
194
+
195
+ /** A prose assertion — the B2 population. */
196
+ const PROSE_ASSERT = /\.(?:toContain|toMatch)\(/g;
197
+
198
+ /**
199
+ * Count prose assertions in `lines[from, to)`.
200
+ * @param {string[]} lines
201
+ * @param {number} from inclusive
202
+ * @param {number} to exclusive
203
+ * @returns {number}
204
+ */
205
+ function countProseAsserts(lines, from, to) {
206
+ let n = 0;
207
+ for (let i = from; i < to; i++) {
208
+ const m = lines[i].match(PROSE_ASSERT);
209
+ if (m) n += m.length;
210
+ }
211
+ return n;
212
+ }
213
+
190
214
  const B1_PATTERNS = [
191
215
  {
192
216
  name: 'toHaveLength',
@@ -343,6 +367,62 @@ function trackedTestFiles() {
343
367
  .filter((rel) => /^tests\/.*\.test\.mjs$/.test(rel));
344
368
  }
345
369
 
370
+ /**
371
+ * Line ranges this commit actually ADDS or CHANGES, per repo-relative path,
372
+ * from `git diff --cached -U0`.
373
+ *
374
+ * Why (#1148, HR-101): `--stdin` scopes the scan to staged FILES, not staged
375
+ * LINES. Touching one line of a 1400-line test file therefore surfaced every
376
+ * pre-existing finding in it — a warning that fires on work the committer did
377
+ * not do, which teaches the committer to ignore the warning. The repo-wide
378
+ * census is not lost by this: the CI job `test-value-bans` runs the scanner
379
+ * unscoped over `$CI_PROJECT_DIR` (.gitlab-ci.yml), so an old prose pin is
380
+ * still reported there — just not at the moment someone edits a neighbouring
381
+ * line.
382
+ *
383
+ * Returns `null` when there is no index to consult (not a git top level, git
384
+ * absent, command failed). A null gate is INERT, never empty: failing open is
385
+ * the only safe direction for an advisory that must not invent silence.
386
+ *
387
+ * @returns {Map<string, Array<[number, number]>> | null}
388
+ */
389
+ function stagedLineRanges() {
390
+ if (!isGitToplevel(repoRoot)) return null;
391
+ let out;
392
+ try {
393
+ out = execFileSync('git', ['diff', '--cached', '-U0', '--no-color'], {
394
+ cwd: repoRoot,
395
+ encoding: 'utf8',
396
+ stdio: ['ignore', 'pipe', 'ignore'],
397
+ maxBuffer: 64 * 1024 * 1024,
398
+ });
399
+ } catch {
400
+ return null;
401
+ }
402
+ /** @type {Map<string, Array<[number, number]>>} */
403
+ const ranges = new Map();
404
+ /** @type {string | null} */
405
+ let current = null;
406
+ for (const line of out.split('\n')) {
407
+ if (line.startsWith('+++ ')) {
408
+ // The POST-image path: a rename stages under its new name, and a
409
+ // deletion stages as /dev/null (no post-image lines to judge).
410
+ const target = line.slice(4).trim();
411
+ current = target === '/dev/null' ? null : target.replace(/^b\//, '').replace(/\\/g, '/');
412
+ if (current && !ranges.has(current)) ranges.set(current, []);
413
+ continue;
414
+ }
415
+ if (!current || !line.startsWith('@@')) continue;
416
+ const m = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
417
+ if (!m) continue;
418
+ const start = Number(m[1]);
419
+ const count = m[2] === undefined ? 1 : Number(m[2]);
420
+ if (count === 0) continue; // pure deletion — no post-image line exists
421
+ ranges.get(current).push([start, start + count - 1]);
422
+ }
423
+ return ranges;
424
+ }
425
+
346
426
  /** Newline-separated paths from stdin (staged-only mode). */
347
427
  function stdinFiles() {
348
428
  let raw;
@@ -641,18 +721,44 @@ function scanContent(relPath, content, denyHooks = new Set()) {
641
721
  });
642
722
 
643
723
  // --- B2: suspected prose pin -------------------------------------------
644
- const mdReadLine = lines.findIndex((l) => /readFileSync\(/.test(l) && /\.md\b/.test(l));
645
- if (mdReadLine !== -1) {
646
- const proseAsserts = (content.match(/\.(?:toContain|toMatch)\(/g) ?? []).length;
647
- if (proseAsserts >= PROSE_ASSERT_THRESHOLD) {
724
+ // ONE finding per .md read, counted over the population that read can reach
725
+ // (#1148). v1 reported the line of the FIRST .md read beside a WHOLE-FILE
726
+ // assert count two different populations with no causal relation, so the
727
+ // reported location was never the location of anything counted.
728
+ // Live proof: tests/unit/vault-mirror.test.mjs was filed at :1077 on
729
+ // 2026-08-23 and reported :1415 three days later with the same 52 asserts.
730
+ // Nothing happened at line 1077; the first .md read simply moved 338 lines.
731
+ //
732
+ // A read's population = its enclosing it()/test() block when it has one,
733
+ // else the whole file — a module-scope read IS visible file-wide, so there
734
+ // the file is the causally correct scope, not a fallback.
735
+ // Ceiling (BV-004): testBlocks() segments it()/test() only, so a read at
736
+ // describe() scope also counts file-wide. That over-includes in the same
737
+ // direction v1 always did; revisit if a describe-scoped .md read is ever
738
+ // reported with a count no single test could produce.
739
+ {
740
+ const blocks = testBlocks(lines);
741
+ /** Segments already reported — a block holding two .md reads reports once. */
742
+ const reportedSegments = new Set();
743
+ lines.forEach((line, idx) => {
744
+ if (!MD_READ_CALL.test(line) || !MD_READ_TARGET.test(line)) return;
745
+ const block = blocks.find((b) => idx >= b.start && idx < b.end);
746
+ const key = block ? `block:${block.start}` : 'file';
747
+ if (reportedSegments.has(key)) return;
748
+ const [from, to] = block ? [block.start, block.end] : [0, lines.length];
749
+ const proseAsserts = countProseAsserts(lines, from, to);
750
+ if (proseAsserts < PROSE_ASSERT_THRESHOLD) return;
751
+ reportedSegments.add(key);
648
752
  findings.push({
649
753
  file: relPath,
650
- line: mdReadLine + 1,
754
+ line: idx + 1,
651
755
  ban: 'B2-prose-pin-suspected',
652
- match: `readFileSync on .md + ${proseAsserts} toContain/toMatch asserts`,
756
+ match: `readFileSync on .md + ${proseAsserts} toContain/toMatch asserts in ${
757
+ block ? 'this test' : 'this file'
758
+ }`,
653
759
  hint: B2_HINT,
654
760
  });
655
- }
761
+ });
656
762
  }
657
763
 
658
764
  // --- B3: bare exit-code allow assertion on a deny-capable hook ----------
@@ -748,23 +854,59 @@ for (const rel of candidates) {
748
854
  findings.push(...scanContent(rel, content, denyHooks));
749
855
  }
750
856
 
857
+ // Under --stdin (the pre-commit path) judge only the lines this commit
858
+ // touches; without it (the CI path) keep the repo-wide census. See
859
+ // stagedLineRanges() for why the two paths differ.
860
+ const stagedScope = flags.has('--stdin') ? stagedLineRanges() : null;
861
+ let suppressed = 0;
862
+ const reported = stagedScope
863
+ ? findings.filter((f) => {
864
+ const ranges = stagedScope.get(f.file);
865
+ const inHunk = Boolean(ranges) && ranges.some(([a, b]) => f.line >= a && f.line <= b);
866
+ if (!inHunk) suppressed++;
867
+ return inHunk;
868
+ })
869
+ : findings;
870
+
751
871
  const counts = {
752
- 'B1-exact-count': findings.filter((f) => f.ban === 'B1-exact-count').length,
753
- 'B2-prose-pin-suspected': findings.filter((f) => f.ban === 'B2-prose-pin-suspected').length,
754
- 'B3-bare-hook-exit-code': findings.filter((f) => f.ban === 'B3-bare-hook-exit-code').length,
872
+ 'B1-exact-count': reported.filter((f) => f.ban === 'B1-exact-count').length,
873
+ 'B2-prose-pin-suspected': reported.filter((f) => f.ban === 'B2-prose-pin-suspected').length,
874
+ 'B3-bare-hook-exit-code': reported.filter((f) => f.ban === 'B3-bare-hook-exit-code').length,
755
875
  'B4-hook-decision-contract-copy': findings.filter(
756
876
  (f) => f.ban === 'B4-hook-decision-contract-copy',
757
877
  ).length,
758
- 'B5-date-time-bomb': findings.filter((f) => f.ban === 'B5-date-time-bomb').length,
878
+ 'B5-date-time-bomb': reported.filter((f) => f.ban === 'B5-date-time-bomb').length,
759
879
  };
760
880
 
761
881
  if (jsonMode) {
762
- console.log(JSON.stringify({ advisory: true, scanned, counts, findings }, null, 2));
763
- } else if (findings.length === 0) {
764
- if (!quiet) console.log(`check-test-value-bans: 0 findings across ${scanned} test file(s) — advisory`);
882
+ console.log(
883
+ JSON.stringify(
884
+ {
885
+ advisory: true,
886
+ scanned,
887
+ counts,
888
+ stagedScope: { applied: stagedScope !== null, suppressed },
889
+ findings: reported,
890
+ },
891
+ null,
892
+ 2,
893
+ ),
894
+ );
895
+ } else if (reported.length === 0) {
896
+ if (!quiet) {
897
+ console.log(`check-test-value-bans: 0 findings across ${scanned} test file(s) — advisory`);
898
+ // Never suppress silently: an empty result that had findings behind it is
899
+ // a different fact from an empty result that did not (HR-106).
900
+ if (stagedScope !== null && suppressed > 0) {
901
+ console.log(
902
+ ` (${suppressed} pre-existing finding(s) outside this commit's staged hunks not shown — ` +
903
+ 'the CI job test-value-bans reports the repo-wide census)',
904
+ );
905
+ }
906
+ }
765
907
  } else {
766
- console.log(`check-test-value-bans: ${findings.length} advisory finding(s) across ${scanned} test file(s)`);
767
- for (const f of findings) {
908
+ console.log(`check-test-value-bans: ${reported.length} advisory finding(s) across ${scanned} test file(s)`);
909
+ for (const f of reported) {
768
910
  console.log(` ${f.ban} ${f.file}:${f.line} ${f.match}`);
769
911
  }
770
912
  if (counts['B1-exact-count'] > 0) console.log(` B1 hint: ${B1_HINT}`);
@@ -772,6 +914,12 @@ if (jsonMode) {
772
914
  if (counts['B3-bare-hook-exit-code'] > 0) console.log(` B3 hint: ${B3_HINT}`);
773
915
  if (counts['B4-hook-decision-contract-copy'] > 0) console.log(` B4 hint: ${B4_HINT}`);
774
916
  if (counts['B5-date-time-bomb'] > 0) console.log(` B5 hint: ${B5_HINT}`);
917
+ if (stagedScope !== null && suppressed > 0) {
918
+ console.log(
919
+ ` (${suppressed} pre-existing finding(s) outside this commit's staged hunks not shown — ` +
920
+ 'the CI job test-value-bans reports the repo-wide census)',
921
+ );
922
+ }
775
923
  console.log(' (advisory — this check never blocks; see .claude/rules/testing.md § Lint-Enforceable Test Bans)');
776
924
  }
777
925
 
@@ -695,9 +695,19 @@ export function resolveUntrackedOracle(repoRoot, candidates) {
695
695
  // directory candidates (`.git`, `.claude`, `docs/prd`, `skills/bootstrap`, …)
696
696
  // were all false. A directory can therefore only be condemned by
697
697
  // `check-ignore`, which does judge directories correctly.
698
+ //
699
+ // `.git` ALSO needs an explicit exemption from THIS FILES-ONLY branch, not
700
+ // just from the directory-only reasoning above. In a normal checkout `.git`
701
+ // is a directory and the `isFile()` guard already excludes it — but in a
702
+ // LINKED WORKTREE (`git worktree add`) `.git` is a FILE containing
703
+ // `gitdir: <path>`, which makes `existsSync() && isFile()` true and
704
+ // re-opens exactly the false-positive the comment above says is closed.
705
+ // `.git` is present in every clone/worktree by construction regardless of
706
+ // which shape it takes, so it is never a "never git-add-ed" candidate.
698
707
  for (const c of all) {
699
708
  if (untracked.has(c)) continue;
700
709
  if (tracked.has(c)) continue;
710
+ if (c === '.git') continue;
701
711
  try {
702
712
  const abs = path.join(repoRoot, c);
703
713
  if (existsSync(abs) && statSync(abs).isFile()) untracked.add(c);