@webpieces/ai-hook-rules 0.4.625 → 0.4.627
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/package.json +2 -2
- package/src/bin/hook-registration.d.ts +4 -3
- package/src/bin/hook-registration.js +4 -3
- package/src/bin/hook-registration.js.map +1 -1
- package/src/bin/shim-deny-reason.js +49 -8
- package/src/bin/shim-deny-reason.js.map +1 -1
- package/src/bin/shim.js +1 -1
- package/src/bin/shim.js.map +1 -1
- package/src/core/effective-tree.d.ts +32 -3
- package/src/core/effective-tree.js +34 -15
- package/src/core/effective-tree.js.map +1 -1
- package/src/core/l1-doc.js +6 -5
- package/src/core/l1-doc.js.map +1 -1
- package/src/core/l1-rows.js +2 -2
- package/src/core/l1-rows.js.map +1 -1
- package/src/core/version-sync.d.ts +19 -0
- package/src/core/version-sync.js +38 -12
- package/src/core/version-sync.js.map +1 -1
- package/templates/ai-hook.sh +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/ai-hook-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.627",
|
|
4
4
|
"description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/tooling/ai-hook-rules"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
28
|
+
"@webpieces/rules-config": "0.4.627"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
|
@@ -32,9 +32,10 @@
|
|
|
32
32
|
* Nothing is lost that was ever delivered — but the case relative registration WANTED to handle (a tree
|
|
33
33
|
* that genuinely needs a different @webpieces) is now DETECTED instead of silently mis-governed:
|
|
34
34
|
* `VersionSyncGuard` (L1 row 8) blocks when a worktree's pin disagrees with the main tree's, and
|
|
35
|
-
* prescribes either aligning the pins (same git hash → same tracked pin →
|
|
36
|
-
* or using a separate CLONE, which — unlike a worktree —
|
|
37
|
-
* own
|
|
35
|
+
* prescribes either aligning the pins (same git hash → same tracked pin → install in each tree that has
|
|
36
|
+
* a node_modules) or using a separate CLONE, which — unlike a worktree — gets its own GOVERNANCE. Note
|
|
37
|
+
* what that does NOT say: a worktree may perfectly well have its own node_modules, and usually does the
|
|
38
|
+
* moment anything installs in it. What it may not have is a DIFFERENT @webpieces version.
|
|
38
39
|
*
|
|
39
40
|
* ─── Why the registration is a DRIFT SURFACE, not just an install step ─────────────────────────────
|
|
40
41
|
* Nothing used to validate `.claude/settings.json` at all, so a settings file left on a superseded form
|
|
@@ -60,9 +60,10 @@ const managed_env_1 = require("./managed-env");
|
|
|
60
60
|
* Nothing is lost that was ever delivered — but the case relative registration WANTED to handle (a tree
|
|
61
61
|
* that genuinely needs a different @webpieces) is now DETECTED instead of silently mis-governed:
|
|
62
62
|
* `VersionSyncGuard` (L1 row 8) blocks when a worktree's pin disagrees with the main tree's, and
|
|
63
|
-
* prescribes either aligning the pins (same git hash → same tracked pin →
|
|
64
|
-
* or using a separate CLONE, which — unlike a worktree —
|
|
65
|
-
* own
|
|
63
|
+
* prescribes either aligning the pins (same git hash → same tracked pin → install in each tree that has
|
|
64
|
+
* a node_modules) or using a separate CLONE, which — unlike a worktree — gets its own GOVERNANCE. Note
|
|
65
|
+
* what that does NOT say: a worktree may perfectly well have its own node_modules, and usually does the
|
|
66
|
+
* moment anything installs in it. What it may not have is a DIFFERENT @webpieces version.
|
|
66
67
|
*
|
|
67
68
|
* ─── Why the registration is a DRIFT SURFACE, not just an install step ─────────────────────────────
|
|
68
69
|
* Nothing used to validate `.claude/settings.json` at all, so a settings file left on a superseded form
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hook-registration.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/hook-registration.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { toError } from '../core/to-error';\nimport { SHIM_MARKER, committedShimStale } from './shim';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\n\n/**\n * THE INSTALLED HOOK SURFACE — two hooks, both ABSOLUTE, and the ONE place their spelling is defined.\n *\n * ─── ONE GOVERNOR: the MAIN tree judges every tree ─────────────────────────────────────────────────\n *\n * H1 sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" wp-ai-guards-hook Write|…|Bash|Read\n * H2 sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" wp-ai-rules-hook Write|Edit|MultiEdit\n *\n * This REPLACES a three-hook form in which these two were RELATIVE (`sh \".claude/webpieces/ai-hook.sh\"`)\n * and a third hook, L-1 `guarantee-root.sh`, existed solely to guarantee that relative path resolved.\n *\n * ─── Why the relative experiment was reversed ──────────────────────────────────────────────────────\n * Relative registration was adopted so each git tree would be governed by \"its own release, binary and\n * pin\". MEASURED 2026-08-10: it never delivered that. A linked worktree has NO `node_modules`, so\n * ai-hook.sh's upward walk executes the PRIMARY's binary — `readlink -f` resolved a worktree agent's bin\n * to `<primary>/node_modules/@webpieces/ai-hook-rules`. A worktree ran its own SCRIPT and its own\n * CONFIG; it never ran its own release. Governance was already the primary's, in every tree, the whole\n * time.\n *\n * The price of that fiction was an entire guard layer. A relative path only resolves at a tree root, and\n * a hook that cannot resolve exits 127 — per the hooks reference a NON-BLOCKING error, i.e. a SILENT\n * UNGUARDED ALLOW. So L-1 had to deny every `cd` into a project subdirectory. That denial produced the\n * force-to-root bug class, the reaped-worktree `cd` prescription, and a measured hard deadlock: L-1 told\n * a worktree-isolated agent to `cd` to the primary clone, which the harness refuses for an isolated\n * agent, leaving it unable to stay or to follow the cure.\n *\n * An absolute path resolves from ANY cwd. So the launch guarantee is structural, L-1 has no job left and\n * is deleted, and `cd` into a subdirectory is simply allowed. What used to be true only by policing the\n * shell is now true by construction.\n *\n * ─── What replaces the property that was lost ──────────────────────────────────────────────────────\n * Nothing is lost that was ever delivered — but the case relative registration WANTED to handle (a tree\n * that genuinely needs a different @webpieces) is now DETECTED instead of silently mis-governed:\n * `VersionSyncGuard` (L1 row 8) blocks when a worktree's pin disagrees with the main tree's, and\n * prescribes either aligning the pins (same git hash → same tracked pin → one install in the main tree)\n * or using a separate CLONE, which — unlike a worktree — really does get its own node_modules and its\n * own governance.\n *\n * ─── Why the registration is a DRIFT SURFACE, not just an install step ─────────────────────────────\n * Nothing used to validate `.claude/settings.json` at all, so a settings file left on a superseded form\n * silently changed who governs. The installed surface is THREE things (ai-hook.sh, the registration, and\n * the managed `env` entry — see managed-env.ts), all three are compared against this release, and\n * `wp-upgrade-shim` regenerates all three. A cure that fixes two of three is worse than no cure, because\n * it reports success.\n */\n\n/**\n * The RETIRED L-1 hook's committed path. Named here, in the one module that must still recognise it, and\n * matched ONLY by isManagedCommand() so repairRegistration() can delete the stale entry. There is no\n * guarantee-root.ts any more — this literal is all that remains of it, deliberately.\n *\n * It is a one-way RECOGNISER, not a shim: nothing emits it, `expectedEntries()` never returns it, and a\n * settings file carrying it is reported STALE. Its only job is to make the retired entry findable so it\n * can be REMOVED.\n */\nexport const LEGACY_GUARANTEE_ROOT_MARKER = '.claude/webpieces/guarantee-root.sh';\n\n/**\n * When this recogniser may be deleted, as a value rather than a comment nobody re-reads.\n *\n * A removal-only migration is still dead weight once no consumer can be carrying the old shape. The\n * hazard of deleting it EARLY is severe and silent — repair would stop stripping the retired entry, and\n * a hook registered against a deleted file exits 127, which the Claude Code hooks reference defines as a\n * NON-BLOCKING error, i.e. every `cd` unjudged while the cure reports success. So it gets a stated date\n * and a test that fails once the date passes, instead of an intention.\n *\n * `legacy-marker-expiry.spec.ts` reads this and fails after it, which is the reminder.\n */\nexport const LEGACY_MARKER_REMOVE_AFTER = '2026-12-01';\n\n/** One PreToolUse hook entry as webpieces registers it. Data-only → a class, per CLAUDE.md. */\nexport class HookRegistrationEntry {\n constructor(\n readonly matcher: string,\n readonly command: string,\n ) {}\n\n sameAs(other: HookRegistrationEntry): boolean {\n return this.matcher === other.matcher && this.command === other.command;\n }\n}\n\n// webpieces-disable no-any-unknown -- settings.json is opaque consumer JSON\nexport interface HookCommand { type: string; command: string; }\nexport interface HookEntry { matcher: string; hooks: HookCommand[]; }\nexport interface ClaudeSettings {\n hooks?: { PreToolUse?: HookEntry[] };\n // Claude Code's settings `env` block: every key is exported into the environment of the session AND\n // of every subagent it spawns. That inheritance is precisely why webpieces pins its managed entry\n // here rather than in a shell profile — see managed-env.ts.\n env?: Record<string, string>;\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport const RULES_BIN = 'wp-ai-rules-hook';\nexport const GUARDS_BIN = 'wp-ai-guards-hook';\nexport const RULES_MATCHER = 'Write|Edit|MultiEdit';\n// Guards match Bash (git/PR guards), Write|Edit|MultiEdit (file-scoped guards) AND Read — Read carries\n// no guard, but the guards hook owns the per-invocation audit log, so matching Read records every file\n// the AI opens (log-and-allow fast path in hook-core.ts; a Read is never blocked).\nexport const GUARDS_MATCHER = 'Write|Edit|MultiEdit|Bash|Read';\n\n/**\n * The guard-hook command — `sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" <bin>`.\n *\n * ABSOLUTE. This replaced a RELATIVE spelling, and the reversal is the whole point of this release.\n *\n * The relative form existed to give each git tree its own release, binary and pin. **It never delivered\n * that.** Measured 2026-08-10: a linked worktree has no `node_modules`, so ai-hook.sh's upward walk\n * executes the PRIMARY's binary — `readlink -f` resolved a worktree agent's bin to\n * `<primary>/node_modules/@webpieces/ai-hook-rules`. A worktree ran its own SCRIPT and its own CONFIG,\n * never its own release. The property was fiction, and paying for it cost an entire guard layer: a\n * relative path only resolves at a tree root, so L-1 (guarantee-root.sh) had to deny every `cd` into a\n * project subdirectory, which produced the force-to-root bug class, the reaped-worktree `cd`\n * prescription, and a measured hard deadlock where L-1 told a worktree-isolated agent to `cd` to the\n * primary clone — which the harness refuses for an isolated agent.\n *\n * Absolute resolves from ANY cwd, so L-1 has no job left and is deleted. One governor: the MAIN tree\n * judges every tree, which is what was already happening via the borrowed binary — the design now says\n * so out loud, and `VersionSyncGuard` blocks the case where that is the wrong answer.\n *\n * Invoked via `sh <file>` rather than executed directly so a missing executable bit on the checked-in\n * shim (fresh clone, a filesystem that drops the bit, git core.fileMode quirks) can never break the hook\n * with a raw `Permission denied` on every tool call. Quoted to survive spaces in the path.\n */\n// webpieces-disable no-function-outside-class -- this module must load on a tree too broken to build a DI container (upgrade-shim.ts depends on that), so it is module-scope like its siblings shim.ts\nexport function shimCommand(bin: string): string {\n return `sh \"$CLAUDE_PROJECT_DIR/${SHIM_MARKER}\" ${bin}`;\n}\n\n/** The registration entry for one guard bin. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function guardHookEntry(bin: string): HookRegistrationEntry {\n return new HookRegistrationEntry(bin === RULES_BIN ? RULES_MATCHER : GUARDS_MATCHER, shimCommand(bin));\n}\n\n/**\n * True when this PreToolUse command is one webpieces owns — in ANY spelling it has ever shipped.\n *\n * THE SINGLE MOST IMPORTANT LINE IN THIS RELEASE. `LEGACY_GUARANTEE_ROOT_MARKER` is matched here and\n * NOWHERE else: it is how `repairRegistration()` finds and REMOVES the retired H1 entry from a settings\n * file written by an older release. Drop it and the repair silently leaves a live L-1 hook registered\n * against a file this release deletes — exit 127, which the Claude Code hooks reference defines as a\n * NON-BLOCKING error, so every `cd` goes unjudged while `wp-upgrade-shim` reports success and no drift\n * check can name it. Removal-only, never emitted: it appears in no `expectedEntries()` result, so it is\n * a one-way migration and not a second accepted spelling.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function isManagedCommand(command: string): boolean {\n return command.includes(SHIM_MARKER) || command.includes(LEGACY_GUARANTEE_ROOT_MARKER);\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nfunction preToolUse(settings: ClaudeSettings): readonly HookEntry[] {\n return settings.hooks?.PreToolUse ?? [];\n}\n\n/** Every webpieces-managed entry of one settings file, flattened to matcher + command pairs. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function managedEntries(settings: ClaudeSettings): readonly HookRegistrationEntry[] {\n const found: HookRegistrationEntry[] = [];\n for (const entry of preToolUse(settings)) {\n for (const hook of entry.hooks) {\n if (isManagedCommand(hook.command)) found.push(new HookRegistrationEntry(entry.matcher, hook.command));\n }\n }\n return found;\n}\n\n/**\n * Which guard bins this settings file registers, in installer order. A file registering NEITHER is not\n * a project (relative) install and is therefore never judged — a global/absolute install names the bin\n * path directly and carries no shim marker at all.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registeredBins(settings: ClaudeSettings): readonly string[] {\n const commands = managedEntries(settings).map((e: HookRegistrationEntry): string => e.command);\n return [GUARDS_BIN, RULES_BIN].filter((bin: string): boolean => commands.some((c: string): boolean => c.includes(bin)));\n}\n\n/**\n * The exact set of entries THIS RELEASE expects in a settings file that registers `bins`.\n *\n * ONE entry per registered bin, and nothing else. There used to be a third, `guarantee-root.sh`, added\n * whenever the GUARDS bin was present; it is retired, and a settings file still carrying it is STALE —\n * `repairRegistration()` removes it via isManagedCommand()'s legacy marker.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function expectedEntries(bins: readonly string[]): readonly HookRegistrationEntry[] {\n return bins.map((bin: string): HookRegistrationEntry => guardHookEntry(bin));\n}\n\n/**\n * True when a settings file registers webpieces hooks in a shape this release does not expect: a\n * RELATIVE shim command, a leftover guarantee-root entry, a stray duplicate, or a wrong matcher.\n *\n * Compared as a SET, not a sequence: Claude Code runs all matching hooks in parallel, so array order\n * carries no meaning and reordering must not read as drift.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registrationStale(settings: ClaudeSettings): boolean {\n const bins = registeredBins(settings);\n if (bins.length === 0) return false;\n const have = managedEntries(settings);\n const want = expectedEntries(bins);\n if (have.length !== want.length) return true;\n return want.some((w: HookRegistrationEntry): boolean => !have.some((h: HookRegistrationEntry): boolean => h.sameAs(w)));\n}\n\n/** Drop every webpieces-managed PreToolUse command; returns true if anything was removed. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function dropManagedEntries(settings: ClaudeSettings): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand): boolean => !isManagedCommand(h.command));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\n/** Append one PreToolUse entry. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function addHookEntry(settings: ClaudeSettings, entry: HookRegistrationEntry): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher: entry.matcher, hooks: [{ type: 'command', command: entry.command }] });\n}\n\n/** The settings `env` block, or null when the file carries none (or carries junk in its place). */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nfunction settingsEnv(settings: ClaudeSettings): Record<string, string> | null {\n // webpieces-disable no-any-unknown -- settings.json is opaque consumer JSON; `env` is only typed after this check\n const env: unknown = settings.env;\n if (typeof env !== 'object' || env === null || Array.isArray(env)) return null;\n return env as Record<string, string>;\n}\n\n/**\n * True when a settings file that registers webpieces hooks is missing the managed `env` entry, or\n * carries any value other than the one required one.\n *\n * Gated on `registeredBins()` for the SAME reason `registrationStale()` is: a settings file that\n * registers no webpieces hooks is not a project install and is never judged — a global install names\n * the bin path directly and carries no shim marker at all, so there is nothing here to keep in step.\n *\n * NOTE what this entry is for NOW. It was originally justified by keeping the then-RELATIVE hook path\n * resolvable; that job is retired, because both hooks are absolute and resolve from any cwd. It is kept\n * for VERDICT STABILITY — a guard's answer must depend on the command, not on where an earlier `cd` left\n * the shell — and because settings `env` is inherited, every subagent shares that cwd and therefore that\n * verdict. See managed-env.ts, which states this at length.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function envStale(settings: ClaudeSettings): boolean {\n if (registeredBins(settings).length === 0) return false;\n const env = settingsEnv(settings);\n return env === null || env[BASH_CWD_ENV_KEY] !== BASH_CWD_ENV_VALUE;\n}\n\n/**\n * Set the managed `env` entry IN PLACE, whatever was there before. Returns true when it changed.\n *\n * A user-set `\"0\"` is BROUGHT TO `\"1\"`, not honoured: this is a managed surface, and \"webpieces sets it\n * unless you disagreed\" would be a second, invisible spelling of the decision. Turning it off means\n * uninstalling the hooks, exactly as it does for every other managed thing.\n *\n * UNGATED on purpose — the installer calls it for the file it is writing hooks into, and the gate lives\n * at the caller (repairRegistration / applyHook), so a file with no webpieces hooks is never touched.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function applyManagedEnv(settings: ClaudeSettings): boolean {\n const existing = settingsEnv(settings);\n if (existing !== null && existing[BASH_CWD_ENV_KEY] === BASH_CWD_ENV_VALUE) return false;\n const env = existing ?? {};\n env[BASH_CWD_ENV_KEY] = BASH_CWD_ENV_VALUE;\n settings.env = env;\n return true;\n}\n\n/**\n * Bring one settings object to the two-hook ABSOLUTE form AND the managed `env` entry IN PLACE. Returns\n * true when it changed anything.\n *\n * REMOVE-then-ADD for the hooks, never add-beside: two spellings of one registration is exactly the\n * compatibility shim the backwards-compat reviewer rejects, and leaving a RELATIVE entry beside the\n * absolute one would run two shims per call — the\n * straddle this whole change exists to delete.\n *\n * THE TWO REPAIRS ARE EVALUATED INDEPENDENTLY, and that is load-bearing rather than tidy: this used to\n * early-return on `!registrationStale(settings)`, so a repo whose hooks are already current but whose\n * `env` entry is missing — the state EVERY existing consumer is in the moment this release lands — would\n * have been reported as drifted by fault S and then left unrepaired by its own prescribed cure. A cure\n * that skips half the surface is the failure mode `upgrade-shim.ts`'s header exists to prevent.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function repairRegistration(settings: ClaudeSettings): boolean {\n const bins = registeredBins(settings);\n if (bins.length === 0) return false;\n let changed = false;\n if (registrationStale(settings)) {\n dropManagedEntries(settings);\n for (const entry of expectedEntries(bins)) addHookEntry(settings, entry);\n changed = true;\n }\n if (applyManagedEnv(settings)) changed = true;\n return changed;\n}\n\n/** The two project settings files the installer can write. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function projectSettingsPaths(projectRoot: string): readonly string[] {\n return [\n path.join(projectRoot, '.claude', 'settings.json'),\n path.join(projectRoot, '.claude', 'settings.local.json'),\n ];\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\n/** True when either project settings file under `root` carries a stale registration. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registrationStaleAt(root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return projectSettingsPaths(root).some((p: string): boolean => registrationStale(readSettings(p)));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable/invalid settings counts as \"not stale\" so it never wedges a call\n return false;\n }\n}\n\n/** True when either project settings file under `root` is missing the managed `env` entry. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function envStaleAt(root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return projectSettingsPaths(root).some((p: string): boolean => envStale(readSettings(p)));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable/invalid settings counts as \"not stale\" so it never wedges a call\n return false;\n }\n}\n\n/**\n * WHAT was rewritten in ONE settings file. Data-only → a class, per CLAUDE.md.\n *\n * Two independent flags rather than one path, because the cure has to be able to SAY which repair it\n * made: \"rewrote the hook registration\" printed for a file whose registration was already current and\n * whose `env` entry was the only thing missing is a cure lying about its own work.\n */\nexport class SettingsRepair {\n constructor(\n readonly settingsPath: string,\n readonly registration: boolean,\n readonly env: boolean,\n ) {}\n}\n\n/** Rewrite every stale project settings file under `root`; returns what changed, per file. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function repairRegistrationAt(root: string): readonly SettingsRepair[] {\n const repairs: SettingsRepair[] = [];\n for (const settingsPath of projectSettingsPaths(root)) {\n if (!fs.existsSync(settingsPath)) continue;\n const settings = readSettings(settingsPath);\n const neededRegistration = registrationStale(settings);\n const neededEnv = envStale(settings);\n if (!repairRegistration(settings)) continue;\n writeSettings(settingsPath, settings);\n repairs.push(new SettingsRepair(settingsPath, neededRegistration, neededEnv));\n }\n return repairs;\n}\n\n/**\n * The THREE names the drift check reports, so a deny can say WHICH of them moved.\n *\n * There were four. `GUARANTEE_ROOT_SURFACE` is gone with L-1 itself: an absolutely-registered shim\n * resolves from any cwd, so there is no launch guarantee left to police and no second .sh file to keep\n * byte-locked. A settings file still carrying the retired H1 entry is not its own surface any more —\n * it is ordinary REGISTRATION drift, which `registrationStaleAt()` already reports and\n * `repairRegistration()` already fixes by removing it.\n */\nexport const SHIM_SURFACE = SHIM_MARKER;\nexport const REGISTRATION_SURFACE = '.claude/settings.json hook registration';\nexport const ENV_SURFACE = `.claude/settings.json env.${BASH_CWD_ENV_KEY}`;\n\n/**\n * WHICH of the three managed surfaces disagree with this release — the input to fault S.\n *\n * All three are checked against the SAME root, resolved from the RUNNING MODULE (governingShimRoot),\n * never from cwd and never from `$CLAUDE_PROJECT_DIR`: the files we compare and the renderers we\n * compare them TO must come from one install, or the check straddles two trees and can never converge.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function managedSurfaceDrift(root: string | null): readonly string[] {\n const drifted: string[] = [];\n if (committedShimStale(root)) drifted.push(SHIM_SURFACE);\n if (registrationStaleAt(root)) drifted.push(REGISTRATION_SURFACE);\n if (envStaleAt(root)) drifted.push(ENV_SURFACE);\n return drifted;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"hook-registration.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/hook-registration.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { toError } from '../core/to-error';\nimport { SHIM_MARKER, committedShimStale } from './shim';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\n\n/**\n * THE INSTALLED HOOK SURFACE — two hooks, both ABSOLUTE, and the ONE place their spelling is defined.\n *\n * ─── ONE GOVERNOR: the MAIN tree judges every tree ─────────────────────────────────────────────────\n *\n * H1 sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" wp-ai-guards-hook Write|…|Bash|Read\n * H2 sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" wp-ai-rules-hook Write|Edit|MultiEdit\n *\n * This REPLACES a three-hook form in which these two were RELATIVE (`sh \".claude/webpieces/ai-hook.sh\"`)\n * and a third hook, L-1 `guarantee-root.sh`, existed solely to guarantee that relative path resolved.\n *\n * ─── Why the relative experiment was reversed ──────────────────────────────────────────────────────\n * Relative registration was adopted so each git tree would be governed by \"its own release, binary and\n * pin\". MEASURED 2026-08-10: it never delivered that. A linked worktree has NO `node_modules`, so\n * ai-hook.sh's upward walk executes the PRIMARY's binary — `readlink -f` resolved a worktree agent's bin\n * to `<primary>/node_modules/@webpieces/ai-hook-rules`. A worktree ran its own SCRIPT and its own\n * CONFIG; it never ran its own release. Governance was already the primary's, in every tree, the whole\n * time.\n *\n * The price of that fiction was an entire guard layer. A relative path only resolves at a tree root, and\n * a hook that cannot resolve exits 127 — per the hooks reference a NON-BLOCKING error, i.e. a SILENT\n * UNGUARDED ALLOW. So L-1 had to deny every `cd` into a project subdirectory. That denial produced the\n * force-to-root bug class, the reaped-worktree `cd` prescription, and a measured hard deadlock: L-1 told\n * a worktree-isolated agent to `cd` to the primary clone, which the harness refuses for an isolated\n * agent, leaving it unable to stay or to follow the cure.\n *\n * An absolute path resolves from ANY cwd. So the launch guarantee is structural, L-1 has no job left and\n * is deleted, and `cd` into a subdirectory is simply allowed. What used to be true only by policing the\n * shell is now true by construction.\n *\n * ─── What replaces the property that was lost ──────────────────────────────────────────────────────\n * Nothing is lost that was ever delivered — but the case relative registration WANTED to handle (a tree\n * that genuinely needs a different @webpieces) is now DETECTED instead of silently mis-governed:\n * `VersionSyncGuard` (L1 row 8) blocks when a worktree's pin disagrees with the main tree's, and\n * prescribes either aligning the pins (same git hash → same tracked pin → install in each tree that has\n * a node_modules) or using a separate CLONE, which — unlike a worktree — gets its own GOVERNANCE. Note\n * what that does NOT say: a worktree may perfectly well have its own node_modules, and usually does the\n * moment anything installs in it. What it may not have is a DIFFERENT @webpieces version.\n *\n * ─── Why the registration is a DRIFT SURFACE, not just an install step ─────────────────────────────\n * Nothing used to validate `.claude/settings.json` at all, so a settings file left on a superseded form\n * silently changed who governs. The installed surface is THREE things (ai-hook.sh, the registration, and\n * the managed `env` entry — see managed-env.ts), all three are compared against this release, and\n * `wp-upgrade-shim` regenerates all three. A cure that fixes two of three is worse than no cure, because\n * it reports success.\n */\n\n/**\n * The RETIRED L-1 hook's committed path. Named here, in the one module that must still recognise it, and\n * matched ONLY by isManagedCommand() so repairRegistration() can delete the stale entry. There is no\n * guarantee-root.ts any more — this literal is all that remains of it, deliberately.\n *\n * It is a one-way RECOGNISER, not a shim: nothing emits it, `expectedEntries()` never returns it, and a\n * settings file carrying it is reported STALE. Its only job is to make the retired entry findable so it\n * can be REMOVED.\n */\nexport const LEGACY_GUARANTEE_ROOT_MARKER = '.claude/webpieces/guarantee-root.sh';\n\n/**\n * When this recogniser may be deleted, as a value rather than a comment nobody re-reads.\n *\n * A removal-only migration is still dead weight once no consumer can be carrying the old shape. The\n * hazard of deleting it EARLY is severe and silent — repair would stop stripping the retired entry, and\n * a hook registered against a deleted file exits 127, which the Claude Code hooks reference defines as a\n * NON-BLOCKING error, i.e. every `cd` unjudged while the cure reports success. So it gets a stated date\n * and a test that fails once the date passes, instead of an intention.\n *\n * `legacy-marker-expiry.spec.ts` reads this and fails after it, which is the reminder.\n */\nexport const LEGACY_MARKER_REMOVE_AFTER = '2026-12-01';\n\n/** One PreToolUse hook entry as webpieces registers it. Data-only → a class, per CLAUDE.md. */\nexport class HookRegistrationEntry {\n constructor(\n readonly matcher: string,\n readonly command: string,\n ) {}\n\n sameAs(other: HookRegistrationEntry): boolean {\n return this.matcher === other.matcher && this.command === other.command;\n }\n}\n\n// webpieces-disable no-any-unknown -- settings.json is opaque consumer JSON\nexport interface HookCommand { type: string; command: string; }\nexport interface HookEntry { matcher: string; hooks: HookCommand[]; }\nexport interface ClaudeSettings {\n hooks?: { PreToolUse?: HookEntry[] };\n // Claude Code's settings `env` block: every key is exported into the environment of the session AND\n // of every subagent it spawns. That inheritance is precisely why webpieces pins its managed entry\n // here rather than in a shell profile — see managed-env.ts.\n env?: Record<string, string>;\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport const RULES_BIN = 'wp-ai-rules-hook';\nexport const GUARDS_BIN = 'wp-ai-guards-hook';\nexport const RULES_MATCHER = 'Write|Edit|MultiEdit';\n// Guards match Bash (git/PR guards), Write|Edit|MultiEdit (file-scoped guards) AND Read — Read carries\n// no guard, but the guards hook owns the per-invocation audit log, so matching Read records every file\n// the AI opens (log-and-allow fast path in hook-core.ts; a Read is never blocked).\nexport const GUARDS_MATCHER = 'Write|Edit|MultiEdit|Bash|Read';\n\n/**\n * The guard-hook command — `sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" <bin>`.\n *\n * ABSOLUTE. This replaced a RELATIVE spelling, and the reversal is the whole point of this release.\n *\n * The relative form existed to give each git tree its own release, binary and pin. **It never delivered\n * that.** Measured 2026-08-10: a linked worktree has no `node_modules`, so ai-hook.sh's upward walk\n * executes the PRIMARY's binary — `readlink -f` resolved a worktree agent's bin to\n * `<primary>/node_modules/@webpieces/ai-hook-rules`. A worktree ran its own SCRIPT and its own CONFIG,\n * never its own release. The property was fiction, and paying for it cost an entire guard layer: a\n * relative path only resolves at a tree root, so L-1 (guarantee-root.sh) had to deny every `cd` into a\n * project subdirectory, which produced the force-to-root bug class, the reaped-worktree `cd`\n * prescription, and a measured hard deadlock where L-1 told a worktree-isolated agent to `cd` to the\n * primary clone — which the harness refuses for an isolated agent.\n *\n * Absolute resolves from ANY cwd, so L-1 has no job left and is deleted. One governor: the MAIN tree\n * judges every tree, which is what was already happening via the borrowed binary — the design now says\n * so out loud, and `VersionSyncGuard` blocks the case where that is the wrong answer.\n *\n * Invoked via `sh <file>` rather than executed directly so a missing executable bit on the checked-in\n * shim (fresh clone, a filesystem that drops the bit, git core.fileMode quirks) can never break the hook\n * with a raw `Permission denied` on every tool call. Quoted to survive spaces in the path.\n */\n// webpieces-disable no-function-outside-class -- this module must load on a tree too broken to build a DI container (upgrade-shim.ts depends on that), so it is module-scope like its siblings shim.ts\nexport function shimCommand(bin: string): string {\n return `sh \"$CLAUDE_PROJECT_DIR/${SHIM_MARKER}\" ${bin}`;\n}\n\n/** The registration entry for one guard bin. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function guardHookEntry(bin: string): HookRegistrationEntry {\n return new HookRegistrationEntry(bin === RULES_BIN ? RULES_MATCHER : GUARDS_MATCHER, shimCommand(bin));\n}\n\n/**\n * True when this PreToolUse command is one webpieces owns — in ANY spelling it has ever shipped.\n *\n * THE SINGLE MOST IMPORTANT LINE IN THIS RELEASE. `LEGACY_GUARANTEE_ROOT_MARKER` is matched here and\n * NOWHERE else: it is how `repairRegistration()` finds and REMOVES the retired H1 entry from a settings\n * file written by an older release. Drop it and the repair silently leaves a live L-1 hook registered\n * against a file this release deletes — exit 127, which the Claude Code hooks reference defines as a\n * NON-BLOCKING error, so every `cd` goes unjudged while `wp-upgrade-shim` reports success and no drift\n * check can name it. Removal-only, never emitted: it appears in no `expectedEntries()` result, so it is\n * a one-way migration and not a second accepted spelling.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function isManagedCommand(command: string): boolean {\n return command.includes(SHIM_MARKER) || command.includes(LEGACY_GUARANTEE_ROOT_MARKER);\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nfunction preToolUse(settings: ClaudeSettings): readonly HookEntry[] {\n return settings.hooks?.PreToolUse ?? [];\n}\n\n/** Every webpieces-managed entry of one settings file, flattened to matcher + command pairs. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function managedEntries(settings: ClaudeSettings): readonly HookRegistrationEntry[] {\n const found: HookRegistrationEntry[] = [];\n for (const entry of preToolUse(settings)) {\n for (const hook of entry.hooks) {\n if (isManagedCommand(hook.command)) found.push(new HookRegistrationEntry(entry.matcher, hook.command));\n }\n }\n return found;\n}\n\n/**\n * Which guard bins this settings file registers, in installer order. A file registering NEITHER is not\n * a project (relative) install and is therefore never judged — a global/absolute install names the bin\n * path directly and carries no shim marker at all.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registeredBins(settings: ClaudeSettings): readonly string[] {\n const commands = managedEntries(settings).map((e: HookRegistrationEntry): string => e.command);\n return [GUARDS_BIN, RULES_BIN].filter((bin: string): boolean => commands.some((c: string): boolean => c.includes(bin)));\n}\n\n/**\n * The exact set of entries THIS RELEASE expects in a settings file that registers `bins`.\n *\n * ONE entry per registered bin, and nothing else. There used to be a third, `guarantee-root.sh`, added\n * whenever the GUARDS bin was present; it is retired, and a settings file still carrying it is STALE —\n * `repairRegistration()` removes it via isManagedCommand()'s legacy marker.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function expectedEntries(bins: readonly string[]): readonly HookRegistrationEntry[] {\n return bins.map((bin: string): HookRegistrationEntry => guardHookEntry(bin));\n}\n\n/**\n * True when a settings file registers webpieces hooks in a shape this release does not expect: a\n * RELATIVE shim command, a leftover guarantee-root entry, a stray duplicate, or a wrong matcher.\n *\n * Compared as a SET, not a sequence: Claude Code runs all matching hooks in parallel, so array order\n * carries no meaning and reordering must not read as drift.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registrationStale(settings: ClaudeSettings): boolean {\n const bins = registeredBins(settings);\n if (bins.length === 0) return false;\n const have = managedEntries(settings);\n const want = expectedEntries(bins);\n if (have.length !== want.length) return true;\n return want.some((w: HookRegistrationEntry): boolean => !have.some((h: HookRegistrationEntry): boolean => h.sameAs(w)));\n}\n\n/** Drop every webpieces-managed PreToolUse command; returns true if anything was removed. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function dropManagedEntries(settings: ClaudeSettings): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand): boolean => !isManagedCommand(h.command));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\n/** Append one PreToolUse entry. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function addHookEntry(settings: ClaudeSettings, entry: HookRegistrationEntry): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher: entry.matcher, hooks: [{ type: 'command', command: entry.command }] });\n}\n\n/** The settings `env` block, or null when the file carries none (or carries junk in its place). */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nfunction settingsEnv(settings: ClaudeSettings): Record<string, string> | null {\n // webpieces-disable no-any-unknown -- settings.json is opaque consumer JSON; `env` is only typed after this check\n const env: unknown = settings.env;\n if (typeof env !== 'object' || env === null || Array.isArray(env)) return null;\n return env as Record<string, string>;\n}\n\n/**\n * True when a settings file that registers webpieces hooks is missing the managed `env` entry, or\n * carries any value other than the one required one.\n *\n * Gated on `registeredBins()` for the SAME reason `registrationStale()` is: a settings file that\n * registers no webpieces hooks is not a project install and is never judged — a global install names\n * the bin path directly and carries no shim marker at all, so there is nothing here to keep in step.\n *\n * NOTE what this entry is for NOW. It was originally justified by keeping the then-RELATIVE hook path\n * resolvable; that job is retired, because both hooks are absolute and resolve from any cwd. It is kept\n * for VERDICT STABILITY — a guard's answer must depend on the command, not on where an earlier `cd` left\n * the shell — and because settings `env` is inherited, every subagent shares that cwd and therefore that\n * verdict. See managed-env.ts, which states this at length.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function envStale(settings: ClaudeSettings): boolean {\n if (registeredBins(settings).length === 0) return false;\n const env = settingsEnv(settings);\n return env === null || env[BASH_CWD_ENV_KEY] !== BASH_CWD_ENV_VALUE;\n}\n\n/**\n * Set the managed `env` entry IN PLACE, whatever was there before. Returns true when it changed.\n *\n * A user-set `\"0\"` is BROUGHT TO `\"1\"`, not honoured: this is a managed surface, and \"webpieces sets it\n * unless you disagreed\" would be a second, invisible spelling of the decision. Turning it off means\n * uninstalling the hooks, exactly as it does for every other managed thing.\n *\n * UNGATED on purpose — the installer calls it for the file it is writing hooks into, and the gate lives\n * at the caller (repairRegistration / applyHook), so a file with no webpieces hooks is never touched.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function applyManagedEnv(settings: ClaudeSettings): boolean {\n const existing = settingsEnv(settings);\n if (existing !== null && existing[BASH_CWD_ENV_KEY] === BASH_CWD_ENV_VALUE) return false;\n const env = existing ?? {};\n env[BASH_CWD_ENV_KEY] = BASH_CWD_ENV_VALUE;\n settings.env = env;\n return true;\n}\n\n/**\n * Bring one settings object to the two-hook ABSOLUTE form AND the managed `env` entry IN PLACE. Returns\n * true when it changed anything.\n *\n * REMOVE-then-ADD for the hooks, never add-beside: two spellings of one registration is exactly the\n * compatibility shim the backwards-compat reviewer rejects, and leaving a RELATIVE entry beside the\n * absolute one would run two shims per call — the\n * straddle this whole change exists to delete.\n *\n * THE TWO REPAIRS ARE EVALUATED INDEPENDENTLY, and that is load-bearing rather than tidy: this used to\n * early-return on `!registrationStale(settings)`, so a repo whose hooks are already current but whose\n * `env` entry is missing — the state EVERY existing consumer is in the moment this release lands — would\n * have been reported as drifted by fault S and then left unrepaired by its own prescribed cure. A cure\n * that skips half the surface is the failure mode `upgrade-shim.ts`'s header exists to prevent.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function repairRegistration(settings: ClaudeSettings): boolean {\n const bins = registeredBins(settings);\n if (bins.length === 0) return false;\n let changed = false;\n if (registrationStale(settings)) {\n dropManagedEntries(settings);\n for (const entry of expectedEntries(bins)) addHookEntry(settings, entry);\n changed = true;\n }\n if (applyManagedEnv(settings)) changed = true;\n return changed;\n}\n\n/** The two project settings files the installer can write. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function projectSettingsPaths(projectRoot: string): readonly string[] {\n return [\n path.join(projectRoot, '.claude', 'settings.json'),\n path.join(projectRoot, '.claude', 'settings.local.json'),\n ];\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\n/** True when either project settings file under `root` carries a stale registration. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registrationStaleAt(root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return projectSettingsPaths(root).some((p: string): boolean => registrationStale(readSettings(p)));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable/invalid settings counts as \"not stale\" so it never wedges a call\n return false;\n }\n}\n\n/** True when either project settings file under `root` is missing the managed `env` entry. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function envStaleAt(root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return projectSettingsPaths(root).some((p: string): boolean => envStale(readSettings(p)));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable/invalid settings counts as \"not stale\" so it never wedges a call\n return false;\n }\n}\n\n/**\n * WHAT was rewritten in ONE settings file. Data-only → a class, per CLAUDE.md.\n *\n * Two independent flags rather than one path, because the cure has to be able to SAY which repair it\n * made: \"rewrote the hook registration\" printed for a file whose registration was already current and\n * whose `env` entry was the only thing missing is a cure lying about its own work.\n */\nexport class SettingsRepair {\n constructor(\n readonly settingsPath: string,\n readonly registration: boolean,\n readonly env: boolean,\n ) {}\n}\n\n/** Rewrite every stale project settings file under `root`; returns what changed, per file. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function repairRegistrationAt(root: string): readonly SettingsRepair[] {\n const repairs: SettingsRepair[] = [];\n for (const settingsPath of projectSettingsPaths(root)) {\n if (!fs.existsSync(settingsPath)) continue;\n const settings = readSettings(settingsPath);\n const neededRegistration = registrationStale(settings);\n const neededEnv = envStale(settings);\n if (!repairRegistration(settings)) continue;\n writeSettings(settingsPath, settings);\n repairs.push(new SettingsRepair(settingsPath, neededRegistration, neededEnv));\n }\n return repairs;\n}\n\n/**\n * The THREE names the drift check reports, so a deny can say WHICH of them moved.\n *\n * There were four. `GUARANTEE_ROOT_SURFACE` is gone with L-1 itself: an absolutely-registered shim\n * resolves from any cwd, so there is no launch guarantee left to police and no second .sh file to keep\n * byte-locked. A settings file still carrying the retired H1 entry is not its own surface any more —\n * it is ordinary REGISTRATION drift, which `registrationStaleAt()` already reports and\n * `repairRegistration()` already fixes by removing it.\n */\nexport const SHIM_SURFACE = SHIM_MARKER;\nexport const REGISTRATION_SURFACE = '.claude/settings.json hook registration';\nexport const ENV_SURFACE = `.claude/settings.json env.${BASH_CWD_ENV_KEY}`;\n\n/**\n * WHICH of the three managed surfaces disagree with this release — the input to fault S.\n *\n * All three are checked against the SAME root, resolved from the RUNNING MODULE (governingShimRoot),\n * never from cwd and never from `$CLAUDE_PROJECT_DIR`: the files we compare and the renderers we\n * compare them TO must come from one install, or the check straddles two trees and can never converge.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function managedSurfaceDrift(root: string | null): readonly string[] {\n const drifted: string[] = [];\n if (committedShimStale(root)) drifted.push(SHIM_SURFACE);\n if (registrationStaleAt(root)) drifted.push(REGISTRATION_SURFACE);\n if (envStaleAt(root)) drifted.push(ENV_SURFACE);\n return drifted;\n}\n"]}
|
|
@@ -40,6 +40,22 @@ const shim_1 = require("./shim");
|
|
|
40
40
|
// these two fields is what sent a real agent chasing the wrong mechanism for four cures. Same field
|
|
41
41
|
// names on purpose, so the deny text and the log lines grep together.
|
|
42
42
|
//
|
|
43
|
+
// THE MESSAGE DIET IS PART OF THE CONTRACT, and this deny is where it regresses. main landed a
|
|
44
|
+
// deliberate L0 message diet (384cdae) and blocks grow straight back into a wall of text when each new
|
|
45
|
+
// finding argues its case here; at eleven sections this one was ~4,000 chars, and `denyBudget` in
|
|
46
|
+
// shim-deny-reason.spec.ts now fails the build if it climbs back. Keep what CHANGES what the reader
|
|
47
|
+
// types (the two exact commands, that it is not a deadlock, the no-chaining rule, root=/projectDir=);
|
|
48
|
+
// cut what merely argues. What was cut and why:
|
|
49
|
+
// - the "up to and including 0.4.588 OPTION 1 was inert, so EMPTY OUTPUT means it did not run"
|
|
50
|
+
// paragraph. It was UNREACHABLE by construction: this text and `wp-upgrade-shim` ship in the SAME
|
|
51
|
+
// package at the SAME version, so a binary new enough to print this sentence necessarily has the
|
|
52
|
+
// process entry point 0.4.589 added. The incident is still recorded where it can bite —
|
|
53
|
+
// bin-process-entry.spec.ts, which asserts the entry point exists.
|
|
54
|
+
// - the "registered ABSOLUTE / a worktree borrows by walking up" aside. That is the DRIFT fault's
|
|
55
|
+
// subject (WP_BORROW_NOTE in shim.ts) and it is restated in the guard-matrix doc this deny points at.
|
|
56
|
+
// - "they are GENERATED and committed ... must NOT be reverted by hand", which said twice over what
|
|
57
|
+
// the closing sentence already says once.
|
|
58
|
+
//
|
|
43
59
|
// CONSTRAINT: the returned string must contain no `"` and no `\` — it is JSON-serialized by denyJson()
|
|
44
60
|
// (a stray quote/backslash would corrupt the PreToolUse decision payload, not just the text). That is an
|
|
45
61
|
// INVARIANT, not a hope, so every interpolated path is STRIPPED of both rather than trusted — a
|
|
@@ -48,11 +64,33 @@ const shim_1 = require("./shim");
|
|
|
48
64
|
//
|
|
49
65
|
// `inSubagent` comes from the PreToolUse payload's `agent_id`, which Claude Code delivers on stdin and
|
|
50
66
|
// populates ONLY off the main loop (main falls back to the session id, so the field is absent there).
|
|
51
|
-
// `agent_type` is NOT usable for this — it is always populated and discriminates nothing.
|
|
52
|
-
// needs one extra sentence, because the hooks that are blocking it resolve through $CLAUDE_PROJECT_DIR,
|
|
53
|
-
// which names the MAIN tree: a cure run only in its own worktree cannot lift the block. It is a
|
|
67
|
+
// `agent_type` is NOT usable for this — it is always populated and discriminates nothing. It is a
|
|
54
68
|
// REQUIRED parameter for the same reason `drifted` is — an optional flag would let a caller keep
|
|
55
69
|
// emitting the main-loop text from a subagent, which is the case that most needs the extra line.
|
|
70
|
+
//
|
|
71
|
+
// THE SUBAGENT SENTENCE REASONS FROM root= vs projectDir=, IT DOES NOT ASSERT A FIXED CONCLUSION. It
|
|
72
|
+
// used to say flatly "the hooks resolve through CLAUDE_PROJECT_DIR, which names the MAIN tree, so a cure
|
|
73
|
+
// run only here CANNOT lift this block". That is a POST-FLIP fact stated during the PRE-FLIP window, and
|
|
74
|
+
// it is FALSE exactly when it fires: measured 2026-08-10, a worktree subagent hit fault S, ran OPTION 1
|
|
75
|
+
// in ITS OWN worktree, and the block lifted — because the registration still in force was the RELATIVE
|
|
76
|
+
// three-hook form, so the worktree's own ai-hook.sh ran against the worktree's own node_modules. The
|
|
77
|
+
// deny's own `root=` field named the worktree while the sentence insisted otherwise.
|
|
78
|
+
// WHICH tree to cure is already answered, for BOTH windows, by the root=/projectDir= verdict above:
|
|
79
|
+
// committedShimStale compares shimPath(root) and `root` is the tree the RUNNING binary came from, so
|
|
80
|
+
// repairing the root= tree clears the fault whichever registration form is live, and the cure is
|
|
81
|
+
// cd-anchored there. Telling an agent its cure cannot work, while it demonstrably can, costs more than
|
|
82
|
+
// saying nothing.
|
|
83
|
+
//
|
|
84
|
+
// SO THE SUBAGENT SENTENCE CARRIES THE PART THAT IS ACTUALLY SUBAGENT-SPECIFIC: there are TWO real
|
|
85
|
+
// cures and they fix different things. A — run the printed cure here; it works, and it makes THIS tree
|
|
86
|
+
// work NOW. B — a subagent cannot reach the main clone, so aligning the two trees is an ESCALATION
|
|
87
|
+
// ("ask the coordinator to run pnpm install in the main tree"), and that is what stops the trees
|
|
88
|
+
// disagreeing. Doing only A leaves the repo with two trees on two @webpieces releases — the live state
|
|
89
|
+
// on 2026-08-10, main clone on 0.4.616 while origin/main and three worktrees were on 0.4.624. And the
|
|
90
|
+
// rule it states is deliberately NOT "do not install in a worktree": a worktree NEEDS its own
|
|
91
|
+
// node_modules (nx, vitest and the eslint plugin all execute there and load from it). The rule is that
|
|
92
|
+
// its @webpieces must EQUAL the main tree's — the older WP_BORROW_NOTE wording got this backwards in
|
|
93
|
+
// both directions at different times.
|
|
56
94
|
// webpieces-disable no-function-outside-class -- pure string builder over exported constants; the single source of the self-guard deny text now that the sh copy is gone.
|
|
57
95
|
function shimStaleDenyReason(installedVersion, root, drifted, inSubagent) {
|
|
58
96
|
const verNote = installedVersion ? ` (installed version ${installedVersion})` : '';
|
|
@@ -67,16 +105,19 @@ function shimStaleDenyReason(installedVersion, root, drifted, inSubagent) {
|
|
|
67
105
|
// guard was rewritten to make unconstructible, so it gets said out loud rather than left to inference.
|
|
68
106
|
const verdict = safeRoot === projectDir
|
|
69
107
|
? 'These two AGREE, so this is the ordinary case - the tree you are in is the tree being judged.'
|
|
70
|
-
: 'These two DISAGREE - the tree being judged is NOT the one CLAUDE_PROJECT_DIR names, so cure the root= tree
|
|
71
|
-
const rootNote = safeRoot === '' ? '' : ` WHERE THIS WAS MEASURED: root=${safeRoot} (the tree
|
|
108
|
+
: 'These two DISAGREE - the tree being judged is NOT the one CLAUDE_PROJECT_DIR names, so cure the root= tree and do not assume your cwd is it.';
|
|
109
|
+
const rootNote = safeRoot === '' ? '' : ` WHERE THIS WAS MEASURED: root=${safeRoot} (the tree whose shim was compared - the one to repair), projectDir=${projectDir} (CLAUDE_PROJECT_DIR as this process sees it; <unset> = absent, not set-but-empty). ${verdict}`;
|
|
72
110
|
const upgrade = cdOk ? `cd ${safeRoot} && ${l0_allowlist_1.UPGRADE_SHIM_CMD}` : l0_allowlist_1.UPGRADE_SHIM_CMD;
|
|
73
111
|
// OPTION 2 is a relative-path `cp`, so it is even MORE cwd-sensitive than OPTION 1 — anchor it too.
|
|
74
112
|
const restore = cdOk ? `cd ${safeRoot} && ${l0_allowlist_1.RESTORE_SHIM_CMD}` : l0_allowlist_1.RESTORE_SHIM_CMD;
|
|
75
113
|
// The subagent sentence goes BEFORE the options, so it is read before a cure is chosen rather than
|
|
76
|
-
// after one has already been run in the wrong tree.
|
|
114
|
+
// after one has already been run in the wrong tree. WHICH tree is already answered by `verdict`
|
|
115
|
+
// above, from root= vs projectDir=; this adds only what is specific to a subagent — that A and B are
|
|
116
|
+
// BOTH real and fix different things, and that B is an ESCALATION because a subagent cannot reach
|
|
117
|
+
// the main clone.
|
|
77
118
|
const subagentNote = inSubagent
|
|
78
|
-
? ' YOU ARE RUNNING IN A SUBAGENT: the
|
|
119
|
+
? ' YOU ARE RUNNING IN A SUBAGENT, so TWO cures are real and they fix DIFFERENT things: A makes THIS tree work now, B stops the two trees disagreeing. A - run the OPTION below exactly as printed; it is already anchored to the tree that must change, and running it from here DOES lift this block (measured), so never conclude a local cure cannot work. B - you cannot reach the main clone, so ESCALATE: ask the coordinator to run pnpm install in the main tree so both trees are on the same @webpieces version. A worktree NEEDS its own node_modules (nx, vitest and the eslint plugin all load from it); the rule is not no-install-here, it is that this tree @webpieces must EQUAL the main tree.'
|
|
79
120
|
: '';
|
|
80
|
-
return `❌ webpieces-managed hook surface was changed: ${what} no longer matches what the INSTALLED @webpieces/ai-hook-rules${verNote} expects (reverted, hand-edited, or predating this binary
|
|
121
|
+
return `❌ webpieces-managed hook surface was changed: ${what} no longer matches what the INSTALLED @webpieces/ai-hook-rules${verNote} expects (reverted, hand-edited, or predating this binary).${rootNote} webpieces manages THREE things and they only work as a set: ${shim_1.SHIM_MARKER} (the guard shim), the .claude/settings.json entries that register it, and the .claude/settings.json env entry ${managed_env_1.BASH_CWD_ENV_KEY}=${managed_env_1.BASH_CWD_ENV_VALUE} (which pins the Bash cwd to the project root, identically for every subagent because settings env is inherited). Every OTHER tool call is blocked until all three match again.${subagentNote} THIS IS NOT A DEADLOCK: both options below are explicitly ALLOWED through while this guard is up, so run one YOURSELF now - do not hand it back to the human. OPTION 1 (preferred - the only one that repairs all three, and it runs on a broken tree) - run EXACTLY this command: '${upgrade}'. OPTION 2 (PARTIAL - repairs ${shim_1.SHIM_MARKER} only; pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where OPTION 1 does not exist yet, then upgrade and run OPTION 1) - run EXACTLY this command: '${restore}'. Do NOT use the bare '${l0_allowlist_1.INSTALL_HOOKS_CMD}' here: it also migrates your config and PROMPTS for a hook target twice, which hangs a non-interactive session. ${shim_1.NO_CHAINING_RULE} If you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json rather than reverting these files.`;
|
|
81
122
|
}
|
|
82
123
|
//# sourceMappingURL=shim-deny-reason.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shim-deny-reason.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-deny-reason.ts"],"names":[],"mappings":";;AAuDA,kDAwBC;AA/ED,0DAAoD;AAEpD,iDAAuF;AACvF,+CAAqE;AACrE,iCAAuD;AAEvD;;;;GAIG;AACH,kGAAkG;AAClG,gCAAgC;AAChC,EAAE;AACF,8FAA8F;AAC9F,qEAAqE;AACrE,qGAAqG;AACrG,sGAAsG;AACtG,iGAAiG;AACjG,gCAAgC;AAChC,EAAE;AACF,oGAAoG;AACpG,mGAAmG;AACnG,EAAE;AACF,mFAAmF;AACnF,qGAAqG;AACrG,8FAA8F;AAC9F,sGAAsG;AACtG,qGAAqG;AACrG,EAAE;AACF,sGAAsG;AACtG,sGAAsG;AACtG,mFAAmF;AACnF,EAAE;AACF,2FAA2F;AAC3F,kGAAkG;AAClG,qGAAqG;AACrG,mGAAmG;AACnG,oGAAoG;AACpG,sEAAsE;AACtE,EAAE;AACF,uGAAuG;AACvG,yGAAyG;AACzG,gGAAgG;AAChG,uGAAuG;AACvG,4GAA4G;AAC5G,EAAE;AACF,uGAAuG;AACvG,sGAAsG;AACtG,qGAAqG;AACrG,wGAAwG;AACxG,gGAAgG;AAChG,iGAAiG;AACjG,iGAAiG;AACjG,0KAA0K;AAC1K,SAAgB,mBAAmB,CAAC,gBAAwB,EAAE,IAAY,EAAE,OAA0B,EAAE,UAAmB;IACvH,MAAM,OAAO,GAAG,gBAAgB,CAAC,CAAC,CAAC,uBAAuB,gBAAgB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACnF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,wBAAS,CAAC,gBAAgB,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACtE,kGAAkG;IAClG,oGAAoG;IACpG,gGAAgG;IAChG,MAAM,IAAI,GAAG,IAAI,KAAK,EAAE,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/D,qGAAqG;IACrG,uGAAuG;IACvG,MAAM,OAAO,GAAG,QAAQ,KAAK,UAAU;QACnC,CAAC,CAAC,+FAA+F;QACjG,CAAC,CAAC,yKAAyK,CAAC;IAChL,MAAM,QAAQ,GAAG,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kCAAkC,QAAQ,+GAA+G,UAAU,gIAAgI,OAAO,EAAE,CAAC;IACrV,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,QAAQ,OAAO,+BAAgB,EAAE,CAAC,CAAC,CAAC,+BAAgB,CAAC;IAClF,oGAAoG;IACpG,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,QAAQ,OAAO,+BAAgB,EAAE,CAAC,CAAC,CAAC,+BAAgB,CAAC;IAClF,mGAAmG;IACnG,oDAAoD;IACpD,MAAM,YAAY,GAAG,UAAU;QAC3B,CAAC,CAAC,ggBAAggB;QAClgB,CAAC,CAAC,EAAE,CAAC;IACT,OAAO,iDAAiD,IAAI,iEAAiE,OAAO,wLAAwL,QAAQ,yEAAyE,kBAAW,0RAA0R,8BAAgB,IAAI,gCAAkB,uVAAuV,YAAY,2hBAA2hB,OAAO,wmBAAwmB,kBAAW,wVAAwV,OAAO,2BAA2B,gCAAiB,oHAAoH,uBAAgB,0IAA0I,CAAC;AAC92F,CAAC","sourcesContent":["import { claudeEnv } from '@webpieces/rules-config';\n\nimport { INSTALL_HOOKS_CMD, RESTORE_SHIM_CMD, UPGRADE_SHIM_CMD } from './l0-allowlist';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { NO_CHAINING_RULE, SHIM_MARKER } from './shim';\n\n/**\n * THE FAIL-CLOSED DENY TEXT for a drifted managed hook surface (L0 fault S) — its own module because\n * shim.ts is at its line cap and this is one cohesive unit: the words a blocked agent reads, and\n * nothing else. It imports FROM shim.ts and is never imported BY it, so the graph stays acyclic.\n */\n// The fail-closed deny text for a drifted MANAGED HOOK SURFACE, built from the single-source cure\n// constants + NO_CHAINING_RULE.\n//\n// `drifted` names WHICH of the three managed things moved — .claude/webpieces/ai-hook.sh, the\n// .claude/settings.json hook registration, and its managed env entry\n// CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR (see hook-registration.ts). It is REQUIRED, not optional:\n// this used to be a shim-only message, and an optional list would let a caller silently keep emitting\n// the one-file text after the surface grew — which is the \"two spellings of one thing\" shape the\n// compatibility policy rejects.\n//\n// It was FOUR. guarantee-root.sh (L-1) is gone: the guard hooks are registered ABSOLUTE now, so the\n// launch guarantee L-1 provided is structural and there is no second .sh file to keep byte-locked.\n//\n// `installedVersion` names WHICH webpieces the cure re-arms to (the binary is that\n// version); pass '' to omit the note rather than print an empty one. `root` is the tree the deciding\n// binary GOVERNS (governingShimRoot) — naming it, and anchoring the cure to it with a leading\n// `cd <root> &&` (which CD_PREFIX_*_ANCHORED already tolerates, locked by a unit test), is what keeps\n// the cure curable when the AI's cwd is a DIFFERENT tree than the one being judged. Pass '' to omit.\n//\n// The cause list is deliberately a LIST: it used to assert flatly \"(it was reverted or hand-edited)\",\n// which is frequently FALSE — the common case is a shim whose logic simply predates this binary — and\n// that false certainty sent a real agent hunting for a tamper that never happened.\n//\n// WHERE IT WAS MEASURED, in the deny itself and not only in the logs. #574 put `root=` and\n// `projectDir=` on every L1 invocation line (see decision-log / ClaudeEnv.projectDirForLog, whose\n// `<unset>` token keeps \"variable absent\" distinguishable from \"set to empty\"). The log is forensics\n// AFTER the fact; the deny is what a blocked agent reads IN the moment, and the absence of exactly\n// these two fields is what sent a real agent chasing the wrong mechanism for four cures. Same field\n// names on purpose, so the deny text and the log lines grep together.\n//\n// CONSTRAINT: the returned string must contain no `\"` and no `\\` — it is JSON-serialized by denyJson()\n// (a stray quote/backslash would corrupt the PreToolUse decision payload, not just the text). That is an\n// INVARIANT, not a hope, so every interpolated path is STRIPPED of both rather than trusted — a\n// Windows-style path or an odd directory name must not be able to corrupt the decision. Locked by unit\n// tests. An unusual root is also dropped from the `cd` cure rather than quoted (CD_PREFIX would reject it).\n//\n// `inSubagent` comes from the PreToolUse payload's `agent_id`, which Claude Code delivers on stdin and\n// populates ONLY off the main loop (main falls back to the session id, so the field is absent there).\n// `agent_type` is NOT usable for this — it is always populated and discriminates nothing. A subagent\n// needs one extra sentence, because the hooks that are blocking it resolve through $CLAUDE_PROJECT_DIR,\n// which names the MAIN tree: a cure run only in its own worktree cannot lift the block. It is a\n// REQUIRED parameter for the same reason `drifted` is — an optional flag would let a caller keep\n// emitting the main-loop text from a subagent, which is the case that most needs the extra line.\n// webpieces-disable no-function-outside-class -- pure string builder over exported constants; the single source of the self-guard deny text now that the sh copy is gone.\nexport function shimStaleDenyReason(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean): string {\n const verNote = installedVersion ? ` (installed version ${installedVersion})` : '';\n const what = drifted.join(', ');\n const safeRoot = root.replace(/[\"\\\\]/g, '');\n const projectDir = claudeEnv.projectDirForLog().replace(/[\"\\\\]/g, '');\n // Tested against the RAW root, never the stripped one: stripping is a display-safety measure, and\n // cd-anchoring to a path we just mangled would prescribe a cd into a directory that does not exist.\n // A root CD_PREFIX cannot express is simply not offered as a `cd` (raw ok ⇒ safeRoot === root).\n const cdOk = root !== '' && /^[A-Za-z0-9._/@~+-]+$/.test(root);\n // Agreement is the routine case; DISAGREEMENT is the signature of the session-root-vs-cwd split this\n // guard was rewritten to make unconstructible, so it gets said out loud rather than left to inference.\n const verdict = safeRoot === projectDir\n ? 'These two AGREE, so this is the ordinary case - the tree you are in is the tree being judged.'\n : 'These two DISAGREE - the tree being judged is NOT the one CLAUDE_PROJECT_DIR names, so cure the root= tree specifically and do not assume your current directory is it.';\n const rootNote = safeRoot === '' ? '' : ` WHERE THIS WAS MEASURED: root=${safeRoot} (the tree the RUNNING guard binary itself came from - that is the tree whose shim must change), projectDir=${projectDir} (CLAUDE_PROJECT_DIR as this process sees it; <unset> means the variable is absent, which is not the same as set-but-empty). ${verdict}`;\n const upgrade = cdOk ? `cd ${safeRoot} && ${UPGRADE_SHIM_CMD}` : UPGRADE_SHIM_CMD;\n // OPTION 2 is a relative-path `cp`, so it is even MORE cwd-sensitive than OPTION 1 — anchor it too.\n const restore = cdOk ? `cd ${safeRoot} && ${RESTORE_SHIM_CMD}` : RESTORE_SHIM_CMD;\n // The subagent sentence goes BEFORE the options, so it is read before a cure is chosen rather than\n // after one has already been run in the wrong tree.\n const subagentNote = inSubagent\n ? ' YOU ARE RUNNING IN A SUBAGENT: the hooks blocking you resolve through CLAUDE_PROJECT_DIR, which names the MAIN tree and not yours, so a cure run only here CANNOT lift this block - the MAIN tree is where pnpm install and the repair have to happen, because the hooks execute the release INSTALLED IN THAT TREE, not the one in this worktree. Running the repair in THIS worktree afterwards is also correct and is the aligned end state: it is what makes this tree right once its own branch is the one being judged.'\n : '';\n return `❌ webpieces-managed hook surface was changed: ${what} no longer matches what the INSTALLED @webpieces/ai-hook-rules${verNote} expects (reverted, hand-edited, or predating this binary - a settings.json still on an OLDER form, including the three-hook RELATIVE form with guarantee-root.sh, reports here too).${rootNote} webpieces manages THREE things together and they only work as a set: ${SHIM_MARKER} (the guard shim, registered ABSOLUTE via $CLAUDE_PROJECT_DIR so the MAIN tree governs every tree - a worktree never had its own binary anyway, it borrows the main tree's by walking up), the .claude/settings.json entries that register it, and the .claude/settings.json env entry ${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE} (which pins the Bash cwd to the project root, identically for every subagent because settings env is inherited). They are GENERATED and committed by webpieces - they must NOT be reverted or edited by hand, and the fail-closed logic cannot be trusted while any of them differs. Every OTHER tool call is blocked until all three match again. ${subagentNote} THIS IS NOT A DEADLOCK: both options below are explicitly ALLOWED through while this guard is up, so run one YOURSELF now - do not hand it back to the human. OPTION 1 (preferred, and the ONLY option that repairs all three - it regenerates the shim, rewrites the settings.json registration to the two-hook ABSOLUTE form (removing any older entries, including the retired guarantee-root.sh hook), AND sets the managed env entry; it touches no config, and it imports only fs/path so it runs on a broken tree) - run EXACTLY this command: '${upgrade}'. HOW TO TELL OPTION 1 WORKED, because up to and including 0.4.588 it silently did NOTHING (it had no process entry point, so it printed nothing and exited 0 - which is why this guard could look like a deadlock): a working OPTION 1 PRINTS a line per repaired surface and re-checks all three afterwards, exiting NON-ZERO and naming whatever still differs. So EMPTY OUTPUT means the cure did not run at all - do not re-run it in a loop; upgrade @webpieces/ai-hook-rules, use OPTION 2 for the shim, and say plainly that OPTION 1 is inert in this release. OPTION 2 (a PARTIAL fallback - it repairs ONE of the three, ${SHIM_MARKER}, and nothing else; pick it only when the installed @webpieces/ai-hook-rules is OLDER than 0.4.408 so wp-upgrade-shim does not exist yet, then upgrade @webpieces and run OPTION 1 to finish the job. Claude Code's own permission prompt may ask you to confirm the file overwrite, and that prompt is NOT this guard) - run EXACTLY this command: '${restore}'. Do NOT use the bare '${INSTALL_HOOKS_CMD}' here: it also migrates your config and PROMPTS for a hook target twice, which hangs a non-interactive session. ${NO_CHAINING_RULE} Do NOT revert these files again - if you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json instead.`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shim-deny-reason.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-deny-reason.ts"],"names":[],"mappings":"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","sourcesContent":["import { claudeEnv } from '@webpieces/rules-config';\n\nimport { INSTALL_HOOKS_CMD, RESTORE_SHIM_CMD, UPGRADE_SHIM_CMD } from './l0-allowlist';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { NO_CHAINING_RULE, SHIM_MARKER } from './shim';\n\n/**\n * THE FAIL-CLOSED DENY TEXT for a drifted managed hook surface (L0 fault S) — its own module because\n * shim.ts is at its line cap and this is one cohesive unit: the words a blocked agent reads, and\n * nothing else. It imports FROM shim.ts and is never imported BY it, so the graph stays acyclic.\n */\n// The fail-closed deny text for a drifted MANAGED HOOK SURFACE, built from the single-source cure\n// constants + NO_CHAINING_RULE.\n//\n// `drifted` names WHICH of the three managed things moved — .claude/webpieces/ai-hook.sh, the\n// .claude/settings.json hook registration, and its managed env entry\n// CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR (see hook-registration.ts). It is REQUIRED, not optional:\n// this used to be a shim-only message, and an optional list would let a caller silently keep emitting\n// the one-file text after the surface grew — which is the \"two spellings of one thing\" shape the\n// compatibility policy rejects.\n//\n// It was FOUR. guarantee-root.sh (L-1) is gone: the guard hooks are registered ABSOLUTE now, so the\n// launch guarantee L-1 provided is structural and there is no second .sh file to keep byte-locked.\n//\n// `installedVersion` names WHICH webpieces the cure re-arms to (the binary is that\n// version); pass '' to omit the note rather than print an empty one. `root` is the tree the deciding\n// binary GOVERNS (governingShimRoot) — naming it, and anchoring the cure to it with a leading\n// `cd <root> &&` (which CD_PREFIX_*_ANCHORED already tolerates, locked by a unit test), is what keeps\n// the cure curable when the AI's cwd is a DIFFERENT tree than the one being judged. Pass '' to omit.\n//\n// The cause list is deliberately a LIST: it used to assert flatly \"(it was reverted or hand-edited)\",\n// which is frequently FALSE — the common case is a shim whose logic simply predates this binary — and\n// that false certainty sent a real agent hunting for a tamper that never happened.\n//\n// WHERE IT WAS MEASURED, in the deny itself and not only in the logs. #574 put `root=` and\n// `projectDir=` on every L1 invocation line (see decision-log / ClaudeEnv.projectDirForLog, whose\n// `<unset>` token keeps \"variable absent\" distinguishable from \"set to empty\"). The log is forensics\n// AFTER the fact; the deny is what a blocked agent reads IN the moment, and the absence of exactly\n// these two fields is what sent a real agent chasing the wrong mechanism for four cures. Same field\n// names on purpose, so the deny text and the log lines grep together.\n//\n// THE MESSAGE DIET IS PART OF THE CONTRACT, and this deny is where it regresses. main landed a\n// deliberate L0 message diet (384cdae) and blocks grow straight back into a wall of text when each new\n// finding argues its case here; at eleven sections this one was ~4,000 chars, and `denyBudget` in\n// shim-deny-reason.spec.ts now fails the build if it climbs back. Keep what CHANGES what the reader\n// types (the two exact commands, that it is not a deadlock, the no-chaining rule, root=/projectDir=);\n// cut what merely argues. What was cut and why:\n// - the \"up to and including 0.4.588 OPTION 1 was inert, so EMPTY OUTPUT means it did not run\"\n// paragraph. It was UNREACHABLE by construction: this text and `wp-upgrade-shim` ship in the SAME\n// package at the SAME version, so a binary new enough to print this sentence necessarily has the\n// process entry point 0.4.589 added. The incident is still recorded where it can bite —\n// bin-process-entry.spec.ts, which asserts the entry point exists.\n// - the \"registered ABSOLUTE / a worktree borrows by walking up\" aside. That is the DRIFT fault's\n// subject (WP_BORROW_NOTE in shim.ts) and it is restated in the guard-matrix doc this deny points at.\n// - \"they are GENERATED and committed ... must NOT be reverted by hand\", which said twice over what\n// the closing sentence already says once.\n//\n// CONSTRAINT: the returned string must contain no `\"` and no `\\` — it is JSON-serialized by denyJson()\n// (a stray quote/backslash would corrupt the PreToolUse decision payload, not just the text). That is an\n// INVARIANT, not a hope, so every interpolated path is STRIPPED of both rather than trusted — a\n// Windows-style path or an odd directory name must not be able to corrupt the decision. Locked by unit\n// tests. An unusual root is also dropped from the `cd` cure rather than quoted (CD_PREFIX would reject it).\n//\n// `inSubagent` comes from the PreToolUse payload's `agent_id`, which Claude Code delivers on stdin and\n// populates ONLY off the main loop (main falls back to the session id, so the field is absent there).\n// `agent_type` is NOT usable for this — it is always populated and discriminates nothing. It is a\n// REQUIRED parameter for the same reason `drifted` is — an optional flag would let a caller keep\n// emitting the main-loop text from a subagent, which is the case that most needs the extra line.\n//\n// THE SUBAGENT SENTENCE REASONS FROM root= vs projectDir=, IT DOES NOT ASSERT A FIXED CONCLUSION. It\n// used to say flatly \"the hooks resolve through CLAUDE_PROJECT_DIR, which names the MAIN tree, so a cure\n// run only here CANNOT lift this block\". That is a POST-FLIP fact stated during the PRE-FLIP window, and\n// it is FALSE exactly when it fires: measured 2026-08-10, a worktree subagent hit fault S, ran OPTION 1\n// in ITS OWN worktree, and the block lifted — because the registration still in force was the RELATIVE\n// three-hook form, so the worktree's own ai-hook.sh ran against the worktree's own node_modules. The\n// deny's own `root=` field named the worktree while the sentence insisted otherwise.\n// WHICH tree to cure is already answered, for BOTH windows, by the root=/projectDir= verdict above:\n// committedShimStale compares shimPath(root) and `root` is the tree the RUNNING binary came from, so\n// repairing the root= tree clears the fault whichever registration form is live, and the cure is\n// cd-anchored there. Telling an agent its cure cannot work, while it demonstrably can, costs more than\n// saying nothing.\n//\n// SO THE SUBAGENT SENTENCE CARRIES THE PART THAT IS ACTUALLY SUBAGENT-SPECIFIC: there are TWO real\n// cures and they fix different things. A — run the printed cure here; it works, and it makes THIS tree\n// work NOW. B — a subagent cannot reach the main clone, so aligning the two trees is an ESCALATION\n// (\"ask the coordinator to run pnpm install in the main tree\"), and that is what stops the trees\n// disagreeing. Doing only A leaves the repo with two trees on two @webpieces releases — the live state\n// on 2026-08-10, main clone on 0.4.616 while origin/main and three worktrees were on 0.4.624. And the\n// rule it states is deliberately NOT \"do not install in a worktree\": a worktree NEEDS its own\n// node_modules (nx, vitest and the eslint plugin all execute there and load from it). The rule is that\n// its @webpieces must EQUAL the main tree's — the older WP_BORROW_NOTE wording got this backwards in\n// both directions at different times.\n// webpieces-disable no-function-outside-class -- pure string builder over exported constants; the single source of the self-guard deny text now that the sh copy is gone.\nexport function shimStaleDenyReason(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean): string {\n const verNote = installedVersion ? ` (installed version ${installedVersion})` : '';\n const what = drifted.join(', ');\n const safeRoot = root.replace(/[\"\\\\]/g, '');\n const projectDir = claudeEnv.projectDirForLog().replace(/[\"\\\\]/g, '');\n // Tested against the RAW root, never the stripped one: stripping is a display-safety measure, and\n // cd-anchoring to a path we just mangled would prescribe a cd into a directory that does not exist.\n // A root CD_PREFIX cannot express is simply not offered as a `cd` (raw ok ⇒ safeRoot === root).\n const cdOk = root !== '' && /^[A-Za-z0-9._/@~+-]+$/.test(root);\n // Agreement is the routine case; DISAGREEMENT is the signature of the session-root-vs-cwd split this\n // guard was rewritten to make unconstructible, so it gets said out loud rather than left to inference.\n const verdict = safeRoot === projectDir\n ? 'These two AGREE, so this is the ordinary case - the tree you are in is the tree being judged.'\n : 'These two DISAGREE - the tree being judged is NOT the one CLAUDE_PROJECT_DIR names, so cure the root= tree and do not assume your cwd is it.';\n const rootNote = safeRoot === '' ? '' : ` WHERE THIS WAS MEASURED: root=${safeRoot} (the tree whose shim was compared - the one to repair), projectDir=${projectDir} (CLAUDE_PROJECT_DIR as this process sees it; <unset> = absent, not set-but-empty). ${verdict}`;\n const upgrade = cdOk ? `cd ${safeRoot} && ${UPGRADE_SHIM_CMD}` : UPGRADE_SHIM_CMD;\n // OPTION 2 is a relative-path `cp`, so it is even MORE cwd-sensitive than OPTION 1 — anchor it too.\n const restore = cdOk ? `cd ${safeRoot} && ${RESTORE_SHIM_CMD}` : RESTORE_SHIM_CMD;\n // The subagent sentence goes BEFORE the options, so it is read before a cure is chosen rather than\n // after one has already been run in the wrong tree. WHICH tree is already answered by `verdict`\n // above, from root= vs projectDir=; this adds only what is specific to a subagent — that A and B are\n // BOTH real and fix different things, and that B is an ESCALATION because a subagent cannot reach\n // the main clone.\n const subagentNote = inSubagent\n ? ' YOU ARE RUNNING IN A SUBAGENT, so TWO cures are real and they fix DIFFERENT things: A makes THIS tree work now, B stops the two trees disagreeing. A - run the OPTION below exactly as printed; it is already anchored to the tree that must change, and running it from here DOES lift this block (measured), so never conclude a local cure cannot work. B - you cannot reach the main clone, so ESCALATE: ask the coordinator to run pnpm install in the main tree so both trees are on the same @webpieces version. A worktree NEEDS its own node_modules (nx, vitest and the eslint plugin all load from it); the rule is not no-install-here, it is that this tree @webpieces must EQUAL the main tree.'\n : '';\n return `❌ webpieces-managed hook surface was changed: ${what} no longer matches what the INSTALLED @webpieces/ai-hook-rules${verNote} expects (reverted, hand-edited, or predating this binary).${rootNote} webpieces manages THREE things and they only work as a set: ${SHIM_MARKER} (the guard shim), the .claude/settings.json entries that register it, and the .claude/settings.json env entry ${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE} (which pins the Bash cwd to the project root, identically for every subagent because settings env is inherited). Every OTHER tool call is blocked until all three match again.${subagentNote} THIS IS NOT A DEADLOCK: both options below are explicitly ALLOWED through while this guard is up, so run one YOURSELF now - do not hand it back to the human. OPTION 1 (preferred - the only one that repairs all three, and it runs on a broken tree) - run EXACTLY this command: '${upgrade}'. OPTION 2 (PARTIAL - repairs ${SHIM_MARKER} only; pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where OPTION 1 does not exist yet, then upgrade and run OPTION 1) - run EXACTLY this command: '${restore}'. Do NOT use the bare '${INSTALL_HOOKS_CMD}' here: it also migrates your config and PROMPTS for a hook target twice, which hangs a non-interactive session. ${NO_CHAINING_RULE} If you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json rather than reverting these files.`;\n}\n"]}
|
package/src/bin/shim.js
CHANGED
|
@@ -197,7 +197,7 @@ WP_INSTALL_CMD="pnpm install"
|
|
|
197
197
|
WP_BORROW_NOTE=""
|
|
198
198
|
if [ "$BIN_ROOT" != "$ROOT" ]; then
|
|
199
199
|
WP_INSTALL_CMD="cd $ROOT && pnpm install"
|
|
200
|
-
WP_BORROW_NOTE=" NOTE: this tree ($ROOT) has NO node_modules of its own, so the guard binary was inherited from $BIN_ROOT by walking up.
|
|
200
|
+
WP_BORROW_NOTE=" NOTE: this tree ($ROOT) has NO node_modules of its own, so the guard binary was inherited from $BIN_ROOT by walking up. TWO cures are real and they fix DIFFERENT things: A makes THIS tree work now, B stops the two trees disagreeing. A - run the command above HERE. That is legitimate and it does work; a worktree NEEDS its own node_modules anyway (nx, vitest and the eslint plugin all execute in this tree and load from it). B - get $BIN_ROOT onto the same @webpieces version: put both trees on the same git hash (the pin is tracked) and run ONE 'pnpm install' there. If you are a SUBAGENT you cannot reach that tree, so ESCALATE - ask the coordinator to run 'pnpm install' in the main tree so both trees are on the same @webpieces version. The rule is NOT no-install-here: it is that this tree's @webpieces must EQUAL $BIN_ROOT's, because doing only A leaves two trees on two releases (the trinary-version-skew guard then BLOCKS rather than letting it pass unnoticed). Adding an ordinary third-party dependency here changes none of that - only a differing @webpieces version does. If this tree genuinely needs a DIFFERENT version, use a separate clone rather than a worktree."
|
|
201
201
|
fi`;
|
|
202
202
|
// Shell fragment: run the installed guard bin and INSPECT its outcome, instead of exec'ing it.
|
|
203
203
|
//
|