session-orchestrator 3.22.0 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/remote-offload/SKILL.md +13 -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 +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- 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 +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- 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 +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -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 +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- 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/hooks/subagent-telemetry.mjs +11 -26
- 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 +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- 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/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- 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/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- 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/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- 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 +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- 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 +517 -18
- 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 +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- 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 +220 -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-canonical.mjs +446 -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/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- 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 +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- 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-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- 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-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- 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/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- 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 +20 -9
- 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 +54 -17
- 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 +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- 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) {
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Check: every `scripts/**.mjs` path cited in `skills/`, `commands/` and
|
|
4
|
+
* `agents/` either EXISTS or is annotated as deliberately absent (#1176).
|
|
5
|
+
* Extended (#1187) to also cite `scripts/**.sh` and `hooks/**.sh` — see
|
|
6
|
+
* "## Mode: BLOCKING for `.mjs`, ADVISORY for `.sh`" below for why that half
|
|
7
|
+
* is advisory, not blocking.
|
|
8
|
+
*
|
|
9
|
+
* ## Why
|
|
10
|
+
*
|
|
11
|
+
* Prose is not executed. A skill body that tells the coordinator to run
|
|
12
|
+
* `node scripts/lib/auto-commit.mjs` costs an operator a failed command and a
|
|
13
|
+
* re-derivation of what the file was supposed to do — and nothing in the
|
|
14
|
+
* corpus notices, because a markdown file compiles under every gate. Measured
|
|
15
|
+
* 2026-09-02 @ c3ab480: 237 distinct citations across the three scan roots,
|
|
16
|
+
* 7 of them dead.
|
|
17
|
+
*
|
|
18
|
+
* ## Fences are skipped, and that is most of the answer
|
|
19
|
+
*
|
|
20
|
+
* 4 of those 7 sat inside fenced code blocks — synthetic example paths
|
|
21
|
+
* (`scripts/example.mjs`, `scripts/lib/a.mjs`) in a snippet demonstrating a
|
|
22
|
+
* command's argument shape. A fenced snippet is an illustration of a FORM, not
|
|
23
|
+
* a claim that a file exists, so the shared fence tracker
|
|
24
|
+
* (`./markdown-fences.mjs`, #1181) silences them structurally
|
|
25
|
+
* rather than by allowlist.
|
|
26
|
+
*
|
|
27
|
+
* ## Annotation, and why placement is a rule rather than a convenience
|
|
28
|
+
*
|
|
29
|
+
* A citation in PROSE is a claim. When the claim is deliberate — a planned file
|
|
30
|
+
* behind an issue, a historical path kept for narrative, an inline example —
|
|
31
|
+
* say so on the line:
|
|
32
|
+
*
|
|
33
|
+
* <!-- path-check: planned #214 -->
|
|
34
|
+
* <!-- path-check: historical -->
|
|
35
|
+
* <!-- path-check: example -->
|
|
36
|
+
*
|
|
37
|
+
* The marker is honoured on the SAME line as the citation, or on the line
|
|
38
|
+
* IMMEDIATELY above WHEN THAT LINE CITES NOTHING ITSELF — nowhere else. A line
|
|
39
|
+
* carrying `citation + marker` exempts only that citation; it does not reach
|
|
40
|
+
* down to the next line, which would silently exempt a citation nobody
|
|
41
|
+
* annotated. Two lines above is INERT and the citation
|
|
42
|
+
* still reports, which is pinned by a test. The reason is the rule
|
|
43
|
+
* `recurring-issue-an-exemption-marker-that-only-works-same-line-is-visually-identical-to-one-in-a-comment-block-3bff005.md`
|
|
44
|
+
* in `.claude/rules/`:
|
|
45
|
+
* a marker that reads like an exemption but changes nothing is worse than no
|
|
46
|
+
* marker at all, because the guard then looks wrong instead of the marker
|
|
47
|
+
* looking misplaced. A malformed marker (unknown class, or `planned` without a
|
|
48
|
+
* `#<iid>`) is itself a finding for the same reason — it must never fail silent.
|
|
49
|
+
*
|
|
50
|
+
* ## Mode: BLOCKING for `.mjs`, ADVISORY for `.sh` (#1187)
|
|
51
|
+
*
|
|
52
|
+
* Unlike `check-doc-cli-commands.mjs`, the oracle here is the repository's own
|
|
53
|
+
* filesystem, not a locally installed third-party binary — there is no version
|
|
54
|
+
* skew that could red an unrelated commit. So `.mjs` findings are `FAIL:` and
|
|
55
|
+
* the check returns non-zero, EXACTLY as before this module grew a second
|
|
56
|
+
* extension.
|
|
57
|
+
*
|
|
58
|
+
* The `.sh` half of the citation grammar (below) does not get that same
|
|
59
|
+
* severity by default. A #1176 repo-wide grep (`scripts/hooks` prose across
|
|
60
|
+
* `skills/commands/agents/docs/hooks`) found 27 distinct `.sh` citations, 21
|
|
61
|
+
* dead — but only ONE of those 27 sits inside this checker's three scan roots
|
|
62
|
+
* (`skills/contract-version-bump/SKILL.md:134`, itself arguably a
|
|
63
|
+
* cross-repo path — see the dry-run note at `scanSkillScriptPaths`'s
|
|
64
|
+
* `strictSh` option). The other 26 live in `docs/`, which this checker does
|
|
65
|
+
* NOT scan and — per this same paragraph's own evidence — MUST NOT start
|
|
66
|
+
* scanning as a side effect of the `.sh` extension: `docs/adr/*.md` alone
|
|
67
|
+
* carries 7 dead `.mjs` citations of its own (all historical/planned ADR
|
|
68
|
+
* prose, e.g. `scripts/lib/tool-adapter.mjs`, `scripts/lib/auto-commit.mjs`),
|
|
69
|
+
* none annotated, all outside this task's edit scope. Widening `SCAN_DIRS` to
|
|
70
|
+
* `docs` would turn those 7 into new BLOCKING findings on a doc surface
|
|
71
|
+
* nobody triaged — the opposite of "the `.mjs` behaviour stays exactly as
|
|
72
|
+
* today". So `SCAN_DIRS` stays `['skills', 'commands', 'agents']`; the wider
|
|
73
|
+
* `docs`/`hooks` prose census is a follow-up for whoever owns those files,
|
|
74
|
+
* not a silent scope change here.
|
|
75
|
+
*
|
|
76
|
+
* A `.sh` finding is therefore `WARN:` by default (visible, never blocking —
|
|
77
|
+
* `ok` and the CLI exit code ignore `severity: 'warn'` findings) and only
|
|
78
|
+
* becomes `FAIL:`/blocking under the `--strict-sh` CLI flag (or
|
|
79
|
+
* `strictSh: true` for `scanSkillScriptPaths()` callers) — flip that default
|
|
80
|
+
* once the dead `.sh` citations this checker CAN see are fixed by their doc
|
|
81
|
+
* owner (BV-004 revisit trigger).
|
|
82
|
+
*
|
|
83
|
+
* @module scripts/lib/validate/check-skill-script-paths
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
87
|
+
import path from 'node:path';
|
|
88
|
+
import { pathToFileURL } from 'node:url';
|
|
89
|
+
import { listRepoFiles } from './repo-files.mjs';
|
|
90
|
+
import { forEachLine } from './markdown-fences.mjs';
|
|
91
|
+
|
|
92
|
+
/** Documentation roots whose prose is treated as a claim about the repo. */
|
|
93
|
+
export const SCAN_DIRS = Object.freeze(['skills', 'commands', 'agents']);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A cited script path. One regex, one alternation, reused for every
|
|
97
|
+
* extension rather than a second scanner (#1187): `scripts/**.mjs` (the
|
|
98
|
+
* original, still the only `.mjs` root scanned), `scripts/**.sh` and
|
|
99
|
+
* `hooks/**.sh`. `hooks/**.mjs` is deliberately NOT part of this grammar —
|
|
100
|
+
* the `.mjs` half of the citation surface stays exactly `scripts/`, matching
|
|
101
|
+
* every existing annotation and fence-skip test unchanged.
|
|
102
|
+
*/
|
|
103
|
+
const CITATION_RE = /scripts\/[a-zA-Z0-9_/-]*\.(?:mjs|sh)|hooks\/[a-zA-Z0-9_/-]*\.sh/g;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Filename fragments that mark a citation as an ILLUSTRATIVE placeholder —
|
|
107
|
+
* `scripts/example.sh`, `hooks/my-hook.sh`, `scripts/<name>.sh` — rather than
|
|
108
|
+
* a claim that a real file exists. Checked only for a citation that already
|
|
109
|
+
* failed `existsSync` (a real file is never suppressed by this list, no
|
|
110
|
+
* matter what it's named). Recognised automatically, with no marker needed,
|
|
111
|
+
* because #1176 found 6 such `hooks/*.mjs` example names in hook-development
|
|
112
|
+
* prose (`hooks/example.mjs`, `guard.mjs`, `my-hook.mjs`, …) that would
|
|
113
|
+
* otherwise all need a hand-written `<!-- path-check: example -->` on every
|
|
114
|
+
* occurrence.
|
|
115
|
+
*
|
|
116
|
+
* Ceiling (BV-004): exactly these six fragments, case-insensitive substring
|
|
117
|
+
* match. A REAL path that happens to contain one of them (`scripts/lib/
|
|
118
|
+
* foobar-report.mjs`, `hooks/my-guard.sh`) is indistinguishable from a
|
|
119
|
+
* placeholder by this heuristic and would be silently swallowed if it were
|
|
120
|
+
* ever cited before being created. Revisit by shrinking this list (never
|
|
121
|
+
* growing it further) the moment that collision is observed for real — the
|
|
122
|
+
* escape hatch until then is the same `<!-- path-check: planned #<iid> -->`
|
|
123
|
+
* marker every other deliberate citation already uses.
|
|
124
|
+
*/
|
|
125
|
+
const PLACEHOLDER_FRAGMENTS = Object.freeze(['example', 'my-', '<', 'placeholder', 'foo', 'bar']);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Is `citedPath` an illustrative placeholder name rather than a real path?
|
|
129
|
+
*
|
|
130
|
+
* @param {string} citedPath
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
export function isPlaceholderCitation(citedPath) {
|
|
134
|
+
const lower = citedPath.toLowerCase();
|
|
135
|
+
return PLACEHOLDER_FRAGMENTS.some((fragment) => lower.includes(fragment));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The annotation marker, in any of its three classes. */
|
|
139
|
+
const ANNOTATION_RE = /<!--\s*path-check:\s*([^>]*?)\s*-->/;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Judge one annotation payload.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} payload the text between `path-check:` and `-->`
|
|
145
|
+
* @returns {{ok: boolean, class: string}}
|
|
146
|
+
*/
|
|
147
|
+
export function classifyAnnotation(payload) {
|
|
148
|
+
const text = payload.trim();
|
|
149
|
+
if (text === 'historical' || text === 'example') return { ok: true, class: text };
|
|
150
|
+
const planned = text.match(/^planned\s+#(\d+)$/);
|
|
151
|
+
if (planned) return { ok: true, class: `planned #${planned[1]}` };
|
|
152
|
+
return { ok: false, class: text };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Split a markdown body into citations and annotations, both OUTSIDE fences.
|
|
157
|
+
*
|
|
158
|
+
* The fence automaton is `./markdown-fences.mjs` (#1181 — one tracker
|
|
159
|
+
* shared with `check-doc-cli-commands.mjs` and
|
|
160
|
+
* `check-vcs-repo-flag.mjs`): a fence opens on ``` / ~~~ with an optional
|
|
161
|
+
* info string and closes on the same character, at least as long, with no
|
|
162
|
+
* info string.
|
|
163
|
+
*
|
|
164
|
+
* Two properties are load-bearing because the automaton fails OPEN:
|
|
165
|
+
*
|
|
166
|
+
* 1. A fence that never closes swallows the whole rest of the file. That is a
|
|
167
|
+
* doc defect in its own right (`unbalanced-fence`), so it is REPORTED —
|
|
168
|
+
* and the swallowed tail is re-read as prose, so a dead citation hiding
|
|
169
|
+
* behind the unmatched opener still surfaces instead of being silenced by
|
|
170
|
+
* the very defect that made it invisible. Measured on
|
|
171
|
+
* `agents/db-specialist.md`, where a stray closing fence opened a block
|
|
172
|
+
* that ran to EOF and blinded the last 41 lines.
|
|
173
|
+
* 2. A fence inside a blockquote (`> ```) is a fence. Without stripping the
|
|
174
|
+
* `>` chain first, a quoted fenced example is read as prose and its
|
|
175
|
+
* illustrative paths are reported — a false red, the fail-CLOSED mirror of
|
|
176
|
+
* the same blind spot.
|
|
177
|
+
*
|
|
178
|
+
* @param {string[]} lines body split on `\n`
|
|
179
|
+
* @returns {{citations: {line: number, path: string}[], annotations: Map<number, {ok: boolean, class: string, raw: string}>, unbalancedFence: {line: number} | null}}
|
|
180
|
+
*/
|
|
181
|
+
export function extractCitations(lines) {
|
|
182
|
+
/** @type {{line: number, path: string}[]} */
|
|
183
|
+
const citations = [];
|
|
184
|
+
/** @type {Map<number, {ok: boolean, class: string, raw: string}>} */
|
|
185
|
+
const annotations = new Map();
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Read one line as prose.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} raw the line
|
|
191
|
+
* @param {number} lineNumber its 1-based position
|
|
192
|
+
*/
|
|
193
|
+
const collect = (raw, lineNumber) => {
|
|
194
|
+
const annotation = raw.match(ANNOTATION_RE);
|
|
195
|
+
if (annotation) {
|
|
196
|
+
annotations.set(lineNumber, { ...classifyAnnotation(annotation[1]), raw: annotation[0] });
|
|
197
|
+
}
|
|
198
|
+
for (const hit of raw.matchAll(CITATION_RE)) {
|
|
199
|
+
citations.push({ line: lineNumber, path: hit[0] });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// A blockquoted fence is still a fence — the shared tracker strips the `>`
|
|
204
|
+
// chain before detection so the quoted example's body stays fenced.
|
|
205
|
+
const { unbalancedFenceLine } = forEachLine(
|
|
206
|
+
lines.join('\n'),
|
|
207
|
+
(raw, { lineNumber, inFence }) => {
|
|
208
|
+
if (inFence) return;
|
|
209
|
+
collect(raw, lineNumber);
|
|
210
|
+
},
|
|
211
|
+
{ stripBlockquotes: true },
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (unbalancedFenceLine === null) return { citations, annotations, unbalancedFence: null };
|
|
215
|
+
|
|
216
|
+
// EOF with the fence still open: never swallow silently. Re-read the tail as
|
|
217
|
+
// prose so the citations the defect hid are reported alongside it.
|
|
218
|
+
for (let index = unbalancedFenceLine; index < lines.length; index += 1) collect(lines[index], index + 1);
|
|
219
|
+
return { citations, annotations, unbalancedFence: { line: unbalancedFenceLine } };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Census the documentation corpus for dead `scripts/**.mjs`/`.sh` and
|
|
224
|
+
* `hooks/**.sh` citations.
|
|
225
|
+
*
|
|
226
|
+
* @param {{pluginRoot: string, dirs?: string[], strictSh?: boolean}} options
|
|
227
|
+
* `strictSh` (default `false`) promotes a dead `.sh` citation from
|
|
228
|
+
* `severity: 'warn'` to `severity: 'fail'` — see the module docblock
|
|
229
|
+
* "Mode: BLOCKING for `.mjs`, ADVISORY for `.sh`" for why the default stays
|
|
230
|
+
* advisory in this release.
|
|
231
|
+
* @returns {{ok: boolean, summary: object, findings: {kind: string, file: string, line: number, path: string, annotation: string | null, message: string, severity: 'fail' | 'warn'}[], toolError: boolean}}
|
|
232
|
+
*/
|
|
233
|
+
export function scanSkillScriptPaths({ pluginRoot, dirs = SCAN_DIRS, strictSh = false }) {
|
|
234
|
+
/** @type {{kind: string, file: string, line: number, path: string, annotation: string | null, message: string, severity: 'fail' | 'warn'}[]} */
|
|
235
|
+
const findings = [];
|
|
236
|
+
const summary = {
|
|
237
|
+
filesScanned: 0,
|
|
238
|
+
citations: 0,
|
|
239
|
+
existing: 0,
|
|
240
|
+
annotated: 0,
|
|
241
|
+
placeholders: 0,
|
|
242
|
+
findings: 0,
|
|
243
|
+
warnings: 0,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/** @type {string[]} */
|
|
247
|
+
let files;
|
|
248
|
+
try {
|
|
249
|
+
// The git index, never a `readdirSync` walk (#1143): a walk cannot see
|
|
250
|
+
// `.gitignore`, so a worktree under `.claude/worktrees/` or any ignored
|
|
251
|
+
// artefact would enter this census as if it were repository documentation.
|
|
252
|
+
files = listRepoFiles(pluginRoot, { dirs, exts: ['.md'] });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
findings.push({
|
|
255
|
+
kind: 'tool-error',
|
|
256
|
+
file: '-',
|
|
257
|
+
line: 0,
|
|
258
|
+
path: '-',
|
|
259
|
+
annotation: null,
|
|
260
|
+
message: `cannot enumerate the scan corpus: ${error instanceof Error ? error.message : String(error)}`,
|
|
261
|
+
severity: 'fail',
|
|
262
|
+
});
|
|
263
|
+
return { ok: false, summary, findings, toolError: true };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const absolute of files) {
|
|
267
|
+
const relative = path.relative(pluginRoot, absolute);
|
|
268
|
+
/** @type {string} */
|
|
269
|
+
let body;
|
|
270
|
+
try {
|
|
271
|
+
body = readFileSync(absolute, 'utf8');
|
|
272
|
+
} catch (error) {
|
|
273
|
+
findings.push({
|
|
274
|
+
kind: 'tool-error',
|
|
275
|
+
file: relative,
|
|
276
|
+
line: 0,
|
|
277
|
+
path: '-',
|
|
278
|
+
annotation: null,
|
|
279
|
+
message: `cannot read: ${error instanceof Error ? error.message : String(error)}`,
|
|
280
|
+
severity: 'fail',
|
|
281
|
+
});
|
|
282
|
+
return { ok: false, summary, findings, toolError: true };
|
|
283
|
+
}
|
|
284
|
+
summary.filesScanned += 1;
|
|
285
|
+
|
|
286
|
+
const { citations, annotations, unbalancedFence } = extractCitations(body.split('\n'));
|
|
287
|
+
if (unbalancedFence) {
|
|
288
|
+
findings.push({
|
|
289
|
+
kind: 'unbalanced-fence',
|
|
290
|
+
file: relative,
|
|
291
|
+
line: unbalancedFence.line,
|
|
292
|
+
path: '-',
|
|
293
|
+
annotation: null,
|
|
294
|
+
message:
|
|
295
|
+
'a code fence opens here and never closes — every line below it is invisible to this ' +
|
|
296
|
+
'check (a fence closes only with the same character, at least as long, and no info ' +
|
|
297
|
+
'string); close it or remove the stray marker',
|
|
298
|
+
severity: 'fail',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
// Which lines carry a citation of their own. A marker that sits on such a
|
|
302
|
+
// line is that citation's OWN exemption and must not also reach downward.
|
|
303
|
+
const citedLines = new Set(citations.map((c) => c.line));
|
|
304
|
+
|
|
305
|
+
// A malformed marker is reported wherever it stands, even with nothing to
|
|
306
|
+
// exempt: it reads as an exemption and grants none.
|
|
307
|
+
for (const [line, annotation] of annotations) {
|
|
308
|
+
if (annotation.ok) continue;
|
|
309
|
+
findings.push({
|
|
310
|
+
kind: 'bad-annotation',
|
|
311
|
+
file: relative,
|
|
312
|
+
line,
|
|
313
|
+
path: '-',
|
|
314
|
+
annotation: annotation.raw,
|
|
315
|
+
message:
|
|
316
|
+
`malformed marker \`${annotation.raw}\` — expected \`path-check: planned #<iid>\`, ` +
|
|
317
|
+
'`path-check: historical` or `path-check: example`',
|
|
318
|
+
severity: 'fail',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const citation of citations) {
|
|
323
|
+
summary.citations += 1;
|
|
324
|
+
if (existsSync(path.join(pluginRoot, citation.path))) {
|
|
325
|
+
summary.existing += 1;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
// An illustrative placeholder name needs no marker — see
|
|
329
|
+
// `isPlaceholderCitation`'s docblock for the closed fragment list and
|
|
330
|
+
// its named ceiling.
|
|
331
|
+
if (isPlaceholderCitation(citation.path)) {
|
|
332
|
+
summary.placeholders += 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// Same line, or the line immediately above — and the line above only
|
|
336
|
+
// when it carries NO citation itself. A `citation + marker` line is one
|
|
337
|
+
// self-contained exemption; letting it also cover the next line silently
|
|
338
|
+
// exempts a dead citation nobody ever annotated (the live shape at
|
|
339
|
+
// skills/wave-executor/wave-loop.md's `example` marker).
|
|
340
|
+
const above = citedLines.has(citation.line - 1)
|
|
341
|
+
? undefined
|
|
342
|
+
: annotations.get(citation.line - 1);
|
|
343
|
+
const marker = annotations.get(citation.line) ?? above;
|
|
344
|
+
if (marker?.ok) {
|
|
345
|
+
summary.annotated += 1;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (marker && !marker.ok) continue; // already reported as bad-annotation
|
|
349
|
+
|
|
350
|
+
// `.mjs` is blocking exactly as before this module grew a `.sh` half.
|
|
351
|
+
// `.sh` is advisory (`warn`) unless the caller opted into `strictSh`.
|
|
352
|
+
const isSh = path.extname(citation.path) === '.sh';
|
|
353
|
+
const severity = isSh && !strictSh ? 'warn' : 'fail';
|
|
354
|
+
if (severity === 'warn') summary.warnings += 1;
|
|
355
|
+
findings.push({
|
|
356
|
+
kind: 'missing-path',
|
|
357
|
+
file: relative,
|
|
358
|
+
line: citation.line,
|
|
359
|
+
path: citation.path,
|
|
360
|
+
annotation: null,
|
|
361
|
+
message:
|
|
362
|
+
(isSh
|
|
363
|
+
? severity === 'warn'
|
|
364
|
+
? `\`${citation.path}\` does not exist (advisory — .sh citations do not block ` +
|
|
365
|
+
'validate-plugin until re-run with --strict-sh; see #1187) — '
|
|
366
|
+
: `\`${citation.path}\` does not exist (--strict-sh) — `
|
|
367
|
+
: `\`${citation.path}\` does not exist — `) +
|
|
368
|
+
'create it, fix the path, or annotate the citation with ' +
|
|
369
|
+
'`<!-- path-check: planned #<iid> | historical | example -->` on this line or the ' +
|
|
370
|
+
'line directly above',
|
|
371
|
+
severity,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
377
|
+
summary.findings = findings.length;
|
|
378
|
+
const blocking = findings.filter((f) => f.severity !== 'warn');
|
|
379
|
+
return { ok: blocking.length === 0, summary, findings, toolError: false };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Run the human-readable validator CLI.
|
|
384
|
+
*
|
|
385
|
+
* @param {string} pluginRoot absolute plugin root
|
|
386
|
+
* @returns {number} 0 = clean, 1 = findings, 2 = tool error
|
|
387
|
+
*/
|
|
388
|
+
export function runCheckSkillScriptPaths(pluginRoot, { strictSh = false } = {}) {
|
|
389
|
+
console.log('--- Check: scripts/*.mjs (+ *.sh) paths cited in skills/commands/agents exist ---');
|
|
390
|
+
const inspection = scanSkillScriptPaths({ pluginRoot, strictSh });
|
|
391
|
+
|
|
392
|
+
for (const item of inspection.findings) {
|
|
393
|
+
// A `warn`-severity finding (a `.sh` citation, non-strict mode) is
|
|
394
|
+
// reported for visibility but must NOT print as ` FAIL:` — the
|
|
395
|
+
// validate-plugin aggregator counts failures by that exact 2-space
|
|
396
|
+
// prefix (`scripts/validate-plugin.mjs`'s `runCheck()`), so a `WARN:`
|
|
397
|
+
// line is how this check stays advisory end-to-end.
|
|
398
|
+
const label = item.severity === 'warn' ? 'WARN' : 'FAIL';
|
|
399
|
+
console.log(` ${label}: [${item.kind}] ${item.file}:${item.line} ${item.path} — ${item.message}`);
|
|
400
|
+
}
|
|
401
|
+
if (inspection.toolError) {
|
|
402
|
+
console.log('');
|
|
403
|
+
console.log(`Results: 0 passed, ${inspection.findings.length} failed`);
|
|
404
|
+
return 2;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const s = inspection.summary;
|
|
408
|
+
const blockingCount = inspection.findings.filter((f) => f.severity !== 'warn').length;
|
|
409
|
+
if (inspection.ok) {
|
|
410
|
+
console.log(
|
|
411
|
+
` PASS: ${s.citations} script citation(s) in ${s.filesScanned} doc file(s) — ` +
|
|
412
|
+
`${s.existing} exist, ${s.annotated} annotated as deliberately absent, ` +
|
|
413
|
+
`${s.placeholders} placeholder(s)` +
|
|
414
|
+
(s.warnings > 0 ? `, ${s.warnings} advisory .sh warning(s) (see --strict-sh)` : ''),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
console.log('');
|
|
418
|
+
console.log(`Results: ${inspection.ok ? 1 : 0} passed, ${blockingCount} failed`);
|
|
419
|
+
return inspection.ok ? 0 : 1;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
|
|
423
|
+
if (isMain) {
|
|
424
|
+
const strictSh = process.argv.includes('--strict-sh');
|
|
425
|
+
const args = process.argv.slice(2).filter((arg) => arg !== '--json' && arg !== '--strict-sh');
|
|
426
|
+
const root = path.resolve(args[0] || process.cwd());
|
|
427
|
+
if (process.argv.includes('--json')) {
|
|
428
|
+
const inspection = scanSkillScriptPaths({ pluginRoot: root, strictSh });
|
|
429
|
+
// Write, THEN set the exit code — `process.exit()` after a large print
|
|
430
|
+
// discards whatever is still queued on an async stdout pipe.
|
|
431
|
+
process.stdout.write(`${JSON.stringify(inspection, null, 2)}\n`);
|
|
432
|
+
process.exitCode = inspection.toolError ? 2 : inspection.ok ? 0 : 1;
|
|
433
|
+
} else {
|
|
434
|
+
process.exitCode = runCheckSkillScriptPaths(root, { strictSh });
|
|
435
|
+
}
|
|
436
|
+
}
|