session-orchestrator 3.16.0 → 3.19.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/rules/030-wave-execution.mdc +17 -1
- package/CHANGELOG.md +130 -412
- package/README.md +17 -12
- package/SECURITY.md +190 -27
- package/agents/AGENTS.md +20 -3
- package/agents/code-implementer.md +6 -6
- package/agents/db-specialist.md +1 -1
- package/agents/qa-strategist.md +31 -6
- package/agents/schemas/qa-strategist.schema.json +27 -0
- package/agents/schemas/test-writer.schema.json +60 -2
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +1 -1
- package/agents/test-writer.md +29 -10
- package/agents/ui-developer.md +1 -1
- package/commands/contract-version-bump.md +28 -0
- package/commands/portfolio.md +1 -1
- package/docs/README.md +2 -1
- package/docs/USER-GUIDE.md +8 -3
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +444 -0
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +306 -6
- package/docs/session-config-template.md +66 -3
- package/docs/telemetry/telemetry-claims.md +204 -0
- package/docs/telemetry.md +158 -0
- package/hooks/_lib/guard-source-loader.mjs +467 -0
- package/hooks/_lib/lock-bootstrap.mjs +21 -0
- package/hooks/_lib/vcs-create-matcher.mjs +119 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/enforce-commands.mjs +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +11 -2
- package/hooks/hooks-pi.json +10 -0
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +178 -18
- package/hooks/on-session-start.mjs +23 -0
- package/hooks/post-bash-write-verify.mjs +977 -0
- package/hooks/post-subagent-discovery-validator.mjs +256 -41
- package/hooks/pre-bash-destructive-guard.mjs +525 -160
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/skill-invocation-telemetry.mjs +109 -10
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +16 -3
- package/pi/prompts/contract-version-bump.md +12 -0
- package/rules/README.md +32 -0
- package/scripts/archive-closed-prds.mjs +12 -22
- package/scripts/autopilot-multi.mjs +103 -20
- package/scripts/backfill-abandoned-sessions.mjs +160 -4
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/compute-grounding-injection.sh +18 -3
- package/scripts/dialectic-deriver.mjs +7 -2
- package/scripts/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- package/scripts/lib/auto-dialectic.mjs +11 -2
- package/scripts/lib/auto-dream.mjs +16 -5
- package/scripts/lib/autopilot/mr-draft.mjs +31 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
- package/scripts/lib/backlog-scan.mjs +39 -6
- package/scripts/lib/blocked-commands-policy.mjs +340 -0
- package/scripts/lib/build-live-signals.mjs +7 -4
- package/scripts/lib/ci-status-banner.mjs +75 -12
- package/scripts/lib/claude-md-budget-lint.mjs +283 -34
- package/scripts/lib/command-blocker.mjs +1013 -58
- package/scripts/lib/config/config-protection.mjs +2 -1
- package/scripts/lib/config/context-coverage.mjs +82 -0
- package/scripts/lib/config/drift-check.mjs +9 -1
- package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
- package/scripts/lib/config/issue-budget.mjs +123 -0
- package/scripts/lib/config/moc-staleness.mjs +98 -0
- package/scripts/lib/config/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config/worktree-orphans.mjs +138 -0
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +32 -0
- package/scripts/lib/context-coverage-banner.mjs +223 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +136 -30
- package/scripts/lib/dispatcher/rank.mjs +22 -8
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
- package/scripts/lib/eval/engine.mjs +47 -5
- package/scripts/lib/events.mjs +59 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +5 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +54 -7
- package/scripts/lib/gates/gate-full.mjs +15 -3
- package/scripts/lib/gates/gate-helpers.mjs +132 -6
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
- package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
- package/scripts/lib/handover-gate.mjs +7 -3
- package/scripts/lib/harness-audit/categories/category4.mjs +22 -5
- package/scripts/lib/instruction-budget-guard.mjs +402 -51
- package/scripts/lib/io.mjs +345 -10
- package/scripts/lib/issue-budget.mjs +269 -0
- package/scripts/lib/issue-close-strip-labels.mjs +39 -9
- package/scripts/lib/label-scope.mjs +47 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/lock-reaper.mjs +1 -2
- package/scripts/lib/memory-proposals/schema.mjs +36 -1
- package/scripts/lib/moc-staleness-banner.mjs +267 -0
- package/scripts/lib/peer-discovery.mjs +645 -0
- package/scripts/lib/pi-hook-bridge.mjs +146 -17
- package/scripts/lib/product-repo-detect.mjs +9 -8
- package/scripts/lib/project-hygiene.mjs +432 -0
- package/scripts/lib/quality-gate.mjs +167 -0
- package/scripts/lib/recommendations-v0.mjs +1 -1
- package/scripts/lib/reconcile/eligibility.mjs +1 -1
- package/scripts/lib/reconcile/emitter.mjs +23 -4
- package/scripts/lib/reconcile/engine.mjs +147 -39
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
- package/scripts/lib/resource-probe/evaluate.mjs +70 -4
- package/scripts/lib/resource-probe.mjs +19 -0
- package/scripts/lib/rule-loader.mjs +6 -0
- package/scripts/lib/scope-baseline.mjs +564 -0
- package/scripts/lib/scope-gate.mjs +399 -98
- package/scripts/lib/session-close-backfill.mjs +61 -6
- package/scripts/lib/session-end/phase-skip.mjs +1 -0
- package/scripts/lib/session-end/worktree-orphan-sweep.mjs +252 -0
- package/scripts/lib/session-id.mjs +221 -41
- package/scripts/lib/session-lock.mjs +304 -6
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/filters.mjs +88 -0
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/sessions-integrity-banner.mjs +294 -0
- package/scripts/lib/sessions-staleness-banner.mjs +121 -12
- package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
- package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
- package/scripts/lib/skill-health/join.mjs +35 -9
- package/scripts/lib/spiral-carryover.mjs +142 -30
- package/scripts/lib/state-md/mission-status.mjs +53 -3
- package/scripts/lib/subagents-schema.mjs +43 -9
- package/scripts/lib/telemetry/anon-id.mjs +141 -0
- package/scripts/lib/telemetry/consent.mjs +299 -0
- package/scripts/lib/telemetry/paths.mjs +27 -0
- package/scripts/lib/telemetry/queue.mjs +287 -0
- package/scripts/lib/telemetry/schema.mjs +384 -0
- package/scripts/lib/telemetry/sync.mjs +312 -0
- package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
- package/scripts/lib/tests-src-ratio.mjs +484 -0
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-rules.mjs +217 -35
- package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate-vendored-rules.mjs +10 -2
- package/scripts/lib/vault-archive.mjs +17 -2
- package/scripts/lib/vault-backfill/glab.mjs +8 -0
- package/scripts/lib/vault-mirror/process.mjs +30 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
- package/scripts/lib/vault-status/board-writer.mjs +63 -5
- package/scripts/lib/vault-status/narrative-mirror.mjs +13 -7
- package/scripts/lib/vcs-repo-spec.mjs +362 -0
- package/scripts/lib/wave-resource-gate.mjs +115 -11
- package/scripts/lib/worktree/listing.mjs +44 -7
- package/scripts/mcp-server.sh +32 -6
- package/scripts/measure-context-overhead.sh +151 -0
- package/scripts/memory-propose.mjs +72 -9
- package/scripts/print-applicable-rules.mjs +51 -12
- package/scripts/release.mjs +534 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/telemetry.mjs +250 -0
- package/scripts/validate-wave-scope.mjs +182 -17
- package/scripts/vault-integration-watcher.mjs +32 -10
- package/skills/_shared/config-reading.md +2 -2
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +145 -28
- package/skills/contract-version-bump/SKILL.md +219 -0
- package/skills/discovery/SKILL.md +4 -4
- package/skills/discovery/issue-templates.md +11 -11
- package/skills/discovery/probes-audit.md +1 -1
- package/skills/discovery/probes-feature.md +1 -1
- package/skills/discovery/probes-session.md +26 -5
- package/skills/ecosystem-health/SKILL.md +1 -1
- package/skills/ecosystem-health/wizard.md +4 -4
- package/skills/evolve/SKILL.md +1 -0
- package/skills/gitlab-ops/SKILL.md +20 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/hook-development/SKILL.md +1 -1
- package/skills/mode-selector/SKILL.md +1 -1
- package/skills/npm-publish/SKILL.md +97 -0
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/mode-feature.md +4 -4
- package/skills/plan/mode-new.md +10 -10
- package/skills/plan/mode-retro.md +1 -1
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +108 -14
- package/skills/session-end/discovery-scan.md +4 -2
- package/skills/session-end/drift-operations.md +4 -4
- package/skills/session-end/metrics-collection.md +13 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +2 -1
- package/skills/session-end/plan-verification.md +5 -2
- package/skills/session-end/vault-operations.md +1 -1
- package/skills/session-end/verification-checklist.md +1 -1
- package/skills/session-plan/SKILL.md +6 -2
- package/skills/session-plan/wave-template.md +2 -0
- package/skills/session-start/SKILL.md +149 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/SKILL.md +1 -1
- package/skills/vault-sync/package-lock.json +3 -3
- package/skills/vault-sync/validator.mjs +228 -40
- package/skills/wave-executor/SKILL.md +5 -2
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +163 -10
- package/templates/_shared/loop.md +4 -4
|
@@ -451,6 +451,62 @@ if (existsSync(agentsDir)) {
|
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
// ============================================================================
|
|
455
|
+
// Check 10: PSA-007 git-write ban on repo-write agents (#724).
|
|
456
|
+
// Every agent that can write the repo (tools list contains an *Edit tool AND
|
|
457
|
+
// Write) MUST carry the subagent git-write prohibition in its BODY: a
|
|
458
|
+
// `PSA-007` reference plus the explicit `git stash` ban line. The git index
|
|
459
|
+
// and stash are shared working-copy resources — a dispatched agent that
|
|
460
|
+
// stages or stashes races its siblings and can silently discard their
|
|
461
|
+
// work-in-progress (.claude/rules/parallel-sessions.md § PSA-007).
|
|
462
|
+
// Structural invariant, so it lives in the gate rather than in a test
|
|
463
|
+
// (.claude/rules/test-value.md § TV-005). Bug it catches: a NEW repo-write
|
|
464
|
+
// agent ships with no ban — exactly the docs-writer gap #724 closed.
|
|
465
|
+
// Agents are discovered dynamically; no hardcoded roster to drift.
|
|
466
|
+
// ============================================================================
|
|
467
|
+
console.log('');
|
|
468
|
+
console.log('--- Check 10: PSA-007 git-write ban (repo-write agents) ---');
|
|
469
|
+
|
|
470
|
+
if (existsSync(agentsDir)) {
|
|
471
|
+
const psaMdFiles = readdirSync(agentsDir).filter(isAgentDefFile);
|
|
472
|
+
let repoWriteCount = 0;
|
|
473
|
+
|
|
474
|
+
for (const agentFile of psaMdFiles) {
|
|
475
|
+
const filePath = join(agentsDir, agentFile);
|
|
476
|
+
const content = readFileSync(filePath, 'utf8');
|
|
477
|
+
const fm = extractFrontmatter(content);
|
|
478
|
+
if (!fm) continue; // already caught by Check 6
|
|
479
|
+
|
|
480
|
+
const toolsArray = parseToolsValue(getField(fm, 'tools'));
|
|
481
|
+
// *Edit covers Edit / MultiEdit / NotebookEdit — all repo-mutating.
|
|
482
|
+
const canEdit = toolsArray.some((t) => /Edit$/.test(t));
|
|
483
|
+
if (!canEdit || !toolsArray.includes('Write')) continue;
|
|
484
|
+
|
|
485
|
+
repoWriteCount++;
|
|
486
|
+
|
|
487
|
+
// Body only: a `description:` that happens to mention PSA-007 must not
|
|
488
|
+
// satisfy the ban — the instruction has to reach the agent's rules.
|
|
489
|
+
const body = content.slice(content.indexOf(`\n---`, 3) + 4);
|
|
490
|
+
const missing = [];
|
|
491
|
+
if (!body.includes('PSA-007')) missing.push('a PSA-007 reference');
|
|
492
|
+
if (!body.includes('git stash')) missing.push('the `git stash` ban line');
|
|
493
|
+
|
|
494
|
+
if (missing.length > 0) {
|
|
495
|
+
fail(
|
|
496
|
+
`${agentFile}: repo-write agent (Edit+Write) is missing ${missing.join(' and ')} ` +
|
|
497
|
+
'— see .claude/rules/parallel-sessions.md § PSA-007 and agents/AGENTS.md § Authoring Convention',
|
|
498
|
+
);
|
|
499
|
+
} else {
|
|
500
|
+
pass(`${agentFile}: PSA-007 git-write ban present`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (repoWriteCount === 0) {
|
|
505
|
+
// No repo-write agents in this plugin root — nothing to enforce.
|
|
506
|
+
console.log(' (no repo-write agents found — check skipped)');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
454
510
|
console.log('');
|
|
455
511
|
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
456
512
|
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// scripts/lib/validate/check-hooks-symmetry.mjs
|
|
3
|
-
// Verify event-key + handler-file symmetry across
|
|
3
|
+
// Verify event-key + handler-file + per-event handler-set symmetry across
|
|
4
|
+
// hooks/{hooks,hooks-codex,hooks-cursor,hooks-pi}.json (Check 6: #942)
|
|
5
|
+
// Check 6 projects platform-native event namespaces onto the logical Claude
|
|
6
|
+
// events first, then refuses to report parity for any counterpart event that
|
|
7
|
+
// lands on no hooks.json event — whether that is one dropped projection or the
|
|
8
|
+
// whole namespace (a vacuum-true PASS — see Check 6).
|
|
4
9
|
// Exit 0 = all checks pass, 1 = any check fails
|
|
5
10
|
// Stdout: ' PASS: ...' / ' FAIL: ...' lines + 'Results: N passed, M failed'
|
|
6
11
|
|
|
7
12
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
8
13
|
import { join } from 'node:path';
|
|
9
14
|
|
|
15
|
+
// The pi projection is IMPORTED, not copied: pi-hook-bridge.mjs is the runtime
|
|
16
|
+
// that actually rewrites `hook_event_name` for every Pi event, so it is the one
|
|
17
|
+
// source of truth for which pi event becomes which Claude event (#953). A local
|
|
18
|
+
// literal here was a second copy of the same 5 pairs — it could drift silently,
|
|
19
|
+
// and the validator would then certify a projection the runtime does not use.
|
|
20
|
+
// Deliberately NOT the same for `cursorEventMap` below: nothing at runtime
|
|
21
|
+
// projects Cursor events (Cursor calls the handlers directly), so the
|
|
22
|
+
// validator's copy is the only witness there. Even if a runtime source existed,
|
|
23
|
+
// reading Check 3's expectation from the artefact it checks would make the
|
|
24
|
+
// check self-certifying — the "undocumented pi-native event" test at
|
|
25
|
+
// tests/scripts/check-hooks-symmetry.test.mjs would become unreachable. Keep
|
|
26
|
+
// cursorEventMap validator-owned; do not "helpfully" collapse it too.
|
|
27
|
+
import { PI_TO_CANONICAL_EVENT } from '../pi-hook-bridge.mjs';
|
|
28
|
+
|
|
10
29
|
const PLUGIN_ROOT = process.argv[2];
|
|
11
30
|
if (!PLUGIN_ROOT) {
|
|
12
31
|
console.error('Usage: check-hooks-symmetry.mjs <plugin-root>');
|
|
@@ -25,12 +44,125 @@ const DOCUMENTED_ASYMMETRIES = {
|
|
|
25
44
|
// Claude/Codex events with no Pi-native v1 mapping yet.
|
|
26
45
|
piMissingFromMain: ['PostToolUseFailure', 'PostToolBatch', 'SubagentStart', 'SubagentStop', 'CwdChanged'],
|
|
27
46
|
// Pi-native extension events that map onto Claude/Codex hook events.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
// Bound to the runtime constant (see the import comment): the projection this
|
|
48
|
+
// validator checks hooks-pi.json against is the SAME object pi-hook-bridge.mjs
|
|
49
|
+
// applies to live payloads, so the two cannot drift apart.
|
|
50
|
+
piEventMap: PI_TO_CANONICAL_EVENT,
|
|
51
|
+
// Cursor-IDE-native events projected onto the logical Claude events they
|
|
52
|
+
// correspond to. WITHOUT this projection Check 6 compared a FOREIGN event
|
|
53
|
+
// namespace against hooks.json: not one key matched, the per-event loop
|
|
54
|
+
// `continue`d on every iteration, and hooks-cursor.json reported
|
|
55
|
+
// "handler sets match hooks.json (documented asymmetries: 0)" while missing
|
|
56
|
+
// 20 of 22 handlers — a vacuum-true PASS. Check 6's projection guard now
|
|
57
|
+
// fails closed PER declared event (#946), so dropping a single entry from
|
|
58
|
+
// this map is caught too, not only the loss of every projection: the first
|
|
59
|
+
// version tested `sharedEvents.length === 0`, under which removing just
|
|
60
|
+
// `afterFileEdit` left the run green and merely moved cursor's documented-
|
|
61
|
+
// asymmetry count from 12 to 8.
|
|
62
|
+
// afterFileEdit fires AFTER the edit (hooks-cursor.json `note`,
|
|
63
|
+
// docs/cursor-setup.md) → it projects onto PostToolUse, never PreToolUse.
|
|
64
|
+
cursorEventMap: {
|
|
65
|
+
beforeShellExecution: 'PreToolUse',
|
|
66
|
+
afterFileEdit: 'PostToolUse',
|
|
67
|
+
},
|
|
68
|
+
// Check 6 (#942): handlers wired on a Claude event but intentionally absent
|
|
69
|
+
// from the SAME logical event on a counterpart manifest. Checks 1-3 compare
|
|
70
|
+
// event KEYS and Check 4 handler EXISTENCE — a handler wired on only one
|
|
71
|
+
// platform was structurally invisible to all four (the #942 blind spot:
|
|
72
|
+
// post-bash-write-verify.mjs shipped Claude-only and nothing flagged it).
|
|
73
|
+
// A handler missing on a shared event and NOT listed here → FAIL.
|
|
74
|
+
handlerAsymmetries: {
|
|
75
|
+
codex: {
|
|
76
|
+
// Codex 0.144.4 has no payload adapter (docs/codex-setup.md § Hook
|
|
77
|
+
// Surface and Trust): Edit/Write handlers expect Claude payload shapes,
|
|
78
|
+
// Bash guards expect tool_name === 'Bash'. Wiring them would be a silent
|
|
79
|
+
// no-op, not enforcement (#919-P2 class) — "pretending the payloads are
|
|
80
|
+
// compatible would create false enforcement".
|
|
81
|
+
PreToolUse: [
|
|
82
|
+
'skill-invocation-telemetry.mjs',
|
|
83
|
+
'enforce-scope.mjs',
|
|
84
|
+
'config-protection.mjs',
|
|
85
|
+
'pre-bash-destructive-guard.mjs',
|
|
86
|
+
'pre-bash-staging-fence.mjs',
|
|
87
|
+
'pre-bash-memory-propose-audit.mjs',
|
|
88
|
+
// pre-bash-sessions-ledger-guard (#958): same Bash-payload dependency
|
|
89
|
+
// as the guards above — it reads tool_name === 'Bash' and
|
|
90
|
+
// tool_input.command, neither of which any Codex bridge delivers
|
|
91
|
+
// (measured 2026-07-31: `grep -rn "tool_name" scripts/lib/codex/*.mjs`
|
|
92
|
+
// → 0 matches, exit 1). A manifest entry would be a silent no-op, not
|
|
93
|
+
// enforcement — #919-P2 class. Gap registered, not faked.
|
|
94
|
+
'pre-bash-sessions-ledger-guard.mjs',
|
|
95
|
+
'pre-bash-templates-first.mjs',
|
|
96
|
+
'pre-bash-issue-budget.mjs',
|
|
97
|
+
'enforce-commands.mjs',
|
|
98
|
+
],
|
|
99
|
+
// post-bash-write-verify (#942): needs tool_name === 'Bash', which no
|
|
100
|
+
// Codex bridge delivers — documented exception until an adapter exists.
|
|
101
|
+
// Measured 2026-07-31: `grep -rn "tool_name" scripts/lib/codex/*.mjs`
|
|
102
|
+
// → 0 matches (exit 1), so a manifest entry here would be a silent
|
|
103
|
+
// no-op, not enforcement. Same #919-P2 class as the PreToolUse block.
|
|
104
|
+
PostToolUse: [
|
|
105
|
+
'post-edit-validate.mjs',
|
|
106
|
+
'post-tooluse-frontend-slop.mjs',
|
|
107
|
+
'post-bash-write-verify.mjs',
|
|
108
|
+
],
|
|
109
|
+
SubagentStart: ['subagent-telemetry.mjs'],
|
|
110
|
+
SubagentStop: ['subagent-telemetry.mjs', 'post-subagent-discovery-validator.mjs'],
|
|
111
|
+
},
|
|
112
|
+
pi: {
|
|
113
|
+
// skill-invocation-telemetry: Pi has no Skill tool (pi-hook-bridge
|
|
114
|
+
// TOOL_NAME_MAP) — the matcher can never fire.
|
|
115
|
+
// pre-bash-templates-first + pre-bash-issue-budget (#946): status-quo
|
|
116
|
+
// gaps predating #942. Operator decision 2026-07-31 — NOT ported, gap
|
|
117
|
+
// registered instead; #946 tracks the port-or-justify follow-up.
|
|
118
|
+
// pre-bash-sessions-ledger-guard (#958): NOT ported. Pi has a payload
|
|
119
|
+
// adapter (pi-hook-bridge.mjs), so unlike codex/cursor this one is
|
|
120
|
+
// portable — but wiring it is a separate decision with its own test
|
|
121
|
+
// surface, and the operator's 2026-07-31 rule is that a gap gets
|
|
122
|
+
// registered rather than faked. #958 tracks the port-or-justify.
|
|
123
|
+
PreToolUse: [
|
|
124
|
+
'skill-invocation-telemetry.mjs',
|
|
125
|
+
'pre-bash-sessions-ledger-guard.mjs', // #958
|
|
126
|
+
'pre-bash-templates-first.mjs', // #946
|
|
127
|
+
'pre-bash-issue-budget.mjs', // #946
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
// Cursor is NOT wired (#919) and will not be: hooks-cursor.json is a
|
|
131
|
+
// mapping REFERENCE, not live enforcement. Every handler expects Claude
|
|
132
|
+
// Code payload shapes (tool_name === 'Bash', tool_input.command) and emits
|
|
133
|
+
// a Claude PreToolUse envelope; fed a Cursor payload enforce-commands.mjs
|
|
134
|
+
// short-circuits at gate G1 and writes 0 bytes to stdout AND stderr with
|
|
135
|
+
// exit 0, so the harness sees no decision and the command runs. Cursor
|
|
136
|
+
// needs an input/output adapter like scripts/lib/pi-hook-bridge.mjs; none
|
|
137
|
+
// exists. Operator decision 2026-07-31 — gap registered, not closed:
|
|
138
|
+
// #919 (Cursor no-op) tracks the adapter, #946 the allowlist-provenance
|
|
139
|
+
// rule that every entry here names its issue. These entries exist so the
|
|
140
|
+
// gap is MACHINE-readable (Check 6 counts them) rather than prose-only.
|
|
141
|
+
cursor: {
|
|
142
|
+
// #919: beforeShellExecution → PreToolUse. Only enforce-commands.mjs is
|
|
143
|
+
// mapped at all; the other eight PreToolUse handlers have no Cursor
|
|
144
|
+
// mapping whatsoever.
|
|
145
|
+
PreToolUse: [
|
|
146
|
+
'skill-invocation-telemetry.mjs', // #919
|
|
147
|
+
'enforce-scope.mjs', // #919
|
|
148
|
+
'config-protection.mjs', // #919
|
|
149
|
+
'pre-bash-destructive-guard.mjs', // #919
|
|
150
|
+
'pre-bash-staging-fence.mjs', // #919
|
|
151
|
+
'pre-bash-memory-propose-audit.mjs', // #919
|
|
152
|
+
'pre-bash-sessions-ledger-guard.mjs',// #919 (#958 — needs tool_name === 'Bash')
|
|
153
|
+
'pre-bash-templates-first.mjs', // #919
|
|
154
|
+
'pre-bash-issue-budget.mjs', // #919
|
|
155
|
+
],
|
|
156
|
+
// #919: afterFileEdit → PostToolUse. Cursor maps enforce-scope.mjs here
|
|
157
|
+
// (post-hoc warning only); none of Claude's four PostToolUse handlers
|
|
158
|
+
// has a Cursor mapping.
|
|
159
|
+
PostToolUse: [
|
|
160
|
+
'post-edit-validate.mjs', // #919
|
|
161
|
+
'post-tooluse-frontend-slop.mjs', // #919
|
|
162
|
+
'post-bash-write-verify.mjs', // #919 (#942 class — needs tool_name === 'Bash')
|
|
163
|
+
'loop-guard.mjs', // #919
|
|
164
|
+
],
|
|
165
|
+
},
|
|
34
166
|
},
|
|
35
167
|
};
|
|
36
168
|
|
|
@@ -163,9 +295,10 @@ if (piJson) {
|
|
|
163
295
|
// Handles two shapes:
|
|
164
296
|
// Claude/Codex: hooks[event] = Array<{ hooks: Array<{ command: "node \"$CLAUDE_PLUGIN_ROOT/hooks/foo.mjs\"" }> }>
|
|
165
297
|
// Cursor: hooks[event] = { script: "hooks/foo.mjs" }
|
|
166
|
-
function
|
|
167
|
-
const
|
|
168
|
-
for (const event of Object.
|
|
298
|
+
function extractHandlersByEvent(json) {
|
|
299
|
+
const byEvent = {};
|
|
300
|
+
for (const [eventName, event] of Object.entries(json.hooks || {})) {
|
|
301
|
+
const handlers = new Set();
|
|
169
302
|
const matchers = Array.isArray(event) ? event : [event];
|
|
170
303
|
for (const m of matchers) {
|
|
171
304
|
// Cursor shape: script field at matcher level
|
|
@@ -181,6 +314,15 @@ function extractHandlers(json) {
|
|
|
181
314
|
if (match) handlers.add(match[1]);
|
|
182
315
|
}
|
|
183
316
|
}
|
|
317
|
+
byEvent[eventName] = handlers;
|
|
318
|
+
}
|
|
319
|
+
return byEvent;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function extractHandlers(json) {
|
|
323
|
+
const handlers = new Set();
|
|
324
|
+
for (const set of Object.values(extractHandlersByEvent(json))) {
|
|
325
|
+
for (const h of set) handlers.add(h);
|
|
184
326
|
}
|
|
185
327
|
return handlers;
|
|
186
328
|
}
|
|
@@ -252,6 +394,98 @@ if (unreferenced.length === 0) {
|
|
|
252
394
|
pass(`hooks/ has ${unreferenced.length} unreferenced .mjs files (allowed; they may be library helpers): ${unreferenced.join(', ')}`);
|
|
253
395
|
}
|
|
254
396
|
|
|
397
|
+
// Step 9 (#942): per-event handler-SET parity, Claude → counterpart.
|
|
398
|
+
// Checks 1-3 compare event keys, Check 4 handler existence — neither sees a
|
|
399
|
+
// handler wired on one platform only. For every logical event the counterpart
|
|
400
|
+
// DECLARES (event-level absences stay governed by Checks 1-3), each Claude
|
|
401
|
+
// handler must be present on the counterpart or listed in
|
|
402
|
+
// DOCUMENTED_ASYMMETRIES.handlerAsymmetries. Undocumented gap → FAIL.
|
|
403
|
+
console.log('');
|
|
404
|
+
console.log('--- Check 6: per-event handler-set parity across manifests (#942) ---');
|
|
405
|
+
const claudeHandlersByEvent = extractHandlersByEvent(claudeJson);
|
|
406
|
+
|
|
407
|
+
// Pi and Cursor declare platform-native event names; project them onto the
|
|
408
|
+
// logical main events before comparing, or the whole comparison is vacuous.
|
|
409
|
+
// Native events with no mapping are carried through under their own name so
|
|
410
|
+
// the projection guard below still sees them as declared.
|
|
411
|
+
//
|
|
412
|
+
// The merge is a UNION, not an assignment: two native events may project onto
|
|
413
|
+
// the SAME logical event (a harness with `before_shell` AND `before_edit` both
|
|
414
|
+
// mapping to PreToolUse, or — reachable today via the identity fallback — a
|
|
415
|
+
// counterpart that declares the logical event alongside a native event that
|
|
416
|
+
// projects onto it). Assignment let the second write silently discard the
|
|
417
|
+
// first set, so its handlers resurfaced as a phantom "undocumented asymmetry"
|
|
418
|
+
// or vanished from the comparison entirely.
|
|
419
|
+
function handlersByMainEvent(json, eventMap) {
|
|
420
|
+
const byNativeEvent = extractHandlersByEvent(json);
|
|
421
|
+
const byMain = {};
|
|
422
|
+
for (const [nativeEvent, handlers] of Object.entries(byNativeEvent)) {
|
|
423
|
+
const target = eventMap[nativeEvent] || nativeEvent;
|
|
424
|
+
byMain[target] = new Set([...(byMain[target] ?? []), ...handlers]);
|
|
425
|
+
}
|
|
426
|
+
return byMain;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const HANDLER_PARITY_COUNTERPARTS = [
|
|
430
|
+
{ file: 'hooks-codex.json', key: 'codex', byEvent: codexJson ? extractHandlersByEvent(codexJson) : null },
|
|
431
|
+
{
|
|
432
|
+
file: 'hooks-cursor.json',
|
|
433
|
+
key: 'cursor',
|
|
434
|
+
byEvent: cursorJson ? handlersByMainEvent(cursorJson, DOCUMENTED_ASYMMETRIES.cursorEventMap) : null,
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
file: 'hooks-pi.json',
|
|
438
|
+
key: 'pi',
|
|
439
|
+
byEvent: piJson ? handlersByMainEvent(piJson, DOCUMENTED_ASYMMETRIES.piEventMap) : null,
|
|
440
|
+
},
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
for (const { file, key, byEvent } of HANDLER_PARITY_COUNTERPARTS) {
|
|
444
|
+
if (!byEvent) {
|
|
445
|
+
pass(`${file} absent (optional config) — handler parity skipped`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
// Projection guard: every event a counterpart declares must land on a
|
|
449
|
+
// logical event hooks.json also declares. An unprojected event makes the
|
|
450
|
+
// per-event loop below `continue` on the logical event it was supposed to
|
|
451
|
+
// cover, so those handlers are never compared and Check 6 still reports
|
|
452
|
+
// PASS — only the `documented asymmetries:` count moves. Losing ALL
|
|
453
|
+
// projections is how hooks-cursor.json passed with 20 of 22 handlers
|
|
454
|
+
// missing; losing ONE is the same blindness at smaller scale, so the guard
|
|
455
|
+
// fails closed per event, not only on the empty intersection (#946).
|
|
456
|
+
const declaredEvents = Object.keys(byEvent);
|
|
457
|
+
const sharedEvents = declaredEvents.filter((e) => Object.hasOwn(claudeHandlersByEvent, e));
|
|
458
|
+
const unprojectedEvents = declaredEvents.filter((e) => !Object.hasOwn(claudeHandlersByEvent, e));
|
|
459
|
+
if (declaredEvents.length > 0 && sharedEvents.length === 0) {
|
|
460
|
+
// Whole-namespace loss keeps its own message: nothing at all was compared.
|
|
461
|
+
fail(`${file} shares ZERO logical events with hooks.json after projection (declares: ${declaredEvents.join(', ')}) — handler parity would pass vacuously; add an event projection to DOCUMENTED_ASYMMETRIES`);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (unprojectedEvents.length > 0) {
|
|
465
|
+
fail(`${file} declares events with no logical counterpart in hooks.json after projection: ${unprojectedEvents.join(', ')} — handlers on the logical events they stand for are never compared; add a projection to DOCUMENTED_ASYMMETRIES.${key}EventMap`);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const allow = DOCUMENTED_ASYMMETRIES.handlerAsymmetries[key] || {};
|
|
470
|
+
const undocumented = [];
|
|
471
|
+
let documentedCount = 0;
|
|
472
|
+
for (const [event, claudeSet] of Object.entries(claudeHandlersByEvent)) {
|
|
473
|
+
const counterpartSet = byEvent[event];
|
|
474
|
+
if (!counterpartSet) continue; // event not declared on counterpart — Checks 1-3 govern
|
|
475
|
+
const allowed = new Set(allow[event] || []);
|
|
476
|
+
for (const handler of claudeSet) {
|
|
477
|
+
if (counterpartSet.has(handler)) continue;
|
|
478
|
+
if (allowed.has(handler)) { documentedCount++; continue; }
|
|
479
|
+
undocumented.push(`${event} → ${handler}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (undocumented.length === 0) {
|
|
483
|
+
pass(`${file} per-event handler sets match hooks.json (documented asymmetries: ${documentedCount})`);
|
|
484
|
+
} else {
|
|
485
|
+
fail(`${file} missing UNDOCUMENTED handlers on shared events: ${undocumented.join(', ')}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
255
489
|
// Final
|
|
256
490
|
console.log('');
|
|
257
491
|
console.log(`Results: ${passed} passed, ${failed} failed`);
|