@webpieces/ai-hook-rules 0.4.735 → 0.4.737
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/README.md +26 -5
- package/package.json +2 -2
- package/src/bin/codex-trust.d.ts +8 -2
- package/src/bin/codex-trust.js +10 -4
- package/src/bin/codex-trust.js.map +1 -1
- package/src/bin/hook-registration.d.ts +49 -23
- package/src/bin/hook-registration.js +80 -6
- package/src/bin/hook-registration.js.map +1 -1
- package/src/bin/neighbour-hooks.d.ts +42 -0
- package/src/bin/neighbour-hooks.js +177 -0
- package/src/bin/neighbour-hooks.js.map +1 -0
- package/src/bin/settings-shape.d.ts +37 -0
- package/src/bin/settings-shape.js +17 -0
- package/src/bin/settings-shape.js.map +1 -0
- package/src/bin/setup.d.ts +2 -1
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim-deny-reason.js +19 -1
- package/src/bin/shim-deny-reason.js.map +1 -1
- package/src/bin/upgrade-shim.js +14 -0
- package/src/bin/upgrade-shim.js.map +1 -1
- package/src/core/l0-matrix.js +3 -2
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/l0-tooling-doc.js +1 -1
- package/src/core/l0-tooling-doc.js.map +1 -1
- package/src/index.d.ts +0 -1
- package/src/index.js +1 -8
- package/src/index.js.map +1 -1
- package/src/bin/codex-guard-presence.d.ts +0 -112
- package/src/bin/codex-guard-presence.js +0 -190
- package/src/bin/codex-guard-presence.js.map +0 -1
package/src/bin/setup.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\nimport { createInterface } from 'readline';\n\nimport { RepoRootFinder, writeTemplate, writeTemplateIfMissing, CONFIG_POLICY_DOC } from '@webpieces/rules-config';\n\nimport { SHIM_MARKER, shimPath, renderShim } from './shim';\nimport {\n ClaudeSettings, HookCommand, HookEntry, HookRegistrationEntry, GUARDS_BIN, LEGACY_GUARANTEE_ROOT_MARKER,\n RULES_BIN, addHookEntry, applyManagedEnv, readSettings, writeSettings,\n HarnessRegistration, CLAUDE_REGISTRATION, CODEX_REGISTRATION,\n} from './hook-registration';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { CodexTrustProbe } from './codex-trust';\n// The config half of the installer — seeding and migrating webpieces.config.json. Split for size along\n// the seam that was already there: this module is hook WIRING, that one is CONFIG SHAPE.\nimport { seedOrSyncConfig } from './setup-config';\n\n\n// ---------------------------------------------------------------------------\n// The two independently-installable GUARD hooks. Each can land in a different settings file (see\n// InstallTarget) so a team can ship the guards while a developer keeps the code-style rules local\n// while iterating. Both are registered ABSOLUTE via $CLAUDE_PROJECT_DIR, so the MAIN tree governs every\n// tree. There used to be a third — the L-1 hook, which existed only to keep a RELATIVE registration\n// resolvable; it is retired, and purgeRetiredGuaranteeRoot() removes anything left of it.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly bin: string,\n ) {}\n\n /**\n * WHICH TOOL NAMES this hook must see, in the harness the target belongs to.\n *\n * It is a lookup on the target rather than a field on the spec because the answer is not a property\n * of the hook: the rules hook matches `Write|Edit|MultiEdit` under Claude Code and `apply_patch`\n * under Codex, and a single stored matcher is exactly how `.codex/hooks.json` came to be registered\n * against tool names Codex never emits.\n */\n matcherFor(target: InstallTarget): string {\n return target.harness.matcherFor(this.bin);\n }\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n // Project targets get the ABSOLUTE shim command, anchored on the harness's own project-root variable\n // (see HarnessRegistration.shimCommand): it resolves from ANY cwd, so a hook can never fail to\n // launch — which per the hooks reference would be exit 127, a SILENT UNGUARDED ALLOW, not a block.\n commandFor(target: InstallTarget, projectRoot: string): string {\n if (target.absolute) {\n return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;\n }\n return target.harness.shimCommand(this.bin);\n }\n}\n\n// Idempotent: re-running the installer overwrites the managed shim in place.\nfunction writeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode is only applied when creating the file; force it on overwrite too.\n fs.chmodSync(target, 0o755);\n}\n\nfunction removeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n if (fs.existsSync(target)) fs.rmSync(target);\n}\n\n// A managed .sh is shared by the project hooks — only safe to delete once no project settings file\n// references it anymore (i.e. the other hook was moved to global or uninstalled too).\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction markerReferenced(targets: InstallTarget[], marker: string): boolean {\n return targets.some((t: InstallTarget) => {\n const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(marker)));\n });\n}\n\n/**\n * Remove every trace of the RETIRED L-1 hook (`guarantee-root.sh`) — the file and any PreToolUse entry\n * still pointing at it. REMOVAL ONLY: nothing here can ever write one back.\n *\n * L-1 existed to guarantee a RELATIVE guard-hook path resolved, by refusing any `cd` that would park the\n * shell in a subdirectory. The guard hooks are ABSOLUTE now (`$CLAUDE_PROJECT_DIR/...`), so they resolve\n * from any cwd and the guarantee is structural — there is nothing left to police, and the subdirectory\n * denial that used to pay for it is gone with it.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction purgeRetiredGuaranteeRoot(targets: InstallTarget[], projectRoot: string): void {\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n if (removeHookByMarker(settings, LEGACY_GUARANTEE_ROOT_MARKER)) {\n writeSettings(target.settingsPath, settings);\n console.log(` 🗑️ removed the retired L-1 guarantee-root hook from ${target.label}`);\n }\n }\n const legacyFile = path.join(projectRoot, LEGACY_GUARANTEE_ROOT_MARKER);\n if (fs.existsSync(legacyFile)) {\n fs.rmSync(legacyFile, { force: true });\n console.log(' 🗑️ deleted the retired .claude/webpieces/guarantee-root.sh');\n }\n}\n\nexport class InstallTarget {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n /**\n * WHICH HARNESS this file arms — it decides the matcher and the shim anchor written into it.\n *\n * REQUIRED, with no default, and the absence of one is the point. A default of\n * CLAUDE_REGISTRATION would make \"omit the harness\" mean \"Claude Code\" — a widening that is an\n * ABSENCE rather than a token, so a target built for Codex without it would silently carry\n * `Write|Edit|MultiEdit` and `$CLAUDE_PROJECT_DIR`, which is EXACTLY the silently-unguarded\n * state this class exists to end, and ungreppable besides.\n */\n readonly harness: HarnessRegistration,\n ) {}\n}\n\n// The bin names come from ./hook-registration, which is also what the drift check and wp-upgrade-shim\n// compare against — one spelling of the registration, or the installer and the validator can disagree\n// about what \"installed\" means. The MATCHER is not here: it belongs to the harness, not the hook (see\n// HookSpec.matcherFor).\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', RULES_BIN);\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', GUARDS_BIN);\n\n/**\n * Every file the installer can write, keyed by the CHOICE a human makes.\n *\n * TWO TARGETS SHARE CHOICE `1`, and that is the design rather than an oversight: \"the project, committed,\n * for the team\" is ONE intention, and a repo that is worked on by both harnesses needs both files armed\n * to mean it. Splitting it into two questions would let a human answer them differently and end up with a\n * repo where Codex is silently unguarded — which is the state this whole change exists to end. Every\n * caller selects by choice id (`targets.filter(t => t.choice === answer)`), never by index, so adding a\n * harness adds a row and changes no numbering.\n *\n * `2` (personal) and `3` (global) stay Claude-only because neither has a Codex counterpart: Codex reads\n * one repo-local `hooks.json` and has no personal or home-scoped hook file.\n *\n * `homeDir` is injectable so tests can point the global target at a temp dir instead of the real\n * ~/.claude/settings.json (a unit test must never write the user's actual global settings).\n */\nexport function installTargets(projectRoot: string, homeDir: string = homedir()): InstallTarget[] {\n return [\n new InstallTarget('1', 'project (.claude/settings.json — committed, for the team)',\n path.join(projectRoot, '.claude', 'settings.json'), false, CLAUDE_REGISTRATION),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false, CLAUDE_REGISTRATION),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true, CLAUDE_REGISTRATION),\n // Given choice `1` so one answer arms the whole project — see the docblock. Its position in the\n // array carries no meaning: every caller selects by choice id.\n new InstallTarget('1', 'project, for Codex too (.codex/hooks.json — committed, for the team)',\n path.join(projectRoot, ...CODEX_REGISTRATION.settingsFiles[0].split('/')), false, CODEX_REGISTRATION),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code settings.json hook wiring.\n// ---------------------------------------------------------------------------\nexport function hasHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(bin)));\n}\n\n// Drop every PreToolUse command containing `marker` (a bin name, or a managed .sh path); returns true\n// if anything was removed. REMOVE-then-ADD is what keeps an upgrade from leaving a superseded spelling\n// (a relative command, or the retired L-1 entry) beside the current one — two spellings of one\n// registration is the compatibility shim the backwards-compat reviewer rejects.\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction removeHookByMarker(settings: ClaudeSettings, marker: string): 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) => !h.command.includes(marker));\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// Apply the chosen install for one hook: remove it from every target file, then add it back to the\n// chosen one (or nowhere, for uninstall). Writes only the files that changed.\nexport function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void {\n // SCOPED TO THE CHOSEN HARNESS, and this is what makes one call per target composable. Installing\n // the Claude hook must not strip the Codex one, so \"remove it from everywhere else\" means everywhere\n // else THIS HARNESS could live. Uninstall (chosen === null) is the one case that means every\n // harness, because \"not installed\" has to be true everywhere or the hook is still armed somewhere.\n const scope = chosen === null ? targets : targets.filter((t: InstallTarget) => t.harness === chosen.harness);\n for (const target of scope) {\n const settings = readSettings(target.settingsPath);\n const removed = removeHookByMarker(settings, hook.bin);\n const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;\n if (isChosen) {\n addHookEntry(settings, new HookRegistrationEntry(hook.matcherFor(target), hook.commandFor(target, projectRoot)));\n // The managed `env` entry goes into the SAME file the hooks go into, on every path that\n // writes hooks — interactive or `--target=`. It pins the Bash cwd to the project root, so a\n // guard's answer depends on the command rather than on where an earlier `cd` left the shell,\n // and settings `env` is inherited, so every subagent gets the identical cwd and therefore the\n // identical guard verdict. (It no longer has a RESOLUTION job — the hooks are absolute.) See\n // hook-registration.ts for the full argument; `wp-upgrade-shim` self-heals it afterwards.\n // …in the harness that HAS that surface. Codex has no settings `env`, and needs none: its\n // cwd is MEASURED not to drift, which is the whole thing this entry buys under Claude Code.\n if (target.harness.managesEnv && applyManagedEnv(settings)) {\n console.log(` ✅ env.${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE} → ${target.label} (pins the Bash cwd to the project root, for this session and every subagent)`);\n }\n writeSettings(target.settingsPath, settings);\n console.log(` ✅ ${hook.label} → ${target.label}`);\n } else if (removed) {\n writeSettings(target.settingsPath, settings);\n }\n }\n // Manage the shared checked-in shim: (re)write it whenever a project (relative) install exists,\n // otherwise clean it up once neither hook references it anymore.\n if (chosen !== null && !chosen.absolute) {\n writeShim(projectRoot);\n } else if (!markerReferenced(targets, SHIM_MARKER)) {\n removeShim(projectRoot);\n }\n // The RETIRED L-1 hook rode with the GUARDS hook, so its removal does too. Doing it here rather than\n // at a separate call site means every existing caller of applyHook — the installer's interactive and\n // --target paths, and every test — converges on the two-hook absolute form with no second step.\n if (hook.bin === GUARDS_BIN) purgeRetiredGuaranteeRoot(targets, projectRoot);\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\n}\n\n/**\n * Say whether Codex will actually RUN what we just registered — the one thing the installer cannot do\n * anything about and therefore must not leave silent.\n *\n * Codex trusts a hook entry TOFU and re-prompts whenever its bytes change, and the prompt's third option\n * is \"Continue without trusting (hooks won't run)\". So a perfectly successful install can be followed by\n * a fully unguarded session, and the only honest thing to print is what is true plus the one action a\n * HUMAN has to take. Nothing here writes `~/.codex/config.toml` — see codex-trust.ts for why forging a\n * `trusted_hash` is not on the table.\n *\n * Silent when this repo did not arm Codex, so a Claude-only install gains no noise.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction reportCodexTrust(projectRoot: string, targets: InstallTarget[], choice: string): void {\n const armed = targets.some((t: InstallTarget): boolean => t.choice === choice && t.harness === CODEX_REGISTRATION);\n if (!armed) return;\n const lines = new CodexTrustProbe().read(projectRoot).lines();\n if (lines.length === 0) return;\n console.log('');\n for (const line of lines) console.log(line);\n}\n\nfunction currentLocation(hook: HookSpec, targets: InstallTarget[]): string {\n const here = targets.filter((t: InstallTarget) => hasHook(readSettings(t.settingsPath), hook.bin));\n return here.length === 0 ? 'none' : here.map((t: InstallTarget) => t.label.split(' (')[0]).join(', ');\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve: (answer: string) => void) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer: string) => { rl.close(); resolve(answer.trim()); });\n });\n}\n\n// Map a friendly `--target` name to an InstallTarget choice id (see installTargets). Returns null\n// for an unknown name so the caller can error out. Kept separate + exported for unit testing.\nexport function resolveTargetChoice(name: string): string | null {\n switch (name) {\n case 'project': return '1';\n case 'project-personal':\n case 'projectpersonal':\n case 'local': return '2';\n case 'global': return '3';\n case 'none':\n case 'uninstall': return '4';\n default: return null;\n }\n}\n\n// Extract the value of `--target=<name>` from argv (null if the flag is absent).\nexport function parseTargetArg(args: string[]): string | null {\n const flag = args.find((a: string): boolean => a.startsWith('--target='));\n return flag ? flag.slice('--target='.length) : null;\n}\n\n/**\n * Apply ONE choice for one hook: every target that answer selects, or uninstall when it selects none.\n *\n * The loop is what lets one choice arm several harnesses (choice `1` writes both the Claude settings\n * file and the Codex hooks file — see installTargets). `applyHook` scopes its removals to the chosen\n * target's harness, which is what keeps these calls from undoing one another.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nexport function applyChoice(hook: HookSpec, choice: string, targets: InstallTarget[], projectRoot: string): void {\n const chosen = targets.filter((t: InstallTarget): boolean => t.choice === choice);\n if (chosen.length === 0) {\n applyHook(hook, null, targets, projectRoot);\n return;\n }\n for (const target of chosen) applyHook(hook, target, targets, projectRoot);\n}\n\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label}`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n // The matcher is printed PER TARGET now, because it differs per harness — Codex's file tool is\n // `apply_patch`, Claude's are Write|Edit|MultiEdit — and one matcher printed above the list would be\n // wrong for whichever harness it did not describe.\n for (const target of targets) console.log(` ${target.choice}) ${target.label} [matcher: ${hook.matcherFor(target)}]`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n applyChoice(hook, answer, targets, projectRoot);\n}\n\n/**\n * Scaffold the SERVER-SIDE PR gate: the CI workflow plus the doc explaining how to turn it on.\n *\n * This lives in the installer, not the PR flow. `wp-start-upsert-pr` used to do it — printing\n * copy-to-`.github` and branch-protection instructions on EVERY run, at an agent doing feature work\n * that could not act on them anyway (marking a check required needs a repo admin). Setup is a\n * one-time, admin-shaped act, so it belongs with the other one-time setup.\n *\n * Written UNCONDITIONALLY, unlike the old version which required a `gateSalt` to already be set: the\n * whole point of the doc is to tell you to set one, so gating it on the thing it teaches meant the\n * instructions only appeared to repos that no longer needed them.\n *\n * Both land in gitignored `.webpieces/instruct-ai/`, never `.github/` directly — writing there would\n * dirty the tree, and copying it is the human's decision. `IfMissing` for the yml so a repo that has\n * customized its workflow never gets it clobbered; the doc itself is refreshed so it cannot go stale.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction scaffoldCiGate(projectRoot: string): void {\n writeTemplateIfMissing(projectRoot, 'webpieces-pr-gate.yml');\n writeTemplate(projectRoot, 'webpieces.ci-gate-setup.md');\n console.log('');\n console.log('ℹ️ Optional: server-side PR gate (stops an UNHOOKED teammate opening a PR in the web UI).');\n console.log(' It is OFF until you set a gateSalt. Three steps, one of which needs a repo admin:');\n console.log(' .webpieces/instruct-ai/webpieces.ci-gate-setup.md');\n}\n\nexport async function main(): Promise<void> {\n const args = process.argv.slice(2);\n // Anchor the install at the repo root (git toplevel — webpieces.config.json may not exist yet on\n // a first install), never a subdir cwd, so `.webpieces`/hooks/config all land at the root.\n const projectRoot = new RepoRootFinder().resolveRepoRoot(process.cwd());\n\n seedOrSyncConfig(projectRoot);\n // Always refreshed: it explains why a retired key is rejected rather than accepted, and what to do\n // about it — which is exactly what an agent needs on the run where a migration just moved keys out\n // from under its config.\n writeTemplate(projectRoot, CONFIG_POLICY_DOC);\n\n scaffoldCiGate(projectRoot);\n\n const targets = installTargets(projectRoot);\n\n // Non-interactive: `--target=project|project-personal|global|none` installs BOTH hooks at that\n // location without prompting, so an agent or CI can run the installer unattended (e.g. after a\n // @webpieces upgrade that changed the hook entry). Omit the flag for the interactive per-hook chooser.\n const targetName = parseTargetArg(args);\n if (targetName !== null) {\n const choice = resolveTargetChoice(targetName);\n if (choice === null) {\n console.error(`❌ Unknown --target '${targetName}'. Use one of: project | project-personal | global | none`);\n process.exitCode = 1;\n return;\n }\n applyChoice(RULES_HOOK, choice, targets, projectRoot);\n applyChoice(GUARDS_HOOK, choice, targets, projectRoot);\n console.log(`\\nDone. Both hooks set to: ${targetName}.`);\n reportCodexTrust(projectRoot, targets, choice);\n return;\n }\n\n console.log('');\n console.log('Two webpieces hooks can be installed independently — choose a location for each:');\n await wireHook(RULES_HOOK, targets, projectRoot);\n await wireHook(GUARDS_HOOK, targets, projectRoot);\n // Whichever choices were made above, report Codex trust for the project choice — an interactive run\n // that armed Codex needs the same warning the --target path prints.\n reportCodexTrust(projectRoot, targets, '1');\n console.log('');\n console.log('Done. Re-run `pnpm wp-install-ai-hooks` anytime to move or uninstall a hook.');\n console.log('(Non-interactive: pnpm wp-install-ai-hooks --target=project|project-personal|global|none)');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup.ts"],"names":[],"mappings":";;;AAuJA,wCAaC;AAKD,0BAGC;AAuBD,8BAyCC;AAsCD,kDAWC;AAGD,wCAGC;AAUD,kCAOC;AAyCD,oBA4CC;;AAzYD,+CAAyB;AACzB,mDAA6B;AAC7B,2BAA6B;AAC7B,uCAA2C;AAE3C,0DAAmH;AAEnH,iCAA2D;AAC3D,2DAI6B;AAE7B,+CAAqE;AACrE,+CAAgD;AAChD,uGAAuG;AACvG,yFAAyF;AACzF,iDAAkD;AAGlD,8EAA8E;AAC9E,iGAAiG;AACjG,kGAAkG;AAClG,wGAAwG;AACxG,oGAAoG;AACpG,0FAA0F;AAC1F,8EAA8E;AAC9E,MAAM,QAAQ;IAEG;IACA;IACA;IAHb,YACa,GAAW,EACX,KAAa,EACb,GAAW;QAFX,QAAG,GAAH,GAAG,CAAQ;QACX,UAAK,GAAL,KAAK,CAAQ;QACb,QAAG,GAAH,GAAG,CAAQ;IACrB,CAAC;IAEJ;;;;;;;OAOG;IACH,UAAU,CAAC,MAAqB;QAC5B,OAAO,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/C,CAAC;IAED,6FAA6F;IAC7F,qGAAqG;IACrG,+FAA+F;IAC/F,mGAAmG;IACnG,UAAU,CAAC,MAAqB,EAAE,WAAmB;QACjD,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YAClB,OAAO,QAAQ,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9E,CAAC;QACD,OAAO,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC;CACJ;AAED,6EAA6E;AAC7E,SAAS,SAAS,CAAC,WAAmB;IAClC,MAAM,MAAM,GAAG,IAAA,eAAQ,EAAC,WAAW,CAAC,CAAC;IACrC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,IAAA,iBAAU,GAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACxD,0FAA0F;IAC1F,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,UAAU,CAAC,WAAmB;IACnC,MAAM,MAAM,GAAG,IAAA,eAAQ,EAAC,WAAW,CAAC,CAAC;IACrC,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACjD,CAAC;AAED,mGAAmG;AACnG,sFAAsF;AACtF,6LAA6L;AAC7L,SAAS,gBAAgB,CAAC,OAAwB,EAAE,MAAc;IAC9D,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAgB,EAAE,EAAE;QACrC,MAAM,OAAO,GAAG,IAAA,gCAAY,EAAC,CAAC,CAAC,YAAY,CAAC,CAAC,KAAK,EAAE,UAAU,IAAI,EAAE,CAAC;QACrE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAY,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAc,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACxG,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;;;;GAQG;AACH,6LAA6L;AAC7L,SAAS,yBAAyB,CAAC,OAAwB,EAAE,WAAmB;IAC5E,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,IAAA,gCAAY,EAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QACnD,IAAI,kBAAkB,CAAC,QAAQ,EAAE,gDAA4B,CAAC,EAAE,CAAC;YAC7D,IAAA,iCAAa,EAAC,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;YAC7C,OAAO,CAAC,GAAG,CAAC,2DAA2D,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QAC3F,CAAC;IACL,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,gDAA4B,CAAC,CAAC;IACxE,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,OAAO,CAAC,GAAG,CAAC,gEAAgE,CAAC,CAAC;IAClF,CAAC;AACL,CAAC;AAED,MAAa,aAAa;IAGT;IACA;IACA;IACA;IAUA;IAfb,yDAAyD;IACzD,YACa,MAAc,EACd,KAAa,EACb,YAAoB,EACpB,QAAiB;IAC1B;;;;;;;;OAQG;IACM,OAA4B;QAb5B,WAAM,GAAN,MAAM,CAAQ;QACd,UAAK,GAAL,KAAK,CAAQ;QACb,iBAAY,GAAZ,YAAY,CAAQ;QACpB,aAAQ,GAAR,QAAQ,CAAS;QAUjB,YAAO,GAAP,OAAO,CAAqB;IACtC,CAAC;CACP;AAlBD,sCAkBC;AAED,sGAAsG;AACtG,sGAAsG;AACtG,sGAAsG;AACtG,wBAAwB;AACX,QAAA,UAAU,GAAG,IAAI,QAAQ,CAAC,OAAO,EAAE,oCAAoC,EAAE,6BAAS,CAAC,CAAC;AACpF,QAAA,WAAW,GAAG,IAAI,QAAQ,CAAC,QAAQ,EAAE,wCAAwC,EAAE,8BAAU,CAAC,CAAC;AAExG;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,cAAc,CAAC,WAAmB,EAAE,UAAkB,IAAA,YAAO,GAAE;IAC3E,OAAO;QACH,IAAI,aAAa,CAAC,GAAG,EAAE,2DAA2D,EAC9E,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,eAAe,CAAC,EAAE,KAAK,EAAE,uCAAmB,CAAC;QACnF,IAAI,aAAa,CAAC,GAAG,EAAE,0DAA0D,EAC7E,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,qBAAqB,CAAC,EAAE,KAAK,EAAE,uCAAmB,CAAC;QACzF,IAAI,aAAa,CAAC,GAAG,EAAE,+DAA+D,EAClF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,eAAe,CAAC,EAAE,IAAI,EAAE,uCAAmB,CAAC;QAC9E,gGAAgG;QAChG,+DAA+D;QAC/D,IAAI,aAAa,CAAC,GAAG,EAAE,sEAAsE,EACzF,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,sCAAkB,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,sCAAkB,CAAC;KAC5G,CAAC;AACN,CAAC;AAED,8EAA8E;AAC9E,yCAAyC;AACzC,8EAA8E;AAC9E,SAAgB,OAAO,CAAC,QAAwB,EAAE,GAAW;IACzD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,EAAE,UAAU,IAAI,EAAE,CAAC;IACjD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAY,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAc,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACrG,CAAC;AAED,sGAAsG;AACtG,uGAAuG;AACvG,+FAA+F;AAC/F,gFAAgF;AAChF,6LAA6L;AAC7L,SAAS,kBAAkB,CAAC,QAAwB,EAAE,MAAc;IAChE,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3C,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,IAAI,GAAgB,EAAE,CAAC;IAC7B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAc,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QAClF,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,KAAK,CAAC,MAAM;YAAE,OAAO,GAAG,IAAI,CAAC;QACxD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,OAAO;QAAE,QAAQ,CAAC,KAAM,CAAC,UAAU,GAAG,IAAI,CAAC;IAC/C,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,mGAAmG;AACnG,8EAA8E;AAC9E,SAAgB,SAAS,CAAC,IAAc,EAAE,MAA4B,EAAE,OAAwB,EAAE,WAAmB;IACjH,kGAAkG;IAClG,qGAAqG;IACrG,6FAA6F;IAC7F,mGAAmG;IACnG,MAAM,KAAK,GAAG,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7G,KAAK,MAAM,MAAM,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,IAAA,gCAAY,EAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACvD,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,YAAY,KAAK,MAAM,CAAC,YAAY,CAAC;QAChF,IAAI,QAAQ,EAAE,CAAC;YACX,IAAA,gCAAY,EAAC,QAAQ,EAAE,IAAI,yCAAqB,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;YACjH,wFAAwF;YACxF,4FAA4F;YAC5F,6FAA6F;YAC7F,8FAA8F;YAC9F,6FAA6F;YAC7F,0FAA0F;YAC1F,0FAA0F;YAC1F,4FAA4F;YAC5F,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,IAAI,IAAA,mCAAe,EAAC,QAAQ,CAAC,EAAE,CAAC;gBACzD,OAAO,CAAC,GAAG,CAAC,WAAW,8BAAgB,IAAI,gCAAkB,MAAM,MAAM,CAAC,KAAK,+EAA+E,CAAC,CAAC;YACpK,CAAC;YACD,IAAA,iCAAa,EAAC,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;YAC7C,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,KAAK,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QACvD,CAAC;aAAM,IAAI,OAAO,EAAE,CAAC;YACjB,IAAA,iCAAa,EAAC,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;QACjD,CAAC;IACL,CAAC;IACD,gGAAgG;IAChG,iEAAiE;IACjE,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACtC,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3B,CAAC;SAAM,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,kBAAW,CAAC,EAAE,CAAC;QACjD,UAAU,CAAC,WAAW,CAAC,CAAC;IAC5B,CAAC;IACD,qGAAqG;IACrG,qGAAqG;IACrG,gGAAgG;IAChG,IAAI,IAAI,CAAC,GAAG,KAAK,8BAAU;QAAE,yBAAyB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAC7E,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,KAAK,8CAA8C,CAAC,CAAC;AACtG,CAAC;AAED;;;;;;;;;;;GAWG;AACH,6LAA6L;AAC7L,SAAS,gBAAgB,CAAC,WAAmB,EAAE,OAAwB,EAAE,MAAc;IACnF,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAgB,EAAW,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,OAAO,KAAK,sCAAkB,CAAC,CAAC;IACnH,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,MAAM,KAAK,GAAG,IAAI,6BAAe,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,KAAK,EAAE,CAAC;IAC9D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC/B,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,eAAe,CAAC,IAAc,EAAE,OAAwB;IAC7D,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,IAAA,gCAAY,EAAC,CAAC,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACnG,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1G,CAAC;AAED,SAAS,MAAM,CAAC,QAAgB;IAC5B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAiC,EAAE,EAAE;QACrD,MAAM,EAAE,GAAG,IAAA,0BAAe,EAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7E,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAc,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvF,CAAC,CAAC,CAAC;AACP,CAAC;AAED,kGAAkG;AAClG,8FAA8F;AAC9F,SAAgB,mBAAmB,CAAC,IAAY;IAC5C,QAAQ,IAAI,EAAE,CAAC;QACX,KAAK,SAAS,CAAC,CAAC,OAAO,GAAG,CAAC;QAC3B,KAAK,kBAAkB,CAAC;QACxB,KAAK,iBAAiB,CAAC;QACvB,KAAK,OAAO,CAAC,CAAC,OAAO,GAAG,CAAC;QACzB,KAAK,QAAQ,CAAC,CAAC,OAAO,GAAG,CAAC;QAC1B,KAAK,MAAM,CAAC;QACZ,KAAK,WAAW,CAAC,CAAC,OAAO,GAAG,CAAC;QAC7B,OAAO,CAAC,CAAC,OAAO,IAAI,CAAC;IACzB,CAAC;AACL,CAAC;AAED,iFAAiF;AACjF,SAAgB,cAAc,CAAC,IAAc;IACzC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1E,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACxD,CAAC;AAED;;;;;;GAMG;AACH,6LAA6L;AAC7L,SAAgB,WAAW,CAAC,IAAc,EAAE,MAAc,EAAE,OAAwB,EAAE,WAAmB;IACrG,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAgB,EAAW,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;IAClF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;QAC5C,OAAO;IACX,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,MAAM;QAAE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;AAC/E,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,IAAc,EAAE,OAAwB,EAAE,WAAmB;IACjF,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC7B,OAAO,CAAC,GAAG,CAAC,6BAA6B,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAC3E,+FAA+F;IAC/F,qGAAqG;IACrG,mDAAmD;IACnD,KAAK,MAAM,MAAM,IAAI,OAAO;QAAE,OAAO,CAAC,GAAG,CAAC,OAAO,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,KAAK,eAAe,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1H,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,gDAAgD,CAAC,CAAC;IAC9E,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,6LAA6L;AAC7L,SAAS,cAAc,CAAC,WAAmB;IACvC,IAAA,qCAAsB,EAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC;IAC7D,IAAA,4BAAa,EAAC,WAAW,EAAE,4BAA4B,CAAC,CAAC;IACzD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,4FAA4F,CAAC,CAAC;IAC1G,OAAO,CAAC,GAAG,CAAC,sFAAsF,CAAC,CAAC;IACpG,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC,CAAC;AAC1E,CAAC;AAEM,KAAK,UAAU,IAAI;IACtB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,iGAAiG;IACjG,2FAA2F;IAC3F,MAAM,WAAW,GAAG,IAAI,6BAAc,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAExE,IAAA,+BAAgB,EAAC,WAAW,CAAC,CAAC;IAC9B,mGAAmG;IACnG,mGAAmG;IACnG,yBAAyB;IACzB,IAAA,4BAAa,EAAC,WAAW,EAAE,gCAAiB,CAAC,CAAC;IAE9C,cAAc,CAAC,WAAW,CAAC,CAAC;IAE5B,MAAM,OAAO,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC;IAE5C,+FAA+F;IAC/F,+FAA+F;IAC/F,uGAAuG;IACvG,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;QAC/C,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YAClB,OAAO,CAAC,KAAK,CAAC,uBAAuB,UAAU,2DAA2D,CAAC,CAAC;YAC5G,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,OAAO;QACX,CAAC;QACD,WAAW,CAAC,kBAAU,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;QACtD,WAAW,CAAC,mBAAW,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;QACvD,OAAO,CAAC,GAAG,CAAC,8BAA8B,UAAU,GAAG,CAAC,CAAC;QACzD,gBAAgB,CAAC,WAAW,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAC/C,OAAO;IACX,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,kFAAkF,CAAC,CAAC;IAChG,MAAM,QAAQ,CAAC,kBAAU,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;IACjD,MAAM,QAAQ,CAAC,mBAAW,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;IAClD,oGAAoG;IACpG,oEAAoE;IACpE,gBAAgB,CAAC,WAAW,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,8EAA8E,CAAC,CAAC;IAC5F,OAAO,CAAC,GAAG,CAAC,2FAA2F,CAAC,CAAC;AAC7G,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC1B,KAAK,IAAI,EAAE,CAAC;AAChB,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\nimport { createInterface } from 'readline';\n\nimport { RepoRootFinder, writeTemplate, writeTemplateIfMissing, CONFIG_POLICY_DOC } from '@webpieces/rules-config';\n\nimport { SHIM_MARKER, shimPath, renderShim } from './shim';\nimport {\n HookRegistrationEntry, GUARDS_BIN, LEGACY_GUARANTEE_ROOT_MARKER,\n RULES_BIN, addHookEntry, applyManagedEnv, readSettings, writeSettings,\n HarnessRegistration, CLAUDE_REGISTRATION, CODEX_REGISTRATION,\n} from './hook-registration';\nimport type { ClaudeSettings, HookCommand, HookEntry } from './settings-shape';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { CodexTrustProbe } from './codex-trust';\n// The config half of the installer — seeding and migrating webpieces.config.json. Split for size along\n// the seam that was already there: this module is hook WIRING, that one is CONFIG SHAPE.\nimport { seedOrSyncConfig } from './setup-config';\n\n\n// ---------------------------------------------------------------------------\n// The two independently-installable GUARD hooks. Each can land in a different settings file (see\n// InstallTarget) so a team can ship the guards while a developer keeps the code-style rules local\n// while iterating. Both are registered ABSOLUTE via $CLAUDE_PROJECT_DIR, so the MAIN tree governs every\n// tree. There used to be a third — the L-1 hook, which existed only to keep a RELATIVE registration\n// resolvable; it is retired, and purgeRetiredGuaranteeRoot() removes anything left of it.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly bin: string,\n ) {}\n\n /**\n * WHICH TOOL NAMES this hook must see, in the harness the target belongs to.\n *\n * It is a lookup on the target rather than a field on the spec because the answer is not a property\n * of the hook: the rules hook matches `Write|Edit|MultiEdit` under Claude Code and `apply_patch`\n * under Codex, and a single stored matcher is exactly how `.codex/hooks.json` came to be registered\n * against tool names Codex never emits.\n */\n matcherFor(target: InstallTarget): string {\n return target.harness.matcherFor(this.bin);\n }\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n // Project targets get the ABSOLUTE shim command, anchored on the harness's own project-root variable\n // (see HarnessRegistration.shimCommand): it resolves from ANY cwd, so a hook can never fail to\n // launch — which per the hooks reference would be exit 127, a SILENT UNGUARDED ALLOW, not a block.\n commandFor(target: InstallTarget, projectRoot: string): string {\n if (target.absolute) {\n return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;\n }\n return target.harness.shimCommand(this.bin);\n }\n}\n\n// Idempotent: re-running the installer overwrites the managed shim in place.\nfunction writeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode is only applied when creating the file; force it on overwrite too.\n fs.chmodSync(target, 0o755);\n}\n\nfunction removeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n if (fs.existsSync(target)) fs.rmSync(target);\n}\n\n// A managed .sh is shared by the project hooks — only safe to delete once no project settings file\n// references it anymore (i.e. the other hook was moved to global or uninstalled too).\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction markerReferenced(targets: InstallTarget[], marker: string): boolean {\n return targets.some((t: InstallTarget) => {\n const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(marker)));\n });\n}\n\n/**\n * Remove every trace of the RETIRED L-1 hook (`guarantee-root.sh`) — the file and any PreToolUse entry\n * still pointing at it. REMOVAL ONLY: nothing here can ever write one back.\n *\n * L-1 existed to guarantee a RELATIVE guard-hook path resolved, by refusing any `cd` that would park the\n * shell in a subdirectory. The guard hooks are ABSOLUTE now (`$CLAUDE_PROJECT_DIR/...`), so they resolve\n * from any cwd and the guarantee is structural — there is nothing left to police, and the subdirectory\n * denial that used to pay for it is gone with it.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction purgeRetiredGuaranteeRoot(targets: InstallTarget[], projectRoot: string): void {\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n if (removeHookByMarker(settings, LEGACY_GUARANTEE_ROOT_MARKER)) {\n writeSettings(target.settingsPath, settings);\n console.log(` 🗑️ removed the retired L-1 guarantee-root hook from ${target.label}`);\n }\n }\n const legacyFile = path.join(projectRoot, LEGACY_GUARANTEE_ROOT_MARKER);\n if (fs.existsSync(legacyFile)) {\n fs.rmSync(legacyFile, { force: true });\n console.log(' 🗑️ deleted the retired .claude/webpieces/guarantee-root.sh');\n }\n}\n\nexport class InstallTarget {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n /**\n * WHICH HARNESS this file arms — it decides the matcher and the shim anchor written into it.\n *\n * REQUIRED, with no default, and the absence of one is the point. A default of\n * CLAUDE_REGISTRATION would make \"omit the harness\" mean \"Claude Code\" — a widening that is an\n * ABSENCE rather than a token, so a target built for Codex without it would silently carry\n * `Write|Edit|MultiEdit` and `$CLAUDE_PROJECT_DIR`, which is EXACTLY the silently-unguarded\n * state this class exists to end, and ungreppable besides.\n */\n readonly harness: HarnessRegistration,\n ) {}\n}\n\n// The bin names come from ./hook-registration, which is also what the drift check and wp-upgrade-shim\n// compare against — one spelling of the registration, or the installer and the validator can disagree\n// about what \"installed\" means. The MATCHER is not here: it belongs to the harness, not the hook (see\n// HookSpec.matcherFor).\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', RULES_BIN);\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', GUARDS_BIN);\n\n/**\n * Every file the installer can write, keyed by the CHOICE a human makes.\n *\n * TWO TARGETS SHARE CHOICE `1`, and that is the design rather than an oversight: \"the project, committed,\n * for the team\" is ONE intention, and a repo that is worked on by both harnesses needs both files armed\n * to mean it. Splitting it into two questions would let a human answer them differently and end up with a\n * repo where Codex is silently unguarded — which is the state this whole change exists to end. Every\n * caller selects by choice id (`targets.filter(t => t.choice === answer)`), never by index, so adding a\n * harness adds a row and changes no numbering.\n *\n * `2` (personal) and `3` (global) stay Claude-only because neither has a Codex counterpart: Codex reads\n * one repo-local `hooks.json` and has no personal or home-scoped hook file.\n *\n * `homeDir` is injectable so tests can point the global target at a temp dir instead of the real\n * ~/.claude/settings.json (a unit test must never write the user's actual global settings).\n */\nexport function installTargets(projectRoot: string, homeDir: string = homedir()): InstallTarget[] {\n return [\n new InstallTarget('1', 'project (.claude/settings.json — committed, for the team)',\n path.join(projectRoot, '.claude', 'settings.json'), false, CLAUDE_REGISTRATION),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false, CLAUDE_REGISTRATION),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true, CLAUDE_REGISTRATION),\n // Given choice `1` so one answer arms the whole project — see the docblock. Its position in the\n // array carries no meaning: every caller selects by choice id.\n new InstallTarget('1', 'project, for Codex too (.codex/hooks.json — committed, for the team)',\n path.join(projectRoot, ...CODEX_REGISTRATION.settingsFiles[0].split('/')), false, CODEX_REGISTRATION),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code settings.json hook wiring.\n// ---------------------------------------------------------------------------\nexport function hasHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(bin)));\n}\n\n// Drop every PreToolUse command containing `marker` (a bin name, or a managed .sh path); returns true\n// if anything was removed. REMOVE-then-ADD is what keeps an upgrade from leaving a superseded spelling\n// (a relative command, or the retired L-1 entry) beside the current one — two spellings of one\n// registration is the compatibility shim the backwards-compat reviewer rejects.\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction removeHookByMarker(settings: ClaudeSettings, marker: string): 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) => !h.command.includes(marker));\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// Apply the chosen install for one hook: remove it from every target file, then add it back to the\n// chosen one (or nowhere, for uninstall). Writes only the files that changed.\nexport function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void {\n // SCOPED TO THE CHOSEN HARNESS, and this is what makes one call per target composable. Installing\n // the Claude hook must not strip the Codex one, so \"remove it from everywhere else\" means everywhere\n // else THIS HARNESS could live. Uninstall (chosen === null) is the one case that means every\n // harness, because \"not installed\" has to be true everywhere or the hook is still armed somewhere.\n const scope = chosen === null ? targets : targets.filter((t: InstallTarget) => t.harness === chosen.harness);\n for (const target of scope) {\n const settings = readSettings(target.settingsPath);\n const removed = removeHookByMarker(settings, hook.bin);\n const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;\n if (isChosen) {\n addHookEntry(settings, new HookRegistrationEntry(hook.matcherFor(target), hook.commandFor(target, projectRoot)));\n // The managed `env` entry goes into the SAME file the hooks go into, on every path that\n // writes hooks — interactive or `--target=`. It pins the Bash cwd to the project root, so a\n // guard's answer depends on the command rather than on where an earlier `cd` left the shell,\n // and settings `env` is inherited, so every subagent gets the identical cwd and therefore the\n // identical guard verdict. (It no longer has a RESOLUTION job — the hooks are absolute.) See\n // hook-registration.ts for the full argument; `wp-upgrade-shim` self-heals it afterwards.\n // …in the harness that HAS that surface. Codex has no settings `env`, and needs none: its\n // cwd is MEASURED not to drift, which is the whole thing this entry buys under Claude Code.\n if (target.harness.managesEnv && applyManagedEnv(settings)) {\n console.log(` ✅ env.${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE} → ${target.label} (pins the Bash cwd to the project root, for this session and every subagent)`);\n }\n writeSettings(target.settingsPath, settings);\n console.log(` ✅ ${hook.label} → ${target.label}`);\n } else if (removed) {\n writeSettings(target.settingsPath, settings);\n }\n }\n // Manage the shared checked-in shim: (re)write it whenever a project (relative) install exists,\n // otherwise clean it up once neither hook references it anymore.\n if (chosen !== null && !chosen.absolute) {\n writeShim(projectRoot);\n } else if (!markerReferenced(targets, SHIM_MARKER)) {\n removeShim(projectRoot);\n }\n // The RETIRED L-1 hook rode with the GUARDS hook, so its removal does too. Doing it here rather than\n // at a separate call site means every existing caller of applyHook — the installer's interactive and\n // --target paths, and every test — converges on the two-hook absolute form with no second step.\n if (hook.bin === GUARDS_BIN) purgeRetiredGuaranteeRoot(targets, projectRoot);\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\n}\n\n/**\n * Say whether Codex will actually RUN what we just registered — the one thing the installer cannot do\n * anything about and therefore must not leave silent.\n *\n * Codex trusts a hook entry TOFU and re-prompts whenever its bytes change, and the prompt's third option\n * is \"Continue without trusting (hooks won't run)\". So a perfectly successful install can be followed by\n * a fully unguarded session, and the only honest thing to print is what is true plus the one action a\n * HUMAN has to take. Nothing here writes `~/.codex/config.toml` — see codex-trust.ts for why forging a\n * `trusted_hash` is not on the table.\n *\n * Silent when this repo did not arm Codex, so a Claude-only install gains no noise.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction reportCodexTrust(projectRoot: string, targets: InstallTarget[], choice: string): void {\n const armed = targets.some((t: InstallTarget): boolean => t.choice === choice && t.harness === CODEX_REGISTRATION);\n if (!armed) return;\n const lines = new CodexTrustProbe().read(projectRoot).lines();\n if (lines.length === 0) return;\n console.log('');\n for (const line of lines) console.log(line);\n}\n\nfunction currentLocation(hook: HookSpec, targets: InstallTarget[]): string {\n const here = targets.filter((t: InstallTarget) => hasHook(readSettings(t.settingsPath), hook.bin));\n return here.length === 0 ? 'none' : here.map((t: InstallTarget) => t.label.split(' (')[0]).join(', ');\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve: (answer: string) => void) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer: string) => { rl.close(); resolve(answer.trim()); });\n });\n}\n\n// Map a friendly `--target` name to an InstallTarget choice id (see installTargets). Returns null\n// for an unknown name so the caller can error out. Kept separate + exported for unit testing.\nexport function resolveTargetChoice(name: string): string | null {\n switch (name) {\n case 'project': return '1';\n case 'project-personal':\n case 'projectpersonal':\n case 'local': return '2';\n case 'global': return '3';\n case 'none':\n case 'uninstall': return '4';\n default: return null;\n }\n}\n\n// Extract the value of `--target=<name>` from argv (null if the flag is absent).\nexport function parseTargetArg(args: string[]): string | null {\n const flag = args.find((a: string): boolean => a.startsWith('--target='));\n return flag ? flag.slice('--target='.length) : null;\n}\n\n/**\n * Apply ONE choice for one hook: every target that answer selects, or uninstall when it selects none.\n *\n * The loop is what lets one choice arm several harnesses (choice `1` writes both the Claude settings\n * file and the Codex hooks file — see installTargets). `applyHook` scopes its removals to the chosen\n * target's harness, which is what keeps these calls from undoing one another.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nexport function applyChoice(hook: HookSpec, choice: string, targets: InstallTarget[], projectRoot: string): void {\n const chosen = targets.filter((t: InstallTarget): boolean => t.choice === choice);\n if (chosen.length === 0) {\n applyHook(hook, null, targets, projectRoot);\n return;\n }\n for (const target of chosen) applyHook(hook, target, targets, projectRoot);\n}\n\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label}`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n // The matcher is printed PER TARGET now, because it differs per harness — Codex's file tool is\n // `apply_patch`, Claude's are Write|Edit|MultiEdit — and one matcher printed above the list would be\n // wrong for whichever harness it did not describe.\n for (const target of targets) console.log(` ${target.choice}) ${target.label} [matcher: ${hook.matcherFor(target)}]`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n applyChoice(hook, answer, targets, projectRoot);\n}\n\n/**\n * Scaffold the SERVER-SIDE PR gate: the CI workflow plus the doc explaining how to turn it on.\n *\n * This lives in the installer, not the PR flow. `wp-start-upsert-pr` used to do it — printing\n * copy-to-`.github` and branch-protection instructions on EVERY run, at an agent doing feature work\n * that could not act on them anyway (marking a check required needs a repo admin). Setup is a\n * one-time, admin-shaped act, so it belongs with the other one-time setup.\n *\n * Written UNCONDITIONALLY, unlike the old version which required a `gateSalt` to already be set: the\n * whole point of the doc is to tell you to set one, so gating it on the thing it teaches meant the\n * instructions only appeared to repos that no longer needed them.\n *\n * Both land in gitignored `.webpieces/instruct-ai/`, never `.github/` directly — writing there would\n * dirty the tree, and copying it is the human's decision. `IfMissing` for the yml so a repo that has\n * customized its workflow never gets it clobbered; the doc itself is refreshed so it cannot go stale.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction scaffoldCiGate(projectRoot: string): void {\n writeTemplateIfMissing(projectRoot, 'webpieces-pr-gate.yml');\n writeTemplate(projectRoot, 'webpieces.ci-gate-setup.md');\n console.log('');\n console.log('ℹ️ Optional: server-side PR gate (stops an UNHOOKED teammate opening a PR in the web UI).');\n console.log(' It is OFF until you set a gateSalt. Three steps, one of which needs a repo admin:');\n console.log(' .webpieces/instruct-ai/webpieces.ci-gate-setup.md');\n}\n\nexport async function main(): Promise<void> {\n const args = process.argv.slice(2);\n // Anchor the install at the repo root (git toplevel — webpieces.config.json may not exist yet on\n // a first install), never a subdir cwd, so `.webpieces`/hooks/config all land at the root.\n const projectRoot = new RepoRootFinder().resolveRepoRoot(process.cwd());\n\n seedOrSyncConfig(projectRoot);\n // Always refreshed: it explains why a retired key is rejected rather than accepted, and what to do\n // about it — which is exactly what an agent needs on the run where a migration just moved keys out\n // from under its config.\n writeTemplate(projectRoot, CONFIG_POLICY_DOC);\n\n scaffoldCiGate(projectRoot);\n\n const targets = installTargets(projectRoot);\n\n // Non-interactive: `--target=project|project-personal|global|none` installs BOTH hooks at that\n // location without prompting, so an agent or CI can run the installer unattended (e.g. after a\n // @webpieces upgrade that changed the hook entry). Omit the flag for the interactive per-hook chooser.\n const targetName = parseTargetArg(args);\n if (targetName !== null) {\n const choice = resolveTargetChoice(targetName);\n if (choice === null) {\n console.error(`❌ Unknown --target '${targetName}'. Use one of: project | project-personal | global | none`);\n process.exitCode = 1;\n return;\n }\n applyChoice(RULES_HOOK, choice, targets, projectRoot);\n applyChoice(GUARDS_HOOK, choice, targets, projectRoot);\n console.log(`\\nDone. Both hooks set to: ${targetName}.`);\n reportCodexTrust(projectRoot, targets, choice);\n return;\n }\n\n console.log('');\n console.log('Two webpieces hooks can be installed independently — choose a location for each:');\n await wireHook(RULES_HOOK, targets, projectRoot);\n await wireHook(GUARDS_HOOK, targets, projectRoot);\n // Whichever choices were made above, report Codex trust for the project choice — an interactive run\n // that armed Codex needs the same warning the --target path prints.\n reportCodexTrust(projectRoot, targets, '1');\n console.log('');\n console.log('Done. Re-run `pnpm wp-install-ai-hooks` anytime to move or uninstall a hook.');\n console.log('(Non-interactive: pnpm wp-install-ai-hooks --target=project|project-personal|global|none)');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
|
|
@@ -6,6 +6,7 @@ const l0_fault_codes_1 = require("../core/l0-fault-codes");
|
|
|
6
6
|
const l0_allowlist_1 = require("./l0-allowlist");
|
|
7
7
|
const l0_codex_read_1 = require("./l0-codex-read");
|
|
8
8
|
const managed_env_1 = require("./managed-env");
|
|
9
|
+
const neighbour_hooks_1 = require("./neighbour-hooks");
|
|
9
10
|
const shim_1 = require("./shim");
|
|
10
11
|
/**
|
|
11
12
|
* THE FAIL-CLOSED DENY TEXT for a drifted managed hook surface (L0 fault S) — its own module because
|
|
@@ -91,11 +92,28 @@ class ShimStaleDeny {
|
|
|
91
92
|
'',
|
|
92
93
|
(0, l0_fault_codes_1.l0GuardHeader)(l0_fault_codes_1.L0_FAULT_SHIM_STALE, label),
|
|
93
94
|
...this.drifted.map((surface) => ` ${surface}`),
|
|
94
|
-
` → webpieces manages those
|
|
95
|
+
` → webpieces manages those FOUR things as ONE set, GENERATED by the INSTALLED @webpieces/ai-hook-rules${verNote}; what is on disk is reverted, hand-edited, or predating this binary. The env entry is ${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.`,
|
|
96
|
+
...this.neighbourNote(),
|
|
95
97
|
` → ${(0, l0_fault_codes_1.l0MatrixCitation)(l0_fault_codes_1.L0_FAULT_SHIM_STALE)}`,
|
|
96
98
|
'',
|
|
97
99
|
];
|
|
98
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* The fourth surface's line, spent ONLY when that surface is the one that moved.
|
|
103
|
+
*
|
|
104
|
+
* Conditional rather than always-on because of what it is: the other three are webpieces' own files
|
|
105
|
+
* and a reader meeting fault S needs no introduction to them, whereas this one is about the READER'S
|
|
106
|
+
* hooks, registered beside ours, and the way it fails is the one thing they cannot infer — a relative
|
|
107
|
+
* entry path that fails to load exits non-zero, which the hooks reference defines as NON-BLOCKING, so
|
|
108
|
+
* the guard silently stops guarding rather than blocking. Printing that when nothing relative is
|
|
109
|
+
* registered would be argument inside a message with a hard budget; printing it when the surface IS
|
|
110
|
+
* drifted is the only place the reader learns why a guard they wrote was not running.
|
|
111
|
+
*/
|
|
112
|
+
neighbourNote() {
|
|
113
|
+
if (!this.drifted.some((s) => s.endsWith(neighbour_hooks_1.NEIGHBOUR_SURFACE_SUFFIX)))
|
|
114
|
+
return [];
|
|
115
|
+
return [' → the fourth is YOUR hooks beside ours: a relative entry path fails to load off the repo root, and that exit is NON-BLOCKING, so the guard silently stops guarding. Fix Option 1 anchors each one and prints it.'];
|
|
116
|
+
}
|
|
99
117
|
/**
|
|
100
118
|
* WHERE IT WAS MEASURED, in the deny itself and not only in the logs. #574 put `root=` and
|
|
101
119
|
* `projectDir=` on every L1 invocation line (see decision-log / ClaudeEnv.projectDirForLog, whose
|
|
@@ -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":"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","sourcesContent":["import { claudeEnv } from '@webpieces/rules-config';\n\nimport { L0_FAULT_SHIM_STALE, l0GuardHeader, l0MatrixCitation } from '../core/l0-fault-codes';\nimport { INSTALL_HOOKS_CMD, RESTORE_SHIM_CMD, UPGRADE_SHIM_CMD } from './l0-allowlist';\nimport { CODEX_READ_STILL_ALLOWED } from './l0-codex-read';\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 * IT IS RENDERED IN THE HOUSE FORMAT, the same skeleton core/report.ts (formatReport) gives every L1\n * and L2 deny: a header naming what was blocked, a `[guard-name]` block listing the offenders with a\n * one-line `→ why`, what is still allowed, then numbered `Fix Option N:` lines each with its command\n * on its own line. L0 used to be the ONLY layer in webpieces that answered in one unbroken paragraph —\n * ~3,300 characters of it here — so the two commands that matter were buried in prose and there was no\n * guard name to grep for. Nothing about the DECISION changed; only the shape of the words.\n *\n * SHORTER IS NOT THE GOAL, SCANNABLE IS. The budget below still exists (a paragraph regrows when every\n * new finding argues its case here), but a section that earns a line gets a line.\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 * NEWLINES ARE SAFE HERE AND NEEDED NO NEW MECHANISM. denyJson() runs JSON.stringify, which escapes a\n * real newline to the two-character `\\n` on the wire, and Claude Code's parser turns it back. This is\n * already proven in production — every L1 deny is formatReport()'s multi-line string down this exact\n * path. The sh half of L0 (faults D/X/U/K in renderShim) cannot do this: it printf's REASON into a JSON\n * string literal, so it spells its newlines `${NL}` the same way it spells the ANSI escape `${ESC}`.\n * A real newline is neither a quote nor a backslash, so the JSON-safety assertions above are untouched.\n */\nclass ShimStaleDeny {\n private readonly installedVersion: string;\n /** The governing root, STRIPPED of `\"` and `\\` for display. '' when there is none to name. */\n private readonly safeRoot: string;\n /** CLAUDE_PROJECT_DIR as this process sees it, stripped the same way. */\n private readonly projectDir: string;\n private readonly drifted: readonly string[];\n private readonly inSubagent: boolean;\n /**\n * Whether the RAW root can carry a leading `cd <root> &&`. Tested against the raw root, never the\n * stripped one: stripping is a display-safety measure, and cd-anchoring to a path we just mangled\n * would prescribe a cd into a directory that does not exist. A root CD_PREFIX cannot express is\n * simply not offered as a `cd` (raw ok ⇒ safeRoot === root).\n */\n private readonly cdOk: boolean;\n\n constructor(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean) {\n this.installedVersion = installedVersion;\n this.safeRoot = root.replace(/[\"\\\\]/g, '');\n this.projectDir = claudeEnv.projectDirForLog().replace(/[\"\\\\]/g, '');\n this.drifted = drifted;\n this.inSubagent = inSubagent;\n this.cdOk = root !== '' && /^[A-Za-z0-9._/@~+-]+$/.test(root);\n }\n\n render(): string {\n return [\n ...this.header(),\n ...this.measured(),\n ...this.caller(),\n ...this.stillAllowed(),\n ...this.fixOptions(),\n ...this.footer(),\n ].join('\\n');\n }\n\n /**\n * `[managed-hook-surface]` and the surfaces that drifted, one per line.\n *\n * `drifted` names WHICH of the managed things moved — .claude/webpieces/ai-hook.sh, each harness's\n * .claude/settings.json hook registration, and its managed env entry (see hook-registration.ts). It\n * is REQUIRED, not optional: this used to be a shim-only message, and an optional list would let a\n * caller silently keep emitting the one-file text after the surface grew — the \"two spellings of one\n * thing\" shape the compatibility policy rejects. It was FOUR; guarantee-root.sh (L-1) is gone,\n * because the guard hooks are registered ABSOLUTE now and there is no second .sh to keep byte-locked.\n *\n * THE CAUSE IS A LIST. It used to assert flatly \"(it was reverted or hand-edited)\", which is\n * frequently FALSE — the common case is a shim whose logic simply predates this binary — and that\n * false certainty sent a real agent hunting for a tamper that never happened.\n */\n private header(): string[] {\n const verNote = this.installedVersion ? ` (installed version ${this.installedVersion})` : '';\n const n = this.drifted.length;\n const label = n === 1 ? '1 surface drifted' : `${String(n)} surfaces drifted`;\n return [\n '❌ webpieces ai-hooks blocked this call: a webpieces-managed hook surface no longer matches the installed guard binary.',\n '',\n l0GuardHeader(L0_FAULT_SHIM_STALE, label),\n ...this.drifted.map((surface: string): string => ` ${surface}`),\n ` → webpieces manages those THREE things as ONE set, GENERATED by the INSTALLED @webpieces/ai-hook-rules${verNote}; what is on disk is reverted, hand-edited, or predating this binary. The env entry is ${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.`,\n ` → ${l0MatrixCitation(L0_FAULT_SHIM_STALE)}`,\n '',\n ];\n }\n\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 * 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 */\n private measured(): string[] {\n if (this.safeRoot === '') return [];\n return [\n 'Where this was measured:',\n ` root=${this.safeRoot} - the tree whose shim was compared, and the one to repair`,\n ` projectDir=${this.projectDir} - CLAUDE_PROJECT_DIR as this process sees it; <unset> = absent, not set-but-empty`,\n ` → ${this.verdict()}`,\n '',\n ];\n }\n\n private verdict(): string {\n return this.callerIsInTheTree()\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 }\n\n /** True when the tree needing repair is the caller's own tree — the input the caller branch gates on. */\n private callerIsInTheTree(): boolean {\n return this.safeRoot === this.projectDir;\n }\n\n /**\n * THE CALLER-GATED BRANCH. It changes the WORDS ONLY — never the block/allow decision, never which\n * command is printed, and never which tree anything acts on (that is decided from the path, which is\n * why the deleted AgentIdentity class is NOT coming back for anything but message shape; agent\n * identity was measured untrustworthy as a location signal when a worktree agent resumed on the\n * primary clone after its tree was reaped).\n *\n * Two inputs, both already on hand:\n * 1. `inSubagent` — from the payload's `agent_id`, which Claude Code populates ONLY off the main\n * loop (main falls back to the session id, so the field is absent there). `agent_type` is NOT\n * usable: it is always populated and discriminates nothing. REQUIRED for the same reason\n * `drifted` is — an optional flag would let a caller keep emitting the main-loop text.\n * 2. root= vs projectDir= — different means the caller is not standing in the tree to repair.\n *\n * A main agent, and a subagent whose cwd IS the tree, get the cure and nothing else: they can run it,\n * see the result and commit it. A WORKTREE-ISOLATED subagent gets one extra step, and only because it\n * is true — MEASURED 2026-08-11: such an agent CAN run `cd <main> && pnpm exec wp-upgrade-shim` and it\n * works (the harness refuses cross-tree GIT operations, not this), but it can neither verify nor\n * commit the result, because `git -C <main>` is refused. So the escalation is of the COMMIT, not of\n * the repair, and the text must never tell it a local cure cannot work — the older wording asserted\n * exactly that and was false in the window where it fired (measured 2026-08-10: a worktree subagent\n * cured in place and the block lifted, with the deny's own root= naming that worktree).\n *\n * WHAT IS DELIBERATELY GONE: the \"ask the coordinator to run pnpm install so both trees are on the\n * same @webpieces version\" clause. Both hooks are registered ABSOLUTE, so every tree is already\n * judged by MAIN's shim and MAIN's binary — there is no version alignment left to ask for, and\n * asking sends an agent after a non-problem.\n */\n private caller(): string[] {\n // No governing root to name means no `Where this was measured` section either, so there is no\n // root= for this text to point at and nothing to escalate ABOUT. Silence beats a dangling field.\n if (this.safeRoot === '' || !this.inSubagent || this.callerIsInTheTree()) return [];\n return [\n 'You are a SUBAGENT and root= is not the tree you are standing in, so this takes TWO steps:',\n ' 1. Run Fix Option 1 below exactly as printed. It is already anchored to the tree that must change, and a worktree-isolated subagent CAN run it against another tree - that was measured and it works, so never conclude a local cure cannot work.',\n ` 2. Then ESCALATE THE COMMIT, which is the part you cannot do: git -C ${this.safeRoot} is refused here, so you can neither verify nor commit what the cure regenerated. Tell the coordinator to run git status in ${this.safeRoot} and commit the regenerated shim.`,\n '',\n ];\n }\n\n private stillAllowed(): string[] {\n return [\n 'Still allowed while this block is up:',\n ` - any Read, and ${CODEX_READ_STILL_ALLOWED}`,\n ' - any Write/Edit whose target is webpieces.config.json',\n ' - every command on the L0 allowlist, including both Fix Options below',\n ' THIS IS NOT A DEADLOCK: both options are explicitly ALLOWED through, so run one YOURSELF now - do not hand it back to the human. Every OTHER tool call is blocked until every managed surface matches again.',\n '',\n ];\n }\n\n /**\n * The two cures, house-numbered. ORDER IS LOAD-BEARING: wp-upgrade-shim LEADS because it is the only\n * cure that repairs every managed surface, it touches no config and imports only fs/path, so it\n * runs on a tree too broken to load the rule engine. The `cp` stays last as the pre-0.4.408 fallback.\n *\n * Both are anchored with a leading `cd <root> &&` when the root allows it — CD_PREFIX_*_ANCHORED\n * tolerates exactly that one prefix (locked by unit test), and it is what keeps the cure curable when\n * the AI's cwd is a DIFFERENT tree than the one being judged. OPTION 2 is a relative-path `cp`, so it\n * is even MORE cwd-sensitive than OPTION 1 — it is anchored too. Never a SECOND `cd … &&`: the\n * allowlist matches the whole command and three segments are denied.\n */\n private fixOptions(): string[] {\n const anchor = (cmd: string): string => (this.cdOk ? `cd ${this.safeRoot} && ${cmd}` : cmd);\n return [\n ' Fix Option 1: (preferred) the only cure that repairs every managed surface, and it runs on a broken tree',\n ` run EXACTLY: '${anchor(UPGRADE_SHIM_CMD)}'`,\n ` Fix Option 2: PARTIAL - repairs ${SHIM_MARKER} only. Pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where Fix Option 1 does not exist yet; then upgrade and run Fix Option 1.`,\n ` run EXACTLY: '${anchor(RESTORE_SHIM_CMD)}'`,\n ` NOT an option: 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.`,\n '',\n ];\n }\n\n private footer(): string[] {\n return [\n NO_CHAINING_RULE,\n `If you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json rather than reverting ${SHIM_MARKER}.`,\n ];\n }\n}\n\n// The ONE entry point its two call sites (hook-core's fault-S deny, and the L0 fault table in\n// l0-matrix) import. The rendering lives on ShimStaleDeny above, per CLAUDE.md; this is the seam.\n// webpieces-disable no-function-outside-class -- one-line constructor+render seam for ShimStaleDeny, in the dependency-free shim module (it must stay callable from a tree too broken to build a DI container).\nexport function shimStaleDenyReason(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean): string {\n return new ShimStaleDeny(installedVersion, root, drifted, inSubagent).render();\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 { L0_FAULT_SHIM_STALE, l0GuardHeader, l0MatrixCitation } from '../core/l0-fault-codes';\nimport { INSTALL_HOOKS_CMD, RESTORE_SHIM_CMD, UPGRADE_SHIM_CMD } from './l0-allowlist';\nimport { CODEX_READ_STILL_ALLOWED } from './l0-codex-read';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { NEIGHBOUR_SURFACE_SUFFIX } from './neighbour-hooks';\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 * IT IS RENDERED IN THE HOUSE FORMAT, the same skeleton core/report.ts (formatReport) gives every L1\n * and L2 deny: a header naming what was blocked, a `[guard-name]` block listing the offenders with a\n * one-line `→ why`, what is still allowed, then numbered `Fix Option N:` lines each with its command\n * on its own line. L0 used to be the ONLY layer in webpieces that answered in one unbroken paragraph —\n * ~3,300 characters of it here — so the two commands that matter were buried in prose and there was no\n * guard name to grep for. Nothing about the DECISION changed; only the shape of the words.\n *\n * SHORTER IS NOT THE GOAL, SCANNABLE IS. The budget below still exists (a paragraph regrows when every\n * new finding argues its case here), but a section that earns a line gets a line.\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 * NEWLINES ARE SAFE HERE AND NEEDED NO NEW MECHANISM. denyJson() runs JSON.stringify, which escapes a\n * real newline to the two-character `\\n` on the wire, and Claude Code's parser turns it back. This is\n * already proven in production — every L1 deny is formatReport()'s multi-line string down this exact\n * path. The sh half of L0 (faults D/X/U/K in renderShim) cannot do this: it printf's REASON into a JSON\n * string literal, so it spells its newlines `${NL}` the same way it spells the ANSI escape `${ESC}`.\n * A real newline is neither a quote nor a backslash, so the JSON-safety assertions above are untouched.\n */\nclass ShimStaleDeny {\n private readonly installedVersion: string;\n /** The governing root, STRIPPED of `\"` and `\\` for display. '' when there is none to name. */\n private readonly safeRoot: string;\n /** CLAUDE_PROJECT_DIR as this process sees it, stripped the same way. */\n private readonly projectDir: string;\n private readonly drifted: readonly string[];\n private readonly inSubagent: boolean;\n /**\n * Whether the RAW root can carry a leading `cd <root> &&`. Tested against the raw root, never the\n * stripped one: stripping is a display-safety measure, and cd-anchoring to a path we just mangled\n * would prescribe a cd into a directory that does not exist. A root CD_PREFIX cannot express is\n * simply not offered as a `cd` (raw ok ⇒ safeRoot === root).\n */\n private readonly cdOk: boolean;\n\n constructor(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean) {\n this.installedVersion = installedVersion;\n this.safeRoot = root.replace(/[\"\\\\]/g, '');\n this.projectDir = claudeEnv.projectDirForLog().replace(/[\"\\\\]/g, '');\n this.drifted = drifted;\n this.inSubagent = inSubagent;\n this.cdOk = root !== '' && /^[A-Za-z0-9._/@~+-]+$/.test(root);\n }\n\n render(): string {\n return [\n ...this.header(),\n ...this.measured(),\n ...this.caller(),\n ...this.stillAllowed(),\n ...this.fixOptions(),\n ...this.footer(),\n ].join('\\n');\n }\n\n /**\n * `[managed-hook-surface]` and the surfaces that drifted, one per line.\n *\n * `drifted` names WHICH of the managed things moved — .claude/webpieces/ai-hook.sh, each harness's\n * .claude/settings.json hook registration, and its managed env entry (see hook-registration.ts). It\n * is REQUIRED, not optional: this used to be a shim-only message, and an optional list would let a\n * caller silently keep emitting the one-file text after the surface grew — the \"two spellings of one\n * thing\" shape the compatibility policy rejects. It was FOUR; guarantee-root.sh (L-1) is gone,\n * because the guard hooks are registered ABSOLUTE now and there is no second .sh to keep byte-locked.\n *\n * THE CAUSE IS A LIST. It used to assert flatly \"(it was reverted or hand-edited)\", which is\n * frequently FALSE — the common case is a shim whose logic simply predates this binary — and that\n * false certainty sent a real agent hunting for a tamper that never happened.\n */\n private header(): string[] {\n const verNote = this.installedVersion ? ` (installed version ${this.installedVersion})` : '';\n const n = this.drifted.length;\n const label = n === 1 ? '1 surface drifted' : `${String(n)} surfaces drifted`;\n return [\n '❌ webpieces ai-hooks blocked this call: a webpieces-managed hook surface no longer matches the installed guard binary.',\n '',\n l0GuardHeader(L0_FAULT_SHIM_STALE, label),\n ...this.drifted.map((surface: string): string => ` ${surface}`),\n ` → webpieces manages those FOUR things as ONE set, GENERATED by the INSTALLED @webpieces/ai-hook-rules${verNote}; what is on disk is reverted, hand-edited, or predating this binary. The env entry is ${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.`,\n ...this.neighbourNote(),\n ` → ${l0MatrixCitation(L0_FAULT_SHIM_STALE)}`,\n '',\n ];\n }\n\n /**\n * The fourth surface's line, spent ONLY when that surface is the one that moved.\n *\n * Conditional rather than always-on because of what it is: the other three are webpieces' own files\n * and a reader meeting fault S needs no introduction to them, whereas this one is about the READER'S\n * hooks, registered beside ours, and the way it fails is the one thing they cannot infer — a relative\n * entry path that fails to load exits non-zero, which the hooks reference defines as NON-BLOCKING, so\n * the guard silently stops guarding rather than blocking. Printing that when nothing relative is\n * registered would be argument inside a message with a hard budget; printing it when the surface IS\n * drifted is the only place the reader learns why a guard they wrote was not running.\n */\n private neighbourNote(): string[] {\n if (!this.drifted.some((s: string): boolean => s.endsWith(NEIGHBOUR_SURFACE_SUFFIX))) return [];\n return [' → the fourth is YOUR hooks beside ours: a relative entry path fails to load off the repo root, and that exit is NON-BLOCKING, so the guard silently stops guarding. Fix Option 1 anchors each one and prints it.'];\n }\n\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 * 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 */\n private measured(): string[] {\n if (this.safeRoot === '') return [];\n return [\n 'Where this was measured:',\n ` root=${this.safeRoot} - the tree whose shim was compared, and the one to repair`,\n ` projectDir=${this.projectDir} - CLAUDE_PROJECT_DIR as this process sees it; <unset> = absent, not set-but-empty`,\n ` → ${this.verdict()}`,\n '',\n ];\n }\n\n private verdict(): string {\n return this.callerIsInTheTree()\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 }\n\n /** True when the tree needing repair is the caller's own tree — the input the caller branch gates on. */\n private callerIsInTheTree(): boolean {\n return this.safeRoot === this.projectDir;\n }\n\n /**\n * THE CALLER-GATED BRANCH. It changes the WORDS ONLY — never the block/allow decision, never which\n * command is printed, and never which tree anything acts on (that is decided from the path, which is\n * why the deleted AgentIdentity class is NOT coming back for anything but message shape; agent\n * identity was measured untrustworthy as a location signal when a worktree agent resumed on the\n * primary clone after its tree was reaped).\n *\n * Two inputs, both already on hand:\n * 1. `inSubagent` — from the payload's `agent_id`, which Claude Code populates ONLY off the main\n * loop (main falls back to the session id, so the field is absent there). `agent_type` is NOT\n * usable: it is always populated and discriminates nothing. REQUIRED for the same reason\n * `drifted` is — an optional flag would let a caller keep emitting the main-loop text.\n * 2. root= vs projectDir= — different means the caller is not standing in the tree to repair.\n *\n * A main agent, and a subagent whose cwd IS the tree, get the cure and nothing else: they can run it,\n * see the result and commit it. A WORKTREE-ISOLATED subagent gets one extra step, and only because it\n * is true — MEASURED 2026-08-11: such an agent CAN run `cd <main> && pnpm exec wp-upgrade-shim` and it\n * works (the harness refuses cross-tree GIT operations, not this), but it can neither verify nor\n * commit the result, because `git -C <main>` is refused. So the escalation is of the COMMIT, not of\n * the repair, and the text must never tell it a local cure cannot work — the older wording asserted\n * exactly that and was false in the window where it fired (measured 2026-08-10: a worktree subagent\n * cured in place and the block lifted, with the deny's own root= naming that worktree).\n *\n * WHAT IS DELIBERATELY GONE: the \"ask the coordinator to run pnpm install so both trees are on the\n * same @webpieces version\" clause. Both hooks are registered ABSOLUTE, so every tree is already\n * judged by MAIN's shim and MAIN's binary — there is no version alignment left to ask for, and\n * asking sends an agent after a non-problem.\n */\n private caller(): string[] {\n // No governing root to name means no `Where this was measured` section either, so there is no\n // root= for this text to point at and nothing to escalate ABOUT. Silence beats a dangling field.\n if (this.safeRoot === '' || !this.inSubagent || this.callerIsInTheTree()) return [];\n return [\n 'You are a SUBAGENT and root= is not the tree you are standing in, so this takes TWO steps:',\n ' 1. Run Fix Option 1 below exactly as printed. It is already anchored to the tree that must change, and a worktree-isolated subagent CAN run it against another tree - that was measured and it works, so never conclude a local cure cannot work.',\n ` 2. Then ESCALATE THE COMMIT, which is the part you cannot do: git -C ${this.safeRoot} is refused here, so you can neither verify nor commit what the cure regenerated. Tell the coordinator to run git status in ${this.safeRoot} and commit the regenerated shim.`,\n '',\n ];\n }\n\n private stillAllowed(): string[] {\n return [\n 'Still allowed while this block is up:',\n ` - any Read, and ${CODEX_READ_STILL_ALLOWED}`,\n ' - any Write/Edit whose target is webpieces.config.json',\n ' - every command on the L0 allowlist, including both Fix Options below',\n ' THIS IS NOT A DEADLOCK: both options are explicitly ALLOWED through, so run one YOURSELF now - do not hand it back to the human. Every OTHER tool call is blocked until every managed surface matches again.',\n '',\n ];\n }\n\n /**\n * The two cures, house-numbered. ORDER IS LOAD-BEARING: wp-upgrade-shim LEADS because it is the only\n * cure that repairs every managed surface, it touches no config and imports only fs/path, so it\n * runs on a tree too broken to load the rule engine. The `cp` stays last as the pre-0.4.408 fallback.\n *\n * Both are anchored with a leading `cd <root> &&` when the root allows it — CD_PREFIX_*_ANCHORED\n * tolerates exactly that one prefix (locked by unit test), and it is what keeps the cure curable when\n * the AI's cwd is a DIFFERENT tree than the one being judged. OPTION 2 is a relative-path `cp`, so it\n * is even MORE cwd-sensitive than OPTION 1 — it is anchored too. Never a SECOND `cd … &&`: the\n * allowlist matches the whole command and three segments are denied.\n */\n private fixOptions(): string[] {\n const anchor = (cmd: string): string => (this.cdOk ? `cd ${this.safeRoot} && ${cmd}` : cmd);\n return [\n ' Fix Option 1: (preferred) the only cure that repairs every managed surface, and it runs on a broken tree',\n ` run EXACTLY: '${anchor(UPGRADE_SHIM_CMD)}'`,\n ` Fix Option 2: PARTIAL - repairs ${SHIM_MARKER} only. Pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where Fix Option 1 does not exist yet; then upgrade and run Fix Option 1.`,\n ` run EXACTLY: '${anchor(RESTORE_SHIM_CMD)}'`,\n ` NOT an option: 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.`,\n '',\n ];\n }\n\n private footer(): string[] {\n return [\n NO_CHAINING_RULE,\n `If you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json rather than reverting ${SHIM_MARKER}.`,\n ];\n }\n}\n\n// The ONE entry point its two call sites (hook-core's fault-S deny, and the L0 fault table in\n// l0-matrix) import. The rendering lives on ShimStaleDeny above, per CLAUDE.md; this is the seam.\n// webpieces-disable no-function-outside-class -- one-line constructor+render seam for ShimStaleDeny, in the dependency-free shim module (it must stay callable from a tree too broken to build a DI container).\nexport function shimStaleDenyReason(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean): string {\n return new ShimStaleDeny(installedVersion, root, drifted, inSubagent).render();\n}\n"]}
|
package/src/bin/upgrade-shim.js
CHANGED
|
@@ -24,6 +24,14 @@ const to_error_1 = require("../core/to-error");
|
|
|
24
24
|
// 3. the .codex/hooks.json registration — the SAME two hooks under Codex's own matchers and its own
|
|
25
25
|
// $PWD anchor. Repaired only where it already EXISTS: arming a harness is the installer's decision,
|
|
26
26
|
// this is the cure for one that has drifted
|
|
27
|
+
// 3b. the NEIGHBOUR hook commands in both of those files — the entries a CONSUMER repo registers
|
|
28
|
+
// beside webpieces' own. A repo-RELATIVE entry path (`node ".claude/hooks/guard-deploy.mjs"`)
|
|
29
|
+
// resolves against the hook process's cwd, so it dies the moment that cwd is not the repo root,
|
|
30
|
+
// and per the hooks reference that non-zero exit is a NON-BLOCKING error — a SILENT UNGUARDED
|
|
31
|
+
// ALLOW. Measured in a consumer repo (issue #852) silently disarming three security guards while
|
|
32
|
+
// printing nothing but a `node:internal/modules/cjs/loader` fragment with no rule name. They are
|
|
33
|
+
// anchored to the harness's own prefix, the same reversal webpieces made for its own hook. See
|
|
34
|
+
// neighbour-hooks.ts
|
|
27
35
|
// 4. the .claude/settings.json `env` entry CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR=1, which pins the
|
|
28
36
|
// Bash cwd to the project root — and, because settings `env` is inherited, pins it identically for
|
|
29
37
|
// every subagent, so a verdict never depends on where an earlier `cd` left the shell. Claude-only:
|
|
@@ -142,6 +150,12 @@ function reportRepairs(shimFile, removedLegacy, repairs) {
|
|
|
142
150
|
console.log(' (both guard hooks ABSOLUTE via $CLAUDE_PROJECT_DIR, so the MAIN tree governs every tree;');
|
|
143
151
|
console.log(' any retired guarantee-root.sh entry was removed).');
|
|
144
152
|
}
|
|
153
|
+
for (const command of repair.anchoredNeighbours) {
|
|
154
|
+
console.log(`✅ @webpieces: anchored a RELATIVE hook command in ${repair.settingsPath} so it resolves from any cwd`);
|
|
155
|
+
console.log(` now: ${command}`);
|
|
156
|
+
console.log(' (a relative entry path resolves against the hook process cwd; off the repo root it fails to load, and');
|
|
157
|
+
console.log(' per the hooks reference that non-zero exit is NON-BLOCKING — the guard silently stops guarding.)');
|
|
158
|
+
}
|
|
145
159
|
if (repair.env) {
|
|
146
160
|
console.log(`✅ @webpieces: set env.${managed_env_1.BASH_CWD_ENV_KEY}=${managed_env_1.BASH_CWD_ENV_VALUE} in ${repair.settingsPath}`);
|
|
147
161
|
console.log(' (pins the Bash cwd to the project root, so the guard hooks resolve identically for every subagent — for');
|
|
@@ -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// 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"]}
|
|
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// 3b. the NEIGHBOUR hook commands in both of those files — the entries a CONSUMER repo registers\n// beside webpieces' own. A repo-RELATIVE entry path (`node \".claude/hooks/guard-deploy.mjs\"`)\n// resolves against the hook process's cwd, so it dies the moment that cwd is not the repo root,\n// and per the hooks reference that non-zero exit is a NON-BLOCKING error — a SILENT UNGUARDED\n// ALLOW. Measured in a consumer repo (issue #852) silently disarming three security guards while\n// printing nothing but a `node:internal/modules/cjs/loader` fragment with no rule name. They are\n// anchored to the harness's own prefix, the same reversal webpieces made for its own hook. See\n// neighbour-hooks.ts\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 for (const command of repair.anchoredNeighbours) {\n console.log(`✅ @webpieces: anchored a RELATIVE hook command in ${repair.settingsPath} so it resolves from any cwd`);\n console.log(` now: ${command}`);\n console.log(' (a relative entry path resolves against the hook process cwd; off the repo root it fails to load, and');\n console.log(' per the hooks reference that non-zero exit is NON-BLOCKING — the guard silently stops guarding.)');\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"]}
|
package/src/core/l0-matrix.js
CHANGED
|
@@ -226,7 +226,8 @@ exports.L0_FAULTS = [
|
|
|
226
226
|
// the rule engine. The INSTALLER is deliberately NOT a cure here: it also migrates the config
|
|
227
227
|
// and prompts for a target twice, which hangs a non-interactive agent.
|
|
228
228
|
bashCure(shim_1.UPGRADE_SHIM_CMD, true, 'this fault fires at all — it is the only cure that repairs EVERY managed surface '
|
|
229
|
-
+ '(ai-hook.sh, each harness hook registration,
|
|
229
|
+
+ '(ai-hook.sh, each harness hook registration, the anchoring of the neighbour hook '
|
|
230
|
+
+ 'commands registered beside ours, and the Claude settings env entry), and it also '
|
|
230
231
|
+ 'deletes the retired guarantee-root.sh and any entry still naming it, and it '
|
|
231
232
|
+ 'touches no config; needs installed @webpieces/ai-hook-rules 0.4.408 or newer'),
|
|
232
233
|
// 2026-07-21: the version gap below caused a real "command not found" deadlock.
|
|
@@ -239,7 +240,7 @@ exports.L0_FAULTS = [
|
|
|
239
240
|
// is exactly the drift this generated-doc arrangement exists to make impossible.
|
|
240
241
|
(0, shim_deny_reason_1.shimStaleDenyReason)('', '', [
|
|
241
242
|
shim_1.SHIM_MARKER,
|
|
242
|
-
...hook_registration_1.HARNESS_REGISTRATIONS.
|
|
243
|
+
...hook_registration_1.HARNESS_REGISTRATIONS.flatMap((h) => [h.registrationSurface, h.neighbourSurface]),
|
|
243
244
|
hook_registration_1.ENV_SURFACE,
|
|
244
245
|
], false)),
|
|
245
246
|
new L0Fault(l0_fault_codes_1.L0_FAULT_CONFIG_MISSING, `${rules_config_1.CONFIG_FILENAME} missing`, 'the guard bin', 'JS', [
|