session-orchestrator 3.22.0 → 3.23.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 (268) 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/repo-audit/SKILL.md +13 -0
  74. package/.cursor/skills/session-end/SKILL.md +13 -0
  75. package/.cursor/skills/session-plan/SKILL.md +13 -0
  76. package/.cursor/skills/session-start/SKILL.md +13 -0
  77. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  78. package/.cursor/skills/spinout/SKILL.md +12 -0
  79. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  80. package/.cursor/skills/test-runner/SKILL.md +13 -0
  81. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  82. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  83. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  84. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  85. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  86. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  87. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  88. package/.mcp.json +4 -1
  89. package/CHANGELOG.md +168 -0
  90. package/README.md +18 -15
  91. package/agents/AGENTS.md +23 -4
  92. package/agents/code-implementer.md +2 -1
  93. package/agents/db-specialist.md +2 -1
  94. package/agents/docs-writer.md +3 -1
  95. package/agents/eval-judge.md +1 -1
  96. package/agents/session-reviewer.md +7 -1
  97. package/agents/test-writer.md +2 -1
  98. package/agents/ui-developer.md +2 -1
  99. package/commands/bootstrap.md +2 -2
  100. package/commands/close.md +3 -1
  101. package/commands/go.md +1 -1
  102. package/commands/journey-audit.md +43 -0
  103. package/docs/USER-GUIDE.md +2 -2
  104. package/docs/ci-setup.md +14 -0
  105. package/docs/codex-setup.md +64 -0
  106. package/docs/components.md +6 -6
  107. package/docs/cursor-setup.md +26 -47
  108. package/docs/events-schema.md +76 -4
  109. package/docs/github-mirror-protection.md +197 -0
  110. package/docs/pi-setup.md +2 -0
  111. package/docs/rule-authoring.md +3 -1
  112. package/docs/scope-collision-guard.md +49 -2
  113. package/docs/session-config-reference.md +26 -4
  114. package/docs/session-config-template.md +4 -3
  115. package/docs/telemetry.md +22 -0
  116. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  117. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  118. package/hooks/enforce-scope.mjs +64 -0
  119. package/hooks/hooks-codex.json +1 -1
  120. package/hooks/hooks-cursor.json +201 -20
  121. package/hooks/hooks-pi.json +1 -1
  122. package/hooks/hooks.json +2 -2
  123. package/hooks/on-session-end.mjs +211 -10
  124. package/hooks/on-session-start.mjs +214 -11
  125. package/hooks/on-stop.mjs +48 -9
  126. package/hooks/post-subagent-discovery-validator.mjs +34 -3
  127. package/hooks/post-tool-batch-wave-signal.mjs +11 -2
  128. package/hooks/pre-bash-issue-budget.mjs +117 -4
  129. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  130. package/hooks/pre-bash-staging-fence.mjs +4 -0
  131. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  132. package/hooks/skill-invocation-telemetry.mjs +21 -10
  133. package/monitors/monitors.json +6 -0
  134. package/package.json +1 -1
  135. package/pi/prompts/journey-audit.md +12 -0
  136. package/rules/_index.md +9 -1
  137. package/rules/always-on/ask-via-tool.md +62 -0
  138. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  139. package/rules/always-on/build-value.md +47 -0
  140. package/rules/always-on/cross-session-messaging.md +59 -0
  141. package/rules/always-on/loop-and-monitor.md +221 -0
  142. package/rules/always-on/parallel-sessions.md +142 -12
  143. package/rules/always-on/receiving-review.md +108 -0
  144. package/rules/always-on/test-value.md +40 -0
  145. package/rules/always-on/verification-before-completion.md +77 -0
  146. package/scripts/archive-closed-prds.mjs +258 -18
  147. package/scripts/autopilot.mjs +5 -0
  148. package/scripts/backfill-evidence-digest.mjs +376 -0
  149. package/scripts/cursor-install.mjs +89 -48
  150. package/scripts/export-hw-learnings.mjs +143 -2
  151. package/scripts/express-path.mjs +299 -0
  152. package/scripts/generate-cursor-adapter.mjs +253 -0
  153. package/scripts/github-protection-audit.mjs +358 -0
  154. package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
  155. package/scripts/lib/build-live-signals.mjs +24 -5
  156. package/scripts/lib/ci-status-banner.mjs +158 -11
  157. package/scripts/lib/command-blocker.mjs +70 -0
  158. package/scripts/lib/config/reconcile.mjs +79 -4
  159. package/scripts/lib/config/section-extractor.mjs +235 -36
  160. package/scripts/lib/config-schema.mjs +9 -1
  161. package/scripts/lib/config.mjs +57 -6
  162. package/scripts/lib/convergence-monitor.mjs +13 -2
  163. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  164. package/scripts/lib/dispatcher/cli.mjs +2 -2
  165. package/scripts/lib/express-path.mjs +327 -0
  166. package/scripts/lib/file-lock.mjs +22 -4
  167. package/scripts/lib/gates/gate-full.mjs +81 -8
  168. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  169. package/scripts/lib/git-config-drift.mjs +134 -5
  170. package/scripts/lib/host-identity.mjs +247 -2
  171. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  172. package/scripts/lib/issue-budget.mjs +229 -30
  173. package/scripts/lib/learnings/io.mjs +55 -10
  174. package/scripts/lib/learnings/schema.mjs +95 -28
  175. package/scripts/lib/lock-reaper.mjs +7 -1
  176. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  177. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  178. package/scripts/lib/memory-banner.mjs +5 -2
  179. package/scripts/lib/memory-paths.mjs +15 -6
  180. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  181. package/scripts/lib/platform.mjs +72 -9
  182. package/scripts/lib/plugin-root.mjs +143 -19
  183. package/scripts/lib/project-hygiene.mjs +43 -3
  184. package/scripts/lib/quality-gate.mjs +271 -13
  185. package/scripts/lib/reconcile/emitter.mjs +87 -19
  186. package/scripts/lib/reconcile/engine.mjs +281 -13
  187. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  188. package/scripts/lib/reconcile/renderer.mjs +148 -3
  189. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  190. package/scripts/lib/reconcile/writer.mjs +415 -84
  191. package/scripts/lib/rule-loader.mjs +37 -2
  192. package/scripts/lib/rules-sync.mjs +51 -8
  193. package/scripts/lib/scope-gate.mjs +90 -0
  194. package/scripts/lib/session-close-backfill.mjs +369 -28
  195. package/scripts/lib/session-discovery.mjs +13 -3
  196. package/scripts/lib/session-end/phase-skip.mjs +37 -4
  197. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  198. package/scripts/lib/session-id.mjs +30 -14
  199. package/scripts/lib/session-identity/own-session.mjs +159 -0
  200. package/scripts/lib/session-lock.mjs +85 -30
  201. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  202. package/scripts/lib/session-schema/validator.mjs +40 -0
  203. package/scripts/lib/session-start-probes.mjs +608 -0
  204. package/scripts/lib/session-transition.mjs +277 -0
  205. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  206. package/scripts/lib/spiral-carryover.mjs +90 -9
  207. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  208. package/scripts/lib/state-md/mission-status.mjs +350 -52
  209. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  210. package/scripts/lib/state-md.mjs +12 -2
  211. package/scripts/lib/telemetry/sync.mjs +46 -8
  212. package/scripts/lib/validate/check-agents.mjs +66 -0
  213. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  214. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  215. package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
  216. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  217. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  218. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  219. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  220. package/scripts/lib/validate/check-unwired-features.mjs +340 -32
  221. package/scripts/lib/validate/repo-files.mjs +275 -0
  222. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  223. package/scripts/lib/vault-mirror/process.mjs +99 -43
  224. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  225. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  226. package/scripts/lib/vault-status/board-writer.mjs +211 -10
  227. package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
  228. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  229. package/scripts/lib/wave-transcript-tail.mjs +869 -0
  230. package/scripts/materialize-wave-scope.mjs +209 -12
  231. package/scripts/mcp-server.sh +11 -2
  232. package/scripts/parse-config.mjs +65 -0
  233. package/scripts/token-audit.sh +9 -2
  234. package/scripts/validate-plugin.mjs +3 -0
  235. package/scripts/validate-wave-scope.mjs +67 -0
  236. package/scripts/vault-mirror.mjs +203 -34
  237. package/skills/_shared/monitor-patterns.md +31 -5
  238. package/skills/_shared/parallel-aware-auq.md +1 -1
  239. package/skills/_shared/parallel-aware-preamble.md +4 -2
  240. package/skills/_shared/platform-tools.md +11 -5
  241. package/skills/_shared/state-ownership.md +29 -2
  242. package/skills/autopilot/SKILL.md +5 -1
  243. package/skills/bootstrap/SKILL.md +3 -3
  244. package/skills/bootstrap/_shared-template.md +18 -10
  245. package/skills/bootstrap/deep-template.md +10 -6
  246. package/skills/bootstrap/fast-template.md +15 -8
  247. package/skills/bootstrap/standard-template.md +10 -6
  248. package/skills/claude-md-drift-check/checker.mjs +39 -11
  249. package/skills/dispatcher/SKILL.md +1 -1
  250. package/skills/journey-audit/SKILL.md +269 -0
  251. package/skills/peekaboo-driver/SKILL.md +15 -3
  252. package/skills/persona-panel/SKILL.md +1 -1
  253. package/skills/reconcile/SKILL.md +41 -1
  254. package/skills/session-end/SKILL.md +17 -4
  255. package/skills/session-end/metrics-collection.md +7 -4
  256. package/skills/session-end/phase-3-6-tail.md +11 -3
  257. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  258. package/skills/session-plan/SKILL.md +6 -1
  259. package/skills/session-plan/wave-template.md +1 -0
  260. package/skills/session-start/SKILL.md +30 -16
  261. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  262. package/skills/session-start/phase-8-5-express-path.md +77 -12
  263. package/skills/vault-sync/validator.mjs +31 -0
  264. package/skills/wave-executor/SKILL.md +4 -2
  265. package/skills/wave-executor/circuit-breaker.md +34 -9
  266. package/skills/wave-executor/wave-loop.md +102 -19
  267. package/templates/_shared/journey-manifest.md +110 -0
  268. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -12,18 +12,12 @@
12
12
  import { readFileSync, existsSync, readdirSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
 
15
- // The pi projection is IMPORTED, not copied: pi-hook-bridge.mjs is the runtime
16
- // that actually rewrites `hook_event_name` for every Pi event, so it is the one
17
- // source of truth for which pi event becomes which Claude event (#953). A local
18
- // literal here was a second copy of the same 5 pairs — it could drift silently,
19
- // and the validator would then certify a projection the runtime does not use.
20
- // Deliberately NOT the same for `cursorEventMap` below: nothing at runtime
21
- // projects Cursor events (Cursor calls the handlers directly), so the
22
- // validator's copy is the only witness there. Even if a runtime source existed,
23
- // reading Check 3's expectation from the artefact it checks would make the
24
- // check self-certifying — the "undocumented pi-native event" test at
25
- // tests/scripts/check-hooks-symmetry.test.mjs would become unreachable. Keep
26
- // cursorEventMap validator-owned; do not "helpfully" collapse it too.
15
+ // The pi and cursor projections are IMPORTED, not copied: the runtime bridges
16
+ // rewrite `hook_event_name` for every native event, so they are the source of
17
+ // truth for which native event becomes which Claude event (#953). A local
18
+ // literal here was a second copy — it could drift silently, and the validator
19
+ // would then certify a projection the runtime does not use.
20
+ import { CURSOR_TO_CANONICAL_EVENT } from '../cursor-hook-bridge.mjs';
27
21
  import { PI_TO_CANONICAL_EVENT } from '../pi-hook-bridge.mjs';
28
22
 
29
23
  const PLUGIN_ROOT = process.argv[2];
@@ -40,7 +34,19 @@ const DOCUMENTED_ASYMMETRIES = {
40
34
  // Events from hooks.json/hooks-codex.json that are intentionally absent in hooks-cursor.json
41
35
  cursorMissingFromMain: ['SessionStart', 'SessionEnd', 'PostToolUse', 'PostToolUseFailure', 'PostToolBatch', 'Stop', 'SubagentStart', 'SubagentStop', 'CwdChanged', 'PreToolUse'],
42
36
  // Events unique to hooks-cursor.json (Cursor IDE-specific)
43
- cursorOnly: ['afterFileEdit', 'beforeShellExecution'],
37
+ cursorOnly: [
38
+ 'afterFileEdit',
39
+ 'afterShellExecution',
40
+ 'beforeShellExecution',
41
+ 'postToolUse',
42
+ 'postToolUseFailure',
43
+ 'preToolUse',
44
+ 'sessionEnd',
45
+ 'sessionStart',
46
+ 'stop',
47
+ 'subagentStart',
48
+ 'subagentStop',
49
+ ],
44
50
  // Claude/Codex events with no Pi-native v1 mapping yet.
45
51
  piMissingFromMain: ['PostToolUseFailure', 'PostToolBatch', 'SubagentStart', 'SubagentStop', 'CwdChanged'],
46
52
  // Pi-native extension events that map onto Claude/Codex hook events.
@@ -48,23 +54,10 @@ const DOCUMENTED_ASYMMETRIES = {
48
54
  // validator checks hooks-pi.json against is the SAME object pi-hook-bridge.mjs
49
55
  // applies to live payloads, so the two cannot drift apart.
50
56
  piEventMap: PI_TO_CANONICAL_EVENT,
51
- // Cursor-IDE-native events projected onto the logical Claude events they
52
- // correspond to. WITHOUT this projection Check 6 compared a FOREIGN event
53
- // namespace against hooks.json: not one key matched, the per-event loop
54
- // `continue`d on every iteration, and hooks-cursor.json reported
55
- // "handler sets match hooks.json (documented asymmetries: 0)" while missing
56
- // 20 of 22 handlers — a vacuum-true PASS. Check 6's projection guard now
57
- // fails closed PER declared event (#946), so dropping a single entry from
58
- // this map is caught too, not only the loss of every projection: the first
59
- // version tested `sharedEvents.length === 0`, under which removing just
60
- // `afterFileEdit` left the run green and merely moved cursor's documented-
61
- // asymmetry count from 12 to 8.
62
- // afterFileEdit fires AFTER the edit (hooks-cursor.json `note`,
63
- // docs/cursor-setup.md) → it projects onto PostToolUse, never PreToolUse.
64
- cursorEventMap: {
65
- beforeShellExecution: 'PreToolUse',
66
- afterFileEdit: 'PostToolUse',
67
- },
57
+ // Bound to the runtime constant (see the import comment): the projection this
58
+ // validator checks hooks-cursor.json against is the SAME object
59
+ // cursor-hook-bridge.mjs applies to live payloads (#919 closed).
60
+ cursorEventMap: CURSOR_TO_CANONICAL_EVENT,
68
61
  // Check 6 (#942): handlers wired on a Claude event but intentionally absent
69
62
  // from the SAME logical event on a counterpart manifest. Checks 1-3 compare
70
63
  // event KEYS and Check 4 handler EXISTENCE — a handler wired on only one
@@ -159,21 +152,9 @@ const DOCUMENTED_ASYMMETRIES = {
159
152
  'pre-bash-issue-budget.mjs', // #946
160
153
  ],
161
154
  },
162
- // Cursor is NOT wired (#919) and will not be: hooks-cursor.json is a
163
- // mapping REFERENCE, not live enforcement. Every handler expects Claude
164
- // Code payload shapes (tool_name === 'Bash', tool_input.command) and emits
165
- // a Claude PreToolUse envelope; fed a Cursor payload enforce-commands.mjs
166
- // short-circuits at gate G1 and writes 0 bytes to stdout AND stderr with
167
- // exit 0, so the harness sees no decision and the command runs. Cursor
168
- // needs an input/output adapter like scripts/lib/pi-hook-bridge.mjs; none
169
- // exists. Operator decision 2026-07-31 — gap registered, not closed:
170
- // #919 (Cursor no-op) tracks the adapter, #946 the allowlist-provenance
171
- // rule that every entry here names its issue. These entries exist so the
172
- // gap is MACHINE-readable (Check 6 counts them) rather than prose-only.
155
+ // Cursor is wired through scripts/lib/cursor-hook-bridge.mjs (#919 closed).
156
+ // Remaining gaps are tools Cursor does not expose, not missing adapters.
173
157
  cursor: {
174
- // #919: beforeShellExecution → PreToolUse. Only enforce-commands.mjs is
175
- // mapped at all; the other eight PreToolUse handlers have no Cursor
176
- // mapping whatsoever.
177
158
  PreToolUse: [
178
159
  // pre-task-scope-disjoint (#1020): NOT ported by construction. The hook
179
160
  // matches the `Agent` dispatch tool, which this platform does not have —
@@ -191,24 +172,9 @@ const DOCUMENTED_ASYMMETRIES = {
191
172
  // tests/hooks/pre-auq-clarity-wiring.test.mjs, which goes red then.
192
173
  'pre-auq-clarity.mjs',
193
174
  'pre-task-scope-disjoint.mjs',
194
- 'skill-invocation-telemetry.mjs', // #919
195
- 'enforce-scope.mjs', // #919
196
- 'config-protection.mjs', // #919
197
- 'pre-bash-destructive-guard.mjs', // #919
198
- 'pre-bash-staging-fence.mjs', // #919
199
- 'pre-bash-memory-propose-audit.mjs', // #919
200
- 'pre-bash-sessions-ledger-guard.mjs',// #919 (#958 — needs tool_name === 'Bash')
201
- 'pre-bash-templates-first.mjs', // #919
202
- 'pre-bash-issue-budget.mjs', // #919
203
- ],
204
- // #919: afterFileEdit → PostToolUse. Cursor maps enforce-scope.mjs here
205
- // (post-hoc warning only); none of Claude's four PostToolUse handlers
206
- // has a Cursor mapping.
207
- PostToolUse: [
208
- 'post-edit-validate.mjs', // #919
209
- 'post-tooluse-frontend-slop.mjs', // #919
210
- 'post-bash-write-verify.mjs', // #919 (#942 class — needs tool_name === 'Bash')
211
- 'loop-guard.mjs', // #919
175
+ // skill-invocation-telemetry: Cursor has no Skill tool — the matcher
176
+ // can never fire. Commands Read skills/*/SKILL.md instead.
177
+ 'skill-invocation-telemetry.mjs',
212
178
  ],
213
179
  },
214
180
  },
@@ -358,7 +324,7 @@ function extractHandlersByEvent(json) {
358
324
  const hookList = m.hooks || [];
359
325
  for (const h of hookList) {
360
326
  const cmd = h.command || h.script || '';
361
- const match = cmd.match(/(?:hooks\/|\$\{?CLAUDE_PLUGIN_ROOT\}?\/hooks\/|\$\{?CODEX_PLUGIN_ROOT\}?\/hooks\/|\$\{?PI_PLUGIN_ROOT\}?\/hooks\/)([\w/-]+\.mjs)/);
327
+ const match = cmd.match(/(?:hooks\/|\$\{?CLAUDE_PLUGIN_ROOT\}?\/hooks\/|\$\{?CODEX_PLUGIN_ROOT\}?\/hooks\/|\$\{?PI_PLUGIN_ROOT\}?\/hooks\/|\$\{?CURSOR_PLUGIN_ROOT\}?\/hooks\/)([\w/-]+\.mjs)/);
362
328
  if (match) handlers.add(match[1]);
363
329
  }
364
330
  }
@@ -36,8 +36,9 @@
36
36
  * - Verify non-scanned file extensions (e.g. .yaml) are not processed
37
37
  */
38
38
 
39
- import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
39
+ import { readFileSync, existsSync } from 'node:fs';
40
40
  import { join, relative } from 'node:path';
41
+ import { listRepoFiles } from './repo-files.mjs';
41
42
 
42
43
  const pluginRoot = process.argv[2];
43
44
  if (!pluginRoot) {
@@ -86,26 +87,6 @@ const SCAN_ROOTS = [
86
87
 
87
88
  const SCAN_EXTENSIONS = ['.md', '.mjs', '.js', '.ts'];
88
89
 
89
- /**
90
- * Recursively walk a directory and return all file paths with the allowed extensions.
91
- * @param {string} dir
92
- * @returns {string[]}
93
- */
94
- function walk(dir) {
95
- const results = [];
96
- if (!existsSync(dir)) return results;
97
- for (const name of readdirSync(dir)) {
98
- const full = join(dir, name);
99
- const st = statSync(full);
100
- if (st.isDirectory()) {
101
- results.push(...walk(full));
102
- } else if (SCAN_EXTENSIONS.some((ext) => name.endsWith(ext))) {
103
- results.push(full);
104
- }
105
- }
106
- return results;
107
- }
108
-
109
90
  const violations = [];
110
91
 
111
92
  for (const rel of SCAN_ROOTS) {
@@ -114,7 +95,17 @@ for (const rel of SCAN_ROOTS) {
114
95
  pass(`${rel}/ does not exist yet (no scan needed)`);
115
96
  continue;
116
97
  }
117
- const files = walk(absDir);
98
+ // The index, not the filesystem (#1143). This walk carried NO exclusion set
99
+ // — not even `node_modules` — while SCAN_EXTENSIONS covers .mjs/.js/.ts, so
100
+ // a vendored copy of the very package this canary forbids (a gitignored
101
+ // `skills/playwright-driver/node_modules/@playwright/mcp`) would be read as
102
+ // repo source and fail a BLOCKING gate on someone else's code.
103
+ // Honest scope note: unlike the other scanners migrated with it, this one is
104
+ // NOT exposed to the `.claude/worktrees/` case — measured 2026-08-26, its
105
+ // three SCAN_ROOTS are fixed deep subdirectories that a worktree cannot land
106
+ // in, and on-disk vs index counts were identical (2/2, 3/3, 2/2). The
107
+ // gitignored-vendored-tree case above is the reason it moved.
108
+ const files = listRepoFiles(pluginRoot, { dirs: [rel], exts: SCAN_EXTENSIONS });
118
109
  let fileViolations = 0;
119
110
  for (const file of files) {
120
111
  const text = readFileSync(file, 'utf8');
@@ -13,7 +13,8 @@
13
13
  * 7. Each name is unique within the array
14
14
  * 8. Array contains an entry named "ecosystem-health"
15
15
  * 9. Array contains an entry named "convergence-monitor"
16
- * 10. The 2 referenced watcher scripts exist on filesystem
16
+ * 10. Array contains an entry named "wave-transcript-tail" (#1114)
17
+ * 11. The 3 referenced watcher scripts exist on filesystem
17
18
  *
18
19
  * Path resolution note:
19
20
  * experimental.monitors value "./monitors/monitors.json" is repo-root-relative
@@ -50,11 +51,16 @@ let failed = 0;
50
51
  function pass(msg) { console.log(` PASS: ${msg}`); passed += 1; }
51
52
  function fail(msg) { console.log(` FAIL: ${msg}`); failed += 1; }
52
53
 
53
- const REQUIRED_MONITOR_NAMES = ['ecosystem-health', 'convergence-monitor'];
54
+ const REQUIRED_MONITOR_NAMES = [
55
+ 'ecosystem-health',
56
+ 'convergence-monitor',
57
+ 'wave-transcript-tail',
58
+ ];
54
59
  const REQUIRED_FIELDS = ['name', 'command', 'description'];
55
60
  const REQUIRED_SCRIPTS = [
56
61
  'scripts/lib/ecosystem-health.mjs',
57
62
  'scripts/lib/convergence-monitor.mjs',
63
+ 'scripts/lib/wave-transcript-tail.mjs',
58
64
  ];
59
65
 
60
66
  // ---------------------------------------------------------------------------
@@ -177,7 +183,7 @@ if (dups.length === 0) {
177
183
  }
178
184
 
179
185
  // ---------------------------------------------------------------------------
180
- // Checks 8-9: required monitor names present
186
+ // Checks 8-10: required monitor names present
181
187
  // ---------------------------------------------------------------------------
182
188
 
183
189
  for (const required of REQUIRED_MONITOR_NAMES) {
@@ -189,7 +195,7 @@ for (const required of REQUIRED_MONITOR_NAMES) {
189
195
  }
190
196
 
191
197
  // ---------------------------------------------------------------------------
192
- // Check 10: referenced watcher scripts exist on disk
198
+ // Check 11: referenced watcher scripts exist on disk
193
199
  // ---------------------------------------------------------------------------
194
200
 
195
201
  for (const scriptRel of REQUIRED_SCRIPTS) {
@@ -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