session-orchestrator 3.21.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 +11 -2
- 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/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +10 -10
- 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 +269 -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/memory-proposal-collector.md +6 -4
- 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/eli5.md +33 -0
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/commands/release.md +5 -3
- package/commands/test.md +2 -2
- 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 +7 -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 +52 -5
- package/docs/session-config-reference.md +57 -12
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +26 -16
- 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 +12 -2
- package/hooks/on-session-end.mjs +237 -104
- package/hooks/on-session-start.mjs +404 -110
- 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-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +133 -21
- 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 +3 -1
- package/pi/prompts/eli5.md +12 -0
- 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/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +12 -8
- 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/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -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 +221 -68
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config/vault-integration.mjs +12 -1
- 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/dispatcher/rank.mjs +4 -7
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +84 -11
- package/scripts/lib/gates/gate-helpers.mjs +93 -21
- 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/io.mjs +239 -0
- package/scripts/lib/issue-budget.mjs +284 -31
- 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/owner-interview.mjs +78 -32
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +104 -4
- 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 +165 -7
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- 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 +111 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +467 -72
- 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-auq-clarity.mjs +274 -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 +59 -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-rules.mjs +153 -9
- 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-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- 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/vcs-repo-spec.mjs +182 -13
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +478 -0
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/release.mjs +443 -122
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +6 -0
- package/scripts/validate-wave-scope.mjs +73 -1
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -25
- package/skills/_shared/parallel-aware-preamble.md +35 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +61 -8
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +5 -4
- 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/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/discovery/SKILL.md +28 -24
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/gitlab-ops/SKILL.md +30 -26
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/npm-publish/SKILL.md +4 -4
- package/skills/peekaboo-driver/SKILL.md +18 -6
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +35 -23
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +44 -4
- package/skills/session-end/SKILL.md +70 -24
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +48 -5
- 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 +96 -48
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- 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/session-start/soul.md +110 -0
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- 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 +129 -24
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/templates/_shared/journey-manifest.md +110 -0
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
- 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
|
|
@@ -84,6 +77,16 @@ const DOCUMENTED_ASYMMETRIES = {
|
|
|
84
77
|
// Cursor IDE and Pi v1 have no parallel agent dispatch at all
|
|
85
78
|
// (skills/session-plan/SKILL.md Platform Note), and Codex has no payload
|
|
86
79
|
// adapter for it. A matcher that can never fire is not enforcement.
|
|
80
|
+
// pre-auq-clarity (#1107): NOT ported by construction. The hook matches
|
|
81
|
+
// the `AskUserQuestion` tool, which this platform does not have — it
|
|
82
|
+
// renders a numbered Markdown list instead (docs/codex-setup.md:112,
|
|
83
|
+
// docs/cursor-setup.md:86), and pi-hook-bridge's TOOL_NAME_MAP carries
|
|
84
|
+
// no `askuserquestion` entry. A matcher on a tool that never fires is a
|
|
85
|
+
// dead entry: maintenance cost that ASSERTS a protection it does not
|
|
86
|
+
// provide. Revisit the day the platform gains the tool — the mechanical
|
|
87
|
+
// witness for that day is the mapPiToolName assertion in
|
|
88
|
+
// tests/hooks/pre-auq-clarity-wiring.test.mjs, which goes red then.
|
|
89
|
+
'pre-auq-clarity.mjs',
|
|
87
90
|
'pre-task-scope-disjoint.mjs',
|
|
88
91
|
'skill-invocation-telemetry.mjs',
|
|
89
92
|
'enforce-scope.mjs',
|
|
@@ -132,6 +135,16 @@ const DOCUMENTED_ASYMMETRIES = {
|
|
|
132
135
|
// Cursor IDE and Pi v1 have no parallel agent dispatch at all
|
|
133
136
|
// (skills/session-plan/SKILL.md Platform Note), and Codex has no payload
|
|
134
137
|
// adapter for it. A matcher that can never fire is not enforcement.
|
|
138
|
+
// pre-auq-clarity (#1107): NOT ported by construction. The hook matches
|
|
139
|
+
// the `AskUserQuestion` tool, which this platform does not have — it
|
|
140
|
+
// renders a numbered Markdown list instead (docs/codex-setup.md:112,
|
|
141
|
+
// docs/cursor-setup.md:86), and pi-hook-bridge's TOOL_NAME_MAP carries
|
|
142
|
+
// no `askuserquestion` entry. A matcher on a tool that never fires is a
|
|
143
|
+
// dead entry: maintenance cost that ASSERTS a protection it does not
|
|
144
|
+
// provide. Revisit the day the platform gains the tool — the mechanical
|
|
145
|
+
// witness for that day is the mapPiToolName assertion in
|
|
146
|
+
// tests/hooks/pre-auq-clarity-wiring.test.mjs, which goes red then.
|
|
147
|
+
'pre-auq-clarity.mjs',
|
|
135
148
|
'pre-task-scope-disjoint.mjs',
|
|
136
149
|
'skill-invocation-telemetry.mjs',
|
|
137
150
|
'pre-bash-sessions-ledger-guard.mjs', // #958
|
|
@@ -139,46 +152,29 @@ const DOCUMENTED_ASYMMETRIES = {
|
|
|
139
152
|
'pre-bash-issue-budget.mjs', // #946
|
|
140
153
|
],
|
|
141
154
|
},
|
|
142
|
-
// Cursor is
|
|
143
|
-
//
|
|
144
|
-
// Code payload shapes (tool_name === 'Bash', tool_input.command) and emits
|
|
145
|
-
// a Claude PreToolUse envelope; fed a Cursor payload enforce-commands.mjs
|
|
146
|
-
// short-circuits at gate G1 and writes 0 bytes to stdout AND stderr with
|
|
147
|
-
// exit 0, so the harness sees no decision and the command runs. Cursor
|
|
148
|
-
// needs an input/output adapter like scripts/lib/pi-hook-bridge.mjs; none
|
|
149
|
-
// exists. Operator decision 2026-07-31 — gap registered, not closed:
|
|
150
|
-
// #919 (Cursor no-op) tracks the adapter, #946 the allowlist-provenance
|
|
151
|
-
// rule that every entry here names its issue. These entries exist so the
|
|
152
|
-
// 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.
|
|
153
157
|
cursor: {
|
|
154
|
-
// #919: beforeShellExecution → PreToolUse. Only enforce-commands.mjs is
|
|
155
|
-
// mapped at all; the other eight PreToolUse handlers have no Cursor
|
|
156
|
-
// mapping whatsoever.
|
|
157
158
|
PreToolUse: [
|
|
158
159
|
// pre-task-scope-disjoint (#1020): NOT ported by construction. The hook
|
|
159
160
|
// matches the `Agent` dispatch tool, which this platform does not have —
|
|
160
161
|
// Cursor IDE and Pi v1 have no parallel agent dispatch at all
|
|
161
162
|
// (skills/session-plan/SKILL.md Platform Note), and Codex has no payload
|
|
162
163
|
// adapter for it. A matcher that can never fire is not enforcement.
|
|
164
|
+
// pre-auq-clarity (#1107): NOT ported by construction. The hook matches
|
|
165
|
+
// the `AskUserQuestion` tool, which this platform does not have — it
|
|
166
|
+
// renders a numbered Markdown list instead (docs/codex-setup.md:112,
|
|
167
|
+
// docs/cursor-setup.md:86), and pi-hook-bridge's TOOL_NAME_MAP carries
|
|
168
|
+
// no `askuserquestion` entry. A matcher on a tool that never fires is a
|
|
169
|
+
// dead entry: maintenance cost that ASSERTS a protection it does not
|
|
170
|
+
// provide. Revisit the day the platform gains the tool — the mechanical
|
|
171
|
+
// witness for that day is the mapPiToolName assertion in
|
|
172
|
+
// tests/hooks/pre-auq-clarity-wiring.test.mjs, which goes red then.
|
|
173
|
+
'pre-auq-clarity.mjs',
|
|
163
174
|
'pre-task-scope-disjoint.mjs',
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
'
|
|
167
|
-
'pre-bash-destructive-guard.mjs', // #919
|
|
168
|
-
'pre-bash-staging-fence.mjs', // #919
|
|
169
|
-
'pre-bash-memory-propose-audit.mjs', // #919
|
|
170
|
-
'pre-bash-sessions-ledger-guard.mjs',// #919 (#958 — needs tool_name === 'Bash')
|
|
171
|
-
'pre-bash-templates-first.mjs', // #919
|
|
172
|
-
'pre-bash-issue-budget.mjs', // #919
|
|
173
|
-
],
|
|
174
|
-
// #919: afterFileEdit → PostToolUse. Cursor maps enforce-scope.mjs here
|
|
175
|
-
// (post-hoc warning only); none of Claude's four PostToolUse handlers
|
|
176
|
-
// has a Cursor mapping.
|
|
177
|
-
PostToolUse: [
|
|
178
|
-
'post-edit-validate.mjs', // #919
|
|
179
|
-
'post-tooluse-frontend-slop.mjs', // #919
|
|
180
|
-
'post-bash-write-verify.mjs', // #919 (#942 class — needs tool_name === 'Bash')
|
|
181
|
-
'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',
|
|
182
178
|
],
|
|
183
179
|
},
|
|
184
180
|
},
|
|
@@ -328,7 +324,7 @@ function extractHandlersByEvent(json) {
|
|
|
328
324
|
const hookList = m.hooks || [];
|
|
329
325
|
for (const h of hookList) {
|
|
330
326
|
const cmd = h.command || h.script || '';
|
|
331
|
-
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)/);
|
|
332
328
|
if (match) handlers.add(match[1]);
|
|
333
329
|
}
|
|
334
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) {
|
|
@@ -30,6 +30,51 @@
|
|
|
30
30
|
// parse error as always-on with empty meta, so the skipped file is
|
|
31
31
|
// exactly the one that loads everywhere and clears every gate. See
|
|
32
32
|
// the inline rationale at the parse site below.
|
|
33
|
+
// (e) HARNESS-PARITY (#1108): a rule that expresses a path restriction
|
|
34
|
+
// must express it in `paths:`, because `paths:` is the ONLY key
|
|
35
|
+
// Claude Code's own rule loader reads. Its documentation
|
|
36
|
+
// (code.claude.com/docs/en/memory § "Path-specific rules") is
|
|
37
|
+
// explicit: rules are scoped "using YAML frontmatter with the
|
|
38
|
+
// `paths` field", and "rules without a `paths` field are loaded
|
|
39
|
+
// unconditionally and apply to all files". `globs:` is the CURSOR
|
|
40
|
+
// field name. A `globs:`-only rule is therefore scoped for
|
|
41
|
+
// rule-loader.mjs (#795 alias) and for Cursor, yet ALWAYS-ON for
|
|
42
|
+
// Claude Code — the rule *looks* scoped everywhere it is inspected
|
|
43
|
+
// and is silently loaded everywhere it is used.
|
|
44
|
+
// Measured 2026-08-22 before the fix: 16 of 31 rule files carried
|
|
45
|
+
// `globs:` and 0 carried `paths:`, so all 31 loaded unconditionally
|
|
46
|
+
// — 186,993 bytes ≈ 46,700 tokens per dispatch, 72,195 of them
|
|
47
|
+
// unwanted (testing.md alone is 36,431 bytes and carries
|
|
48
|
+
// `tier: wave-only`, i.e. is explicitly meant to be conditional).
|
|
49
|
+
// This check is the recurrence guard for that fix, not the fix.
|
|
50
|
+
// It is formulated as a PARITY check rather than a presence check,
|
|
51
|
+
// which folds two defects into one invariant: the rule must load in
|
|
52
|
+
// the SAME contexts under Claude Code's loader (which reads
|
|
53
|
+
// `paths:`, treating absence as always-on) as under rule-loader.mjs
|
|
54
|
+
// (which reads `globs:` and falls back to `paths:` — #795, globs
|
|
55
|
+
// wins). Two ways to violate it:
|
|
56
|
+
// - `globs:` present and non-empty, `paths:` ABSENT → always-on
|
|
57
|
+
// in Claude Code.
|
|
58
|
+
// - both present with DIFFERENT pattern sets → Claude Code scopes
|
|
59
|
+
// on one list, rule-loader.mjs and Cursor on the other.
|
|
60
|
+
// Order and duplicates are irrelevant (a glob list is matched
|
|
61
|
+
// any-of), so the comparison is over the sorted unique set.
|
|
62
|
+
// NOT flagged: `paths:` alone. It is the form the native
|
|
63
|
+
// documentation prescribes and the form the primary downstream
|
|
64
|
+
// consumer uses exclusively (projects-baseline: 26 rule files, all
|
|
65
|
+
// `paths:`, 0 `globs:` — rule-loader.mjs module doc). The
|
|
66
|
+
// `globs:`-is-canonical preference for VENDORED rules is a separate,
|
|
67
|
+
// warn-level concern already owned by
|
|
68
|
+
// scripts/lib/validate-vendored-rules.mjs (~:289, issue #742) and is
|
|
69
|
+
// deliberately not duplicated here.
|
|
70
|
+
// NOT flagged either: an EMPTY `globs: []`. That shape is already
|
|
71
|
+
// owned — with an accurate, opposite message — by the dedicated
|
|
72
|
+
// empty-globs branches below (#880 QA Defect 1 / #892), and firing
|
|
73
|
+
// a second, contradictory-sounding finding ("never loads" beside
|
|
74
|
+
// "always-on in Claude Code") on one file is exactly the confusion
|
|
75
|
+
// those fixes went out of their way to remove. The parity check
|
|
76
|
+
// therefore requires a NON-EMPTY `globs:` list; populating it is the
|
|
77
|
+
// prescribed remedy for `globs: []`, at which point parity binds.
|
|
33
78
|
//
|
|
34
79
|
// (2) HANDWRITTEN rules (no `auto-generated: true` — #880 FA5, WARN-only,
|
|
35
80
|
// NEVER affects the exit code). The auto-generated brandmauer above only
|
|
@@ -70,9 +115,10 @@
|
|
|
70
115
|
//
|
|
71
116
|
// Usage: check-rules.mjs <plugin-root>
|
|
72
117
|
// Outputs lines of the form " PASS: ..." / " FAIL: ..." / " WARN: ...".
|
|
73
|
-
// Exit 0 = all AUTO-GENERATED invariants
|
|
74
|
-
//
|
|
75
|
-
//
|
|
118
|
+
// Exit 0 = all AUTO-GENERATED invariants plus the two cohort-independent
|
|
119
|
+
// invariants (d) frontmatter-parses and (e) harness-parity are satisfied (WARN
|
|
120
|
+
// lines from the handwritten check NEVER affect the exit code); exit 1 = at
|
|
121
|
+
// least one FAIL.
|
|
76
122
|
|
|
77
123
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
78
124
|
import { join } from 'node:path';
|
|
@@ -132,6 +178,69 @@ function hasFrontmatterKey(contents, key) {
|
|
|
132
178
|
return Boolean(keyMatch && keyMatch[1] && keyMatch[1].trim() !== '');
|
|
133
179
|
}
|
|
134
180
|
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Per-key list access for the harness-parity check (#1108).
|
|
183
|
+
//
|
|
184
|
+
// `parseGlobsFrontmatter()` deliberately COLLAPSES `globs:` and `paths:` into
|
|
185
|
+
// one value ("callers never see which key produced the value" — rule-loader.mjs
|
|
186
|
+
// module doc, #795 precedence). The parity check needs both lists separately,
|
|
187
|
+
// and re-deriving list parsing here would be a second frontmatter parser — the
|
|
188
|
+
// exact drift class that produced #840. So instead of re-parsing, the OTHER key
|
|
189
|
+
// is MASKED to a name the parser ignores (any key that is neither globs/paths
|
|
190
|
+
// nor in SCALAR_META_KEYS is dropped without error, and its indented ` - x`
|
|
191
|
+
// continuation lines are skipped as "another block's continuation"), and the
|
|
192
|
+
// SAME parser then returns the surviving key's list verbatim — flow style,
|
|
193
|
+
// block style, quote stripping and provenance-header tolerance included.
|
|
194
|
+
//
|
|
195
|
+
// Masking cannot make a parseable file unparseable: it renames a key, so the
|
|
196
|
+
// line keeps its colon. Callers reach this only for files that already parsed.
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
const MASKED_KEY_PREFIX = 'x-check-rules-masked-';
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Returns the value of exactly ONE of the two scope keys, ignoring the other —
|
|
202
|
+
* `null` when that key is absent, `[]` when present but empty.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} contents - raw file contents (must already parse)
|
|
205
|
+
* @param {'globs'|'paths'} key - the key whose list to return
|
|
206
|
+
* @returns {string[] | null}
|
|
207
|
+
*/
|
|
208
|
+
function scopeKeyValue(contents, key) {
|
|
209
|
+
const other = key === 'globs' ? 'paths' : 'globs';
|
|
210
|
+
const masked = contents.replace(
|
|
211
|
+
new RegExp(`^${other}:`, 'gm'),
|
|
212
|
+
`${MASKED_KEY_PREFIX}${other}:`,
|
|
213
|
+
);
|
|
214
|
+
return parseGlobsFrontmatter(masked).globs;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Sorted, de-duplicated copy — glob lists are matched any-of, so neither
|
|
218
|
+
* order nor repetition changes which files a rule applies to. */
|
|
219
|
+
function normalizePatterns(list) {
|
|
220
|
+
return [...new Set(list)].sort();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Harness-parity verdict for one rule file (#1108) — see module doc (e).
|
|
225
|
+
*
|
|
226
|
+
* @param {string} contents - raw file contents (must already parse)
|
|
227
|
+
* @returns {{ ok: true } | { ok: false, kind: 'missing-paths'|'divergent', globs: string[], paths: string[] | null }}
|
|
228
|
+
*/
|
|
229
|
+
function checkHarnessParity(contents) {
|
|
230
|
+
const globs = scopeKeyValue(contents, 'globs');
|
|
231
|
+
// Only a NON-EMPTY globs list expresses a path restriction. Absent globs →
|
|
232
|
+
// whatever `paths:` says is what every loader sees. Empty globs → owned by
|
|
233
|
+
// the dedicated empty-globs branches, which prescribe populating it.
|
|
234
|
+
if (!Array.isArray(globs) || globs.length === 0) return { ok: true };
|
|
235
|
+
|
|
236
|
+
const paths = scopeKeyValue(contents, 'paths');
|
|
237
|
+
if (paths === null) return { ok: false, kind: 'missing-paths', globs, paths };
|
|
238
|
+
|
|
239
|
+
const same =
|
|
240
|
+
JSON.stringify(normalizePatterns(globs)) === JSON.stringify(normalizePatterns(paths));
|
|
241
|
+
return same ? { ok: true } : { ok: false, kind: 'divergent', globs, paths };
|
|
242
|
+
}
|
|
243
|
+
|
|
135
244
|
// ============================================================================
|
|
136
245
|
// Check: .claude/rules/ rule invariants
|
|
137
246
|
// ============================================================================
|
|
@@ -207,17 +316,44 @@ for (const name of mdFiles.sort()) {
|
|
|
207
316
|
const { globs, meta } = parsed;
|
|
208
317
|
const rel = `.claude/rules/${name}`;
|
|
209
318
|
|
|
319
|
+
// (e) HARNESS-PARITY (#1108) — cohort-independent, like the parse check
|
|
320
|
+
// above: `paths:` is the only scope key Claude Code's own loader reads, so a
|
|
321
|
+
// `globs:`-only or divergently-scoped rule loads differently there than it
|
|
322
|
+
// does under rule-loader.mjs. Emitted here, in file order, and recorded on
|
|
323
|
+
// the entry so the cohort branches below suppress their PASS line — a file
|
|
324
|
+
// must never both PASS and FAIL.
|
|
325
|
+
const parity = checkHarnessParity(contents);
|
|
326
|
+
if (!parity.ok && parity.kind === 'missing-paths') {
|
|
327
|
+
fail(
|
|
328
|
+
`${rel} — declares globs: (${parity.globs.length} pattern(s)) but no paths: — Claude Code's native rule ` +
|
|
329
|
+
'loader reads ONLY the `paths` frontmatter field and treats a rule without it as unconditional ' +
|
|
330
|
+
'("rules without a paths field are loaded unconditionally and apply to all files" — ' +
|
|
331
|
+
'code.claude.com/docs/en/memory § Path-specific rules). `globs:` is the Cursor field name, so this rule ' +
|
|
332
|
+
'is scoped for rule-loader.mjs and Cursor but loads ALWAYS-ON in Claude Code — a silent instruction-budget ' +
|
|
333
|
+
'failure (#1108). Add a paths: key carrying the same patterns as globs: (keep globs: — it stays the ' +
|
|
334
|
+
'canonical form for vendored rules, validate-vendored-rules.mjs #742).',
|
|
335
|
+
);
|
|
336
|
+
} else if (!parity.ok && parity.kind === 'divergent') {
|
|
337
|
+
fail(
|
|
338
|
+
`${rel} — globs: and paths: declare DIFFERENT pattern sets (globs: ${JSON.stringify(parity.globs)} vs ` +
|
|
339
|
+
`paths: ${JSON.stringify(parity.paths)}) — Claude Code's native loader scopes on paths:, while ` +
|
|
340
|
+
'rule-loader.mjs and Cursor scope on globs: (globs: wins silently when both are present, #795). The rule ' +
|
|
341
|
+
'therefore loads in different contexts on different harnesses (#1108). Make the two lists carry the same ' +
|
|
342
|
+
'patterns — order and duplicates are irrelevant.',
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
210
346
|
if (meta['auto-generated'] === true) {
|
|
211
|
-
autoGeneratedEntries.push({ rel, globs, meta });
|
|
347
|
+
autoGeneratedEntries.push({ rel, globs, meta, parityOk: parity.ok });
|
|
212
348
|
} else {
|
|
213
|
-
handwrittenEntries.push({ rel, globs, meta, contents });
|
|
349
|
+
handwrittenEntries.push({ rel, globs, meta, contents, parityOk: parity.ok });
|
|
214
350
|
}
|
|
215
351
|
}
|
|
216
352
|
|
|
217
353
|
// ---------------------------------------------------------------------------
|
|
218
354
|
// Auto-generated branch (FA4 #697) — UNCHANGED behaviour, hard-fail.
|
|
219
355
|
// ---------------------------------------------------------------------------
|
|
220
|
-
for (const { rel, globs, meta } of autoGeneratedEntries) {
|
|
356
|
+
for (const { rel, globs, meta, parityOk } of autoGeneratedEntries) {
|
|
221
357
|
// (a) never-always-on: must have at least one activation axis.
|
|
222
358
|
// globs is one of: null (no frontmatter / no globs key), [] (key present
|
|
223
359
|
// but EMPTY — the #892 QA-Defect special case, see module doc), or a
|
|
@@ -264,9 +400,11 @@ for (const { rel, globs, meta } of autoGeneratedEntries) {
|
|
|
264
400
|
// an empty globs array, which is dead-not-passing regardless of host-class)
|
|
265
401
|
// AND learning-key AND expires-at. When any failed, the FAIL line(s) above
|
|
266
402
|
// already emitted.
|
|
403
|
+
// `parityOk` joins the conjunction for the same reason `hasEmptyGlobs` does:
|
|
404
|
+
// its FAIL already emitted above, and a file must never both PASS and FAIL.
|
|
267
405
|
const lkOk = Object.prototype.hasOwnProperty.call(meta, 'learning-key');
|
|
268
406
|
const eaOk = Object.prototype.hasOwnProperty.call(meta, 'expires-at');
|
|
269
|
-
if (!hasEmptyGlobs && (hasGlobs || hasHostClass) && lkOk && eaOk) {
|
|
407
|
+
if (!hasEmptyGlobs && (hasGlobs || hasHostClass) && lkOk && eaOk && parityOk) {
|
|
270
408
|
pass(`${rel} — auto-generated rule satisfies all invariants`);
|
|
271
409
|
}
|
|
272
410
|
}
|
|
@@ -283,7 +421,7 @@ console.log(
|
|
|
283
421
|
'--- Check: .claude/rules/ handwritten-rule activation + review-date (warn mode, #880) ---',
|
|
284
422
|
);
|
|
285
423
|
|
|
286
|
-
for (const { rel, globs, meta, contents } of handwrittenEntries) {
|
|
424
|
+
for (const { rel, globs, meta, contents, parityOk } of handwrittenEntries) {
|
|
287
425
|
// `globs` (the merged globs/paths value from parseGlobsFrontmatter — #795
|
|
288
426
|
// alias, `globs:` wins when both are present) is one of:
|
|
289
427
|
// - null → neither key present
|
|
@@ -333,7 +471,13 @@ for (const { rel, globs, meta, contents } of handwrittenEntries) {
|
|
|
333
471
|
);
|
|
334
472
|
}
|
|
335
473
|
|
|
336
|
-
|
|
474
|
+
// `parityOk` joins the conjunction for the same reason `hasEmptyGlobs` does
|
|
475
|
+
// (#1108): its FAIL already emitted in the parse loop, and a file must never
|
|
476
|
+
// both PASS and FAIL. This is the one place a handwritten rule's PASS is
|
|
477
|
+
// withheld for a HARD-fail reason — the parity invariant is cohort-
|
|
478
|
+
// independent by construction, so it is not part of this branch's
|
|
479
|
+
// deliberately warn-only posture.
|
|
480
|
+
if (!hasEmptyGlobs && hasAxis && hasReviewDate && parityOk) {
|
|
337
481
|
pass(`${rel} — handwritten rule has an activation axis and a review-date`);
|
|
338
482
|
}
|
|
339
483
|
}
|