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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +168 -0
- package/README.md +18 -15
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -1
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +76 -4
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +26 -4
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/enforce-scope.mjs +64 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +211 -10
- package/hooks/on-session-start.mjs +214 -11
- package/hooks/on-stop.mjs +48 -9
- package/hooks/post-subagent-discovery-validator.mjs +34 -3
- package/hooks/post-tool-batch-wave-signal.mjs +11 -2
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +5 -0
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +57 -6
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +281 -13
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +90 -0
- package/scripts/lib/session-close-backfill.mjs +369 -28
- package/scripts/lib/session-discovery.mjs +13 -3
- package/scripts/lib/session-end/phase-skip.mjs +37 -4
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +159 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/sync.mjs +46 -8
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-unwired-features.mjs +340 -32
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-writer.mjs +211 -10
- package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +209 -12
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/parallel-aware-preamble.md +4 -2
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +41 -1
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +11 -3
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +30 -16
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +102 -19
- package/templates/_shared/journey-manifest.md +110 -0
- 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
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// literal here was a second copy
|
|
19
|
-
//
|
|
20
|
-
|
|
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: [
|
|
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
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
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
|
|
163
|
-
//
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
'
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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 = [
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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:
|
|
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':
|
|
753
|
-
'B2-prose-pin-suspected':
|
|
754
|
-
'B3-bare-hook-exit-code':
|
|
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':
|
|
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(
|
|
763
|
-
|
|
764
|
-
|
|
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: ${
|
|
767
|
-
for (const f of
|
|
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
|
|