@webpieces/ai-hook-rules 0.4.731 → 0.4.733

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/ai-hook-rules",
3
- "version": "0.4.731",
3
+ "version": "0.4.733",
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.731",
28
+ "@webpieces/rules-config": "0.4.733",
29
29
  "inversify": "7.10.4",
30
30
  "reflect-metadata": "0.2.2"
31
31
  },
@@ -29,6 +29,20 @@ const to_error_1 = require("../core/to-error");
29
29
  // every subagent, so a verdict never depends on where an earlier `cd` left the shell. Claude-only:
30
30
  // Codex has no settings `env`, and its cwd is measured not to drift
31
31
  //
32
+ // WHAT IT DELIBERATELY DOES *NOT* DO: sweep this tree's dangling `node_modules/.bin/wp-*` symlinks. That
33
+ // is the same class of defect as the retired-file removal below — an entry pointing at a missing file is
34
+ // worse than absence, because `ls node_modules/.bin` advertises a capability that only fails on execution
35
+ // — and it WOULD read naturally here. It is not here because the one implementation of that sweep lives in
36
+ // `@webpieces/rules-config` (`stale-bin-sweep.ts`), which this module may not import: the barrel pulls in
37
+ // inversify and the config loader, and a subpath import resolves against the INSTALLED rules-config, which
38
+ // is a release behind this source (see .claude/rules/published-vs-local-source.md) — so a spawned
39
+ // `wp-upgrade-shim` would die on module resolution, in the one command an L0-blocked session has left. A
40
+ // second copy of the sweep here would be a second spelling of it, which is worse again.
41
+ //
42
+ // It does not need to be here. The sweep rides the `wp-*` startup pass that regenerates
43
+ // `.webpieces/instruct-ai/*`, so ANY `wp-*` command heals the tree — including the `pnpm install` +
44
+ // `pnpm exec wp-upgrade-shim` sequence this bin's own advice ends with.
45
+ //
32
46
  // It also DELETES the retired `.claude/webpieces/guarantee-root.sh` and any settings entry still naming
33
47
  // it. That file was the L-1 hook, which existed only to guarantee the once-RELATIVE shim path resolved;
34
48
  // an absolute path resolves from any cwd, so it has no job left. Removing the file without removing the
@@ -1 +1 @@
1
- {"version":3,"file":"upgrade-shim.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/upgrade-shim.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { renderShim, shimPath, findShimRoot } from './shim';\nimport { repairRegistrationAt, managedSurfaceDrift, SettingsRepair, LEGACY_GUARANTEE_ROOT_MARKER } from './hook-registration';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { toError } from '../core/to-error';\n\n// ---------------------------------------------------------------------------\n// The `wp-upgrade-shim` entry point — the CURE for the managed-hook-surface self-guard (L0 fault S).\n//\n// WHAT IT REPAIRS, and why all of them (2026-08-07, extended). This used to write EXACTLY ONE FILE,\n// ai-hook.sh, and touch nothing else. That was correct while the installed surface WAS one file. It is\n// now four (the name `wp-upgrade-shim` is older than the job and is NOT renamed — a rename with no\n// functional change is a cost with no payer; the prose is what gets corrected):\n//\n// 1. .claude/webpieces/ai-hook.sh the ONE guard shim, shared by every harness and registered\n// ABSOLUTE, so the MAIN tree governs every tree\n// 2. the .claude/settings.json registration itself\n// 3. the .codex/hooks.json registration — the SAME two hooks under Codex's own matchers and its own\n// $PWD anchor. Repaired only where it already EXISTS: arming a harness is the installer's decision,\n// this is the cure for one that has drifted\n// 4. the .claude/settings.json `env` entry CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR=1, which pins the\n// Bash cwd to the project root — and, because settings `env` is inherited, pins it identically for\n// every subagent, so a verdict never depends on where an earlier `cd` left the shell. Claude-only:\n// Codex has no settings `env`, and its cwd is measured not to drift\n//\n// It also DELETES the retired `.claude/webpieces/guarantee-root.sh` and any settings entry still naming\n// it. That file was the L-1 hook, which existed only to guarantee the once-RELATIVE shim path resolved;\n// an absolute path resolves from any cwd, so it has no job left. Removing the file without removing the\n// entry (or vice versa) is the worst possible half-state — a registered hook pointing at a missing file\n// exits 127, which per the hooks reference is a NON-BLOCKING error, i.e. a SILENT UNGUARDED ALLOW — so\n// both happen here, file first.\n//\n// A cure that fixes some of them is worse than no cure, because it reports success. This bin is the\n// sanctioned cure named in fault S's message and is on the L0 allowlist, so it must repair everything.\n//\n// Deliberately imports only ./shim and ./hook-registration (fs + path) + toError,\n// exactly like install-entry: the whole job is to rewrite webpieces-managed files, which never needed\n// the rule engine, and it must stay runnable on a tree too broken to load it.\n// ---------------------------------------------------------------------------\nconst RED = '[31;1m';\nconst RESET = '[0m';\n\n// Returns the process exit code (0 = ok). Kept as a function (not top-level code) so it is unit-testable\n// without spawning node.\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header), mirroring install-entry.ts. A DI-managed class would pull the container in and reintroduce the require-time crash this dependency-free path exists to survive.\nexport function runUpgradeShim(cwd: string): number {\n const root = findShimRoot(cwd);\n if (root === null) {\n console.error(`${RED}🛑 @webpieces: no committed .claude/webpieces/ai-hook.sh found to regenerate.${RESET}`);\n console.error(' Run this from a repo that installs @webpieces/ai-hook-rules, or run the installer (pnpm wp-install-ai-hooks).');\n return 1;\n }\n const target = shimPath(root);\n // webpieces-disable no-unmanaged-exceptions -- bin entry chokepoint: turn an fs error into an actionable line + non-zero exit rather than a raw node trace; there is no caller above a bin to handle it.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode only applies on create; force it on overwrite too (matches writeShim).\n fs.chmodSync(target, 0o755);\n // ORDER MATTERS, and it is registration-FIRST. The reverse leaves a window in which a settings\n // entry still names a file that is gone — exit 127, which the hooks reference defines as a\n // NON-BLOCKING error, i.e. a SILENT UNGUARDED ALLOW. This way round the transient state is an\n // ORPHANED FILE that nothing references, which is inert. (An earlier draft of this function\n // argued file-first was safer; it is not. repairRegistration() also early-returns when the file\n // registers no guard bins, and the retired H1 command contains neither bin name, so a\n // guarantee-root-ONLY settings file would have lost the file and kept the entry.)\n const repairs = repairRegistrationAt(root);\n const removedLegacy = removeRetiredGuaranteeRoot(root);\n reportRepairs(target, removedLegacy, repairs);\n // ADVISORY ONLY, and deliberately after the ✅ lines: it never touches the exit code (see\n // reportTreeDivergence).\n reportTreeDivergence(root);\n return verifyRepaired(root);\n } catch (err: unknown) {\n const error = toError(err);\n console.error(`${RED}🛑 @webpieces: could not write under ${root}: ${error.message}${RESET}`);\n return 1;\n }\n}\n\n/**\n * Say what was actually done, per managed thing. The old single line (\"regenerated the managed shim\")\n * would now be a lie by omission on the three most important repairs — and an agent reading a cure's\n * output is how it decides whether the cure worked.\n *\n * Each settings file reports the repairs IT needed, from the flags recorded before the rewrite. Printing\n * \"rewrote the hook registration\" for a file whose hooks were already current and whose `env` entry was\n * the only thing missing would be the same class of dishonesty one level down.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\n/**\n * Delete the RETIRED L-1 hook file, returning whether there was one. Removal-only, one way, no writer.\n *\n * This runs AFTER repairRegistrationAt(), which strips the stale H1 ENTRY from settings.json. Removing\n * the entry first means the file is merely orphaned in between — inert, because nothing references it.\n * File-first would instead leave a registered hook pointing at a missing file, and per the hooks\n * reference a non-2 non-zero exit is a NON-BLOCKING error: every `cd` would go unjudged.\n */\n// webpieces-disable no-function-outside-class -- module-scope like every other helper in this bin, which must load on a tree too broken to build a DI container\nfunction removeRetiredGuaranteeRoot(root: string): boolean {\n // The path comes from LEGACY_GUARANTEE_ROOT_MARKER, never re-spelled here: a second literal is a\n // second spelling, and when the expiry in hook-registration.ts fires the documented removal would\n // miss this copy and the dead name would survive in a file nobody thought to grep.\n const legacy = path.join(root, LEGACY_GUARANTEE_ROOT_MARKER);\n if (!fs.existsSync(legacy)) return false;\n fs.rmSync(legacy, { force: true });\n return true;\n}\n\n// webpieces-disable no-function-outside-class -- module-scope like every other helper in this bin, which must load on a tree too broken to build a DI container\nfunction reportRepairs(shimFile: string, removedLegacy: boolean, repairs: readonly SettingsRepair[]): void {\n console.log(`✅ @webpieces: regenerated the managed shim at ${shimFile} — tool calls are re-armed.`);\n if (removedLegacy) {\n console.log('✅ @webpieces: deleted the RETIRED L-1 hook .claude/webpieces/guarantee-root.sh.');\n console.log(' The guard hooks are ABSOLUTE now, so the launch guarantee it provided is structural.');\n }\n if (repairs.length === 0) {\n console.log(' .claude/settings.json (hook registration + managed env) already matches this release — no change.');\n }\n for (const repair of repairs) {\n if (repair.registration) {\n console.log(`✅ @webpieces: rewrote the hook registration in ${repair.settingsPath} to the two-hook form`);\n console.log(' (both guard hooks ABSOLUTE via $CLAUDE_PROJECT_DIR, so the MAIN tree governs every tree;');\n console.log(' any retired guarantee-root.sh entry was removed).');\n }\n if (repair.env) {\n console.log(`✅ @webpieces: set env.${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE} in ${repair.settingsPath}`);\n console.log(' (pins the Bash cwd to the project root, so the guard hooks resolve identically for every subagent — for');\n console.log(' this session and, because settings env is inherited, for every subagent it spawns).');\n }\n }\n console.log(' These files are generated + committed by webpieces; do not revert or hand-edit them.');\n}\n\n/**\n * WAS THE REPAIRED TREE THE TREE THE HOOKS LAUNCH FROM — the second way this cure can report success\n * while changing nothing the session is actually governed by.\n *\n * H1 is registered ABSOLUTE, `sh \"$CLAUDE_PROJECT_DIR/…\"`, and `$CLAUDE_PROJECT_DIR` never moves off the\n * PRIMARY clone (the two-tree straddle recorded in shim.ts, and the whole reason H2/H3 are relative\n * while H1 is not). So repairing a LINKED WORKTREE leaves the running session still loading the\n * PRIMARY's files, the PRIMARY's binary and the PRIMARY's pin. Four green lines, and the block does not\n * lift. Nothing printed above is false — but the question the reader has (\"will the block lift?\") went\n * unanswered, which is the same failure this file's header exists to prevent, one level out.\n *\n * THE PREDICATE IS TREE DIVERGENCE, NOT \"am I a subagent\". There is no runtime subagent marker in the\n * hook environment to read, and divergence is the more accurate question anyway: a MAIN agent in a\n * linked worktree HAS this problem (a subagent test would miss it), and a SUBAGENT in the same tree does\n * NOT (a subagent test would cry wolf). Both paths are realpath'd before comparing — a worktree path can\n * arrive symlinked, and /tmp vs /private/tmp on darwin is a live case in this repo's own specs.\n *\n * SILENT when `$CLAUDE_PROJECT_DIR` is unset: a plain CLI run outside Claude Code has no second tree to\n * talk about. And ADVISORY always — it must never turn a verified repair into a failure, so it returns\n * nothing and `verifyRepaired()`'s contract (non-zero only when a surface in THIS tree still differs) is\n * untouched.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction reportTreeDivergence(root: string): void {\n const projectDir = process.env['CLAUDE_PROJECT_DIR'];\n if (projectDir === undefined || projectDir === '') return;\n if (sameTree(root, projectDir)) return;\n console.log('');\n console.log('⚠️ @webpieces: the tree just repaired is NOT the tree the hooks launch from.');\n console.log(` repaired: ${root}`);\n console.log(` CLAUDE_PROJECT_DIR: ${projectDir}`);\n console.log(' The hooks governing this session resolve through CLAUDE_PROJECT_DIR (H1 is registered');\n console.log(' absolute), so this repair has not changed what is currently enforcing — it made THIS');\n console.log(' tree correct for when its own branch is the one being judged, which is not wasted work.');\n console.log(' To change what is enforcing NOW, run the same repair in the primary tree, and install');\n console.log(' there too — the hooks execute the INSTALLED release, not this tree\\'s source:');\n console.log(` cd ${projectDir} && pnpm install && pnpm exec wp-upgrade-shim`);\n console.log(' Repaired in BOTH trees is the aligned end state, and running it twice is safe.');\n}\n\n/**\n * Do two paths name the same tree? realpath'd (symlinked worktrees, /tmp vs /private/tmp) and stripped\n * of a trailing separator before comparing. A path that cannot be realpath'd falls back to `resolve`,\n * so an absent CLAUDE_PROJECT_DIR directory reads as \"different\" rather than throwing inside a cure.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction sameTree(a: string, b: string): boolean {\n return canonicalTree(a) === canonicalTree(b);\n}\n\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction canonicalTree(dir: string): string {\n // webpieces-disable no-unmanaged-exceptions -- realpath throws on a path that does not exist; the fallback IS the handling, and an advisory notice must never crash the cure it annotates.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return path.resolve(fs.realpathSync(dir));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: an unresolvable path simply compares as itself\n return path.resolve(dir);\n }\n}\n\n/**\n * DID THE CURE ACTUALLY CURE IT — asked of the same predicate the guard asks, not of our own writes.\n *\n * A cure that cannot fail loudly is worse than no cure. This bin used to print three ✅ lines and\n * return 0 the moment `writeFileSync` did not throw, which asserts only \"the bytes we chose were\n * written\", never \"the surface the guard measures now agrees\". Fault S blocks EVERY tool call, so the\n * one thing a blocked agent must be able to trust is whether the block will lift — and a success line\n * that is not backed by the guard's own check is exactly the false certainty that leaves it retrying a\n * cure that cannot work. So re-run `managedSurfaceDrift()`, the very function `enforceCommittedShim()`\n * calls, and return NON-ZERO naming whatever still differs.\n *\n * Measured against `root` (the tree we just repaired), not `governingShimRoot()` (the tree the running\n * binary came from). Those differ when the cure is run across trees, and the honest claim here is about\n * the files this invocation wrote.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction verifyRepaired(root: string): number {\n const stillDrifted = managedSurfaceDrift(root);\n if (stillDrifted.length === 0) return 0;\n console.error(`${RED}🛑 @webpieces: the repair ran but ${stillDrifted.length} managed surface(s) STILL differ: ${stillDrifted.join(', ')}.${RESET}`);\n console.error(` The guard will keep blocking. This is a webpieces bug or an unwritable tree under ${root} - do not retry this command in a loop; report it with the list above.`);\n return 1;\n}\n\n/**\n * THE PROCESS ENTRY POINT — the thing whose absence made this whole bin a lie.\n *\n * Up to and including 0.4.588 this module ENDED at the closing brace above. `pnpm exec wp-upgrade-shim`\n * loaded it, defined two functions, and exited 0 having printed nothing and changed no file. Fault S\n * names this command as OPTION 1, the only option that repairs every managed surface, so the\n * guard's own \"THIS IS NOT A DEADLOCK\" promise was false: OPTION 2 repairs one of three, and OPTION 1\n * did nothing at all. Twenty-one unit tests missed it because every one of them called\n * `runUpgradeShim()` as a FUNCTION — the defect lived entirely in what the module does when SPAWNED.\n *\n * `runMain` from @webpieces/rules-config is the repo-wide wrapper and is deliberately NOT used here:\n * this bin must load with fs+path only (see the header) so it still runs on the broken tree it exists\n * to repair. `main()` is the sanctioned exit site instead, and `bin-process-entry.spec.ts` spawns\n * this file as a process so a future refactor cannot silently drop the launcher again.\n */\n// webpieces-disable no-function-outside-class -- bin entry point in this deliberately dependency-free module; see header\nexport function main(): void {\n process.exit(runUpgradeShim(process.cwd()));\n}\n\nif (require.main === module) {\n main();\n}\n"]}
1
+ {"version":3,"file":"upgrade-shim.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/upgrade-shim.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { renderShim, shimPath, findShimRoot } from './shim';\nimport { repairRegistrationAt, managedSurfaceDrift, SettingsRepair, LEGACY_GUARANTEE_ROOT_MARKER } from './hook-registration';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { toError } from '../core/to-error';\n\n// ---------------------------------------------------------------------------\n// The `wp-upgrade-shim` entry point — the CURE for the managed-hook-surface self-guard (L0 fault S).\n//\n// WHAT IT REPAIRS, and why all of them (2026-08-07, extended). This used to write EXACTLY ONE FILE,\n// ai-hook.sh, and touch nothing else. That was correct while the installed surface WAS one file. It is\n// now four (the name `wp-upgrade-shim` is older than the job and is NOT renamed — a rename with no\n// functional change is a cost with no payer; the prose is what gets corrected):\n//\n// 1. .claude/webpieces/ai-hook.sh the ONE guard shim, shared by every harness and registered\n// ABSOLUTE, so the MAIN tree governs every tree\n// 2. the .claude/settings.json registration itself\n// 3. the .codex/hooks.json registration — the SAME two hooks under Codex's own matchers and its own\n// $PWD anchor. Repaired only where it already EXISTS: arming a harness is the installer's decision,\n// this is the cure for one that has drifted\n// 4. the .claude/settings.json `env` entry CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR=1, which pins the\n// Bash cwd to the project root — and, because settings `env` is inherited, pins it identically for\n// every subagent, so a verdict never depends on where an earlier `cd` left the shell. Claude-only:\n// Codex has no settings `env`, and its cwd is measured not to drift\n//\n// WHAT IT DELIBERATELY DOES *NOT* DO: sweep this tree's dangling `node_modules/.bin/wp-*` symlinks. That\n// is the same class of defect as the retired-file removal below — an entry pointing at a missing file is\n// worse than absence, because `ls node_modules/.bin` advertises a capability that only fails on execution\n// — and it WOULD read naturally here. It is not here because the one implementation of that sweep lives in\n// `@webpieces/rules-config` (`stale-bin-sweep.ts`), which this module may not import: the barrel pulls in\n// inversify and the config loader, and a subpath import resolves against the INSTALLED rules-config, which\n// is a release behind this source (see .claude/rules/published-vs-local-source.md) — so a spawned\n// `wp-upgrade-shim` would die on module resolution, in the one command an L0-blocked session has left. A\n// second copy of the sweep here would be a second spelling of it, which is worse again.\n//\n// It does not need to be here. The sweep rides the `wp-*` startup pass that regenerates\n// `.webpieces/instruct-ai/*`, so ANY `wp-*` command heals the tree — including the `pnpm install` +\n// `pnpm exec wp-upgrade-shim` sequence this bin's own advice ends with.\n//\n// It also DELETES the retired `.claude/webpieces/guarantee-root.sh` and any settings entry still naming\n// it. That file was the L-1 hook, which existed only to guarantee the once-RELATIVE shim path resolved;\n// an absolute path resolves from any cwd, so it has no job left. Removing the file without removing the\n// entry (or vice versa) is the worst possible half-state — a registered hook pointing at a missing file\n// exits 127, which per the hooks reference is a NON-BLOCKING error, i.e. a SILENT UNGUARDED ALLOW — so\n// both happen here, file first.\n//\n// A cure that fixes some of them is worse than no cure, because it reports success. This bin is the\n// sanctioned cure named in fault S's message and is on the L0 allowlist, so it must repair everything.\n//\n// Deliberately imports only ./shim and ./hook-registration (fs + path) + toError,\n// exactly like install-entry: the whole job is to rewrite webpieces-managed files, which never needed\n// the rule engine, and it must stay runnable on a tree too broken to load it.\n// ---------------------------------------------------------------------------\nconst RED = '[31;1m';\nconst RESET = '[0m';\n\n// Returns the process exit code (0 = ok). Kept as a function (not top-level code) so it is unit-testable\n// without spawning node.\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header), mirroring install-entry.ts. A DI-managed class would pull the container in and reintroduce the require-time crash this dependency-free path exists to survive.\nexport function runUpgradeShim(cwd: string): number {\n const root = findShimRoot(cwd);\n if (root === null) {\n console.error(`${RED}🛑 @webpieces: no committed .claude/webpieces/ai-hook.sh found to regenerate.${RESET}`);\n console.error(' Run this from a repo that installs @webpieces/ai-hook-rules, or run the installer (pnpm wp-install-ai-hooks).');\n return 1;\n }\n const target = shimPath(root);\n // webpieces-disable no-unmanaged-exceptions -- bin entry chokepoint: turn an fs error into an actionable line + non-zero exit rather than a raw node trace; there is no caller above a bin to handle it.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode only applies on create; force it on overwrite too (matches writeShim).\n fs.chmodSync(target, 0o755);\n // ORDER MATTERS, and it is registration-FIRST. The reverse leaves a window in which a settings\n // entry still names a file that is gone — exit 127, which the hooks reference defines as a\n // NON-BLOCKING error, i.e. a SILENT UNGUARDED ALLOW. This way round the transient state is an\n // ORPHANED FILE that nothing references, which is inert. (An earlier draft of this function\n // argued file-first was safer; it is not. repairRegistration() also early-returns when the file\n // registers no guard bins, and the retired H1 command contains neither bin name, so a\n // guarantee-root-ONLY settings file would have lost the file and kept the entry.)\n const repairs = repairRegistrationAt(root);\n const removedLegacy = removeRetiredGuaranteeRoot(root);\n reportRepairs(target, removedLegacy, repairs);\n // ADVISORY ONLY, and deliberately after the ✅ lines: it never touches the exit code (see\n // reportTreeDivergence).\n reportTreeDivergence(root);\n return verifyRepaired(root);\n } catch (err: unknown) {\n const error = toError(err);\n console.error(`${RED}🛑 @webpieces: could not write under ${root}: ${error.message}${RESET}`);\n return 1;\n }\n}\n\n/**\n * Say what was actually done, per managed thing. The old single line (\"regenerated the managed shim\")\n * would now be a lie by omission on the three most important repairs — and an agent reading a cure's\n * output is how it decides whether the cure worked.\n *\n * Each settings file reports the repairs IT needed, from the flags recorded before the rewrite. Printing\n * \"rewrote the hook registration\" for a file whose hooks were already current and whose `env` entry was\n * the only thing missing would be the same class of dishonesty one level down.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\n/**\n * Delete the RETIRED L-1 hook file, returning whether there was one. Removal-only, one way, no writer.\n *\n * This runs AFTER repairRegistrationAt(), which strips the stale H1 ENTRY from settings.json. Removing\n * the entry first means the file is merely orphaned in between — inert, because nothing references it.\n * File-first would instead leave a registered hook pointing at a missing file, and per the hooks\n * reference a non-2 non-zero exit is a NON-BLOCKING error: every `cd` would go unjudged.\n */\n// webpieces-disable no-function-outside-class -- module-scope like every other helper in this bin, which must load on a tree too broken to build a DI container\nfunction removeRetiredGuaranteeRoot(root: string): boolean {\n // The path comes from LEGACY_GUARANTEE_ROOT_MARKER, never re-spelled here: a second literal is a\n // second spelling, and when the expiry in hook-registration.ts fires the documented removal would\n // miss this copy and the dead name would survive in a file nobody thought to grep.\n const legacy = path.join(root, LEGACY_GUARANTEE_ROOT_MARKER);\n if (!fs.existsSync(legacy)) return false;\n fs.rmSync(legacy, { force: true });\n return true;\n}\n\n// webpieces-disable no-function-outside-class -- module-scope like every other helper in this bin, which must load on a tree too broken to build a DI container\nfunction reportRepairs(shimFile: string, removedLegacy: boolean, repairs: readonly SettingsRepair[]): void {\n console.log(`✅ @webpieces: regenerated the managed shim at ${shimFile} — tool calls are re-armed.`);\n if (removedLegacy) {\n console.log('✅ @webpieces: deleted the RETIRED L-1 hook .claude/webpieces/guarantee-root.sh.');\n console.log(' The guard hooks are ABSOLUTE now, so the launch guarantee it provided is structural.');\n }\n if (repairs.length === 0) {\n console.log(' .claude/settings.json (hook registration + managed env) already matches this release — no change.');\n }\n for (const repair of repairs) {\n if (repair.registration) {\n console.log(`✅ @webpieces: rewrote the hook registration in ${repair.settingsPath} to the two-hook form`);\n console.log(' (both guard hooks ABSOLUTE via $CLAUDE_PROJECT_DIR, so the MAIN tree governs every tree;');\n console.log(' any retired guarantee-root.sh entry was removed).');\n }\n if (repair.env) {\n console.log(`✅ @webpieces: set env.${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE} in ${repair.settingsPath}`);\n console.log(' (pins the Bash cwd to the project root, so the guard hooks resolve identically for every subagent — for');\n console.log(' this session and, because settings env is inherited, for every subagent it spawns).');\n }\n }\n console.log(' These files are generated + committed by webpieces; do not revert or hand-edit them.');\n}\n\n/**\n * WAS THE REPAIRED TREE THE TREE THE HOOKS LAUNCH FROM — the second way this cure can report success\n * while changing nothing the session is actually governed by.\n *\n * H1 is registered ABSOLUTE, `sh \"$CLAUDE_PROJECT_DIR/…\"`, and `$CLAUDE_PROJECT_DIR` never moves off the\n * PRIMARY clone (the two-tree straddle recorded in shim.ts, and the whole reason H2/H3 are relative\n * while H1 is not). So repairing a LINKED WORKTREE leaves the running session still loading the\n * PRIMARY's files, the PRIMARY's binary and the PRIMARY's pin. Four green lines, and the block does not\n * lift. Nothing printed above is false — but the question the reader has (\"will the block lift?\") went\n * unanswered, which is the same failure this file's header exists to prevent, one level out.\n *\n * THE PREDICATE IS TREE DIVERGENCE, NOT \"am I a subagent\". There is no runtime subagent marker in the\n * hook environment to read, and divergence is the more accurate question anyway: a MAIN agent in a\n * linked worktree HAS this problem (a subagent test would miss it), and a SUBAGENT in the same tree does\n * NOT (a subagent test would cry wolf). Both paths are realpath'd before comparing — a worktree path can\n * arrive symlinked, and /tmp vs /private/tmp on darwin is a live case in this repo's own specs.\n *\n * SILENT when `$CLAUDE_PROJECT_DIR` is unset: a plain CLI run outside Claude Code has no second tree to\n * talk about. And ADVISORY always — it must never turn a verified repair into a failure, so it returns\n * nothing and `verifyRepaired()`'s contract (non-zero only when a surface in THIS tree still differs) is\n * untouched.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction reportTreeDivergence(root: string): void {\n const projectDir = process.env['CLAUDE_PROJECT_DIR'];\n if (projectDir === undefined || projectDir === '') return;\n if (sameTree(root, projectDir)) return;\n console.log('');\n console.log('⚠️ @webpieces: the tree just repaired is NOT the tree the hooks launch from.');\n console.log(` repaired: ${root}`);\n console.log(` CLAUDE_PROJECT_DIR: ${projectDir}`);\n console.log(' The hooks governing this session resolve through CLAUDE_PROJECT_DIR (H1 is registered');\n console.log(' absolute), so this repair has not changed what is currently enforcing — it made THIS');\n console.log(' tree correct for when its own branch is the one being judged, which is not wasted work.');\n console.log(' To change what is enforcing NOW, run the same repair in the primary tree, and install');\n console.log(' there too — the hooks execute the INSTALLED release, not this tree\\'s source:');\n console.log(` cd ${projectDir} && pnpm install && pnpm exec wp-upgrade-shim`);\n console.log(' Repaired in BOTH trees is the aligned end state, and running it twice is safe.');\n}\n\n/**\n * Do two paths name the same tree? realpath'd (symlinked worktrees, /tmp vs /private/tmp) and stripped\n * of a trailing separator before comparing. A path that cannot be realpath'd falls back to `resolve`,\n * so an absent CLAUDE_PROJECT_DIR directory reads as \"different\" rather than throwing inside a cure.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction sameTree(a: string, b: string): boolean {\n return canonicalTree(a) === canonicalTree(b);\n}\n\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction canonicalTree(dir: string): string {\n // webpieces-disable no-unmanaged-exceptions -- realpath throws on a path that does not exist; the fallback IS the handling, and an advisory notice must never crash the cure it annotates.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return path.resolve(fs.realpathSync(dir));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: an unresolvable path simply compares as itself\n return path.resolve(dir);\n }\n}\n\n/**\n * DID THE CURE ACTUALLY CURE IT — asked of the same predicate the guard asks, not of our own writes.\n *\n * A cure that cannot fail loudly is worse than no cure. This bin used to print three ✅ lines and\n * return 0 the moment `writeFileSync` did not throw, which asserts only \"the bytes we chose were\n * written\", never \"the surface the guard measures now agrees\". Fault S blocks EVERY tool call, so the\n * one thing a blocked agent must be able to trust is whether the block will lift — and a success line\n * that is not backed by the guard's own check is exactly the false certainty that leaves it retrying a\n * cure that cannot work. So re-run `managedSurfaceDrift()`, the very function `enforceCommittedShim()`\n * calls, and return NON-ZERO naming whatever still differs.\n *\n * Measured against `root` (the tree we just repaired), not `governingShimRoot()` (the tree the running\n * binary came from). Those differ when the cure is run across trees, and the honest claim here is about\n * the files this invocation wrote.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction verifyRepaired(root: string): number {\n const stillDrifted = managedSurfaceDrift(root);\n if (stillDrifted.length === 0) return 0;\n console.error(`${RED}🛑 @webpieces: the repair ran but ${stillDrifted.length} managed surface(s) STILL differ: ${stillDrifted.join(', ')}.${RESET}`);\n console.error(` The guard will keep blocking. This is a webpieces bug or an unwritable tree under ${root} - do not retry this command in a loop; report it with the list above.`);\n return 1;\n}\n\n/**\n * THE PROCESS ENTRY POINT — the thing whose absence made this whole bin a lie.\n *\n * Up to and including 0.4.588 this module ENDED at the closing brace above. `pnpm exec wp-upgrade-shim`\n * loaded it, defined two functions, and exited 0 having printed nothing and changed no file. Fault S\n * names this command as OPTION 1, the only option that repairs every managed surface, so the\n * guard's own \"THIS IS NOT A DEADLOCK\" promise was false: OPTION 2 repairs one of three, and OPTION 1\n * did nothing at all. Twenty-one unit tests missed it because every one of them called\n * `runUpgradeShim()` as a FUNCTION — the defect lived entirely in what the module does when SPAWNED.\n *\n * `runMain` from @webpieces/rules-config is the repo-wide wrapper and is deliberately NOT used here:\n * this bin must load with fs+path only (see the header) so it still runs on the broken tree it exists\n * to repair. `main()` is the sanctioned exit site instead, and `bin-process-entry.spec.ts` spawns\n * this file as a process so a future refactor cannot silently drop the launcher again.\n */\n// webpieces-disable no-function-outside-class -- bin entry point in this deliberately dependency-free module; see header\nexport function main(): void {\n process.exit(runUpgradeShim(process.cwd()));\n}\n\nif (require.main === module) {\n main();\n}\n"]}