@webpieces/ai-hook-rules 0.4.584 → 0.4.588
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 +27 -8
- package/package.json +2 -2
- package/src/adapters/hook-core.js +33 -15
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/guarantee-root.d.ts +95 -0
- package/src/bin/guarantee-root.js +284 -0
- package/src/bin/guarantee-root.js.map +1 -0
- package/src/bin/hook-registration.d.ts +134 -0
- package/src/bin/hook-registration.js +320 -0
- package/src/bin/hook-registration.js.map +1 -0
- package/src/bin/l0-allowlist.js +1 -1
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/setup.d.ts +1 -15
- package/src/bin/setup.js +84 -77
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim-audit-log.js +21 -4
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim.d.ts +1 -1
- package/src/bin/shim.js +85 -13
- package/src/bin/shim.js.map +1 -1
- package/src/bin/upgrade-shim.js +48 -14
- package/src/bin/upgrade-shim.js.map +1 -1
- package/src/core/decision-log.d.ts +6 -2
- package/src/core/decision-log.js +12 -7
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-matrix.js +18 -11
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/log-stream.d.ts +72 -0
- package/src/core/log-stream.js +104 -0
- package/src/core/log-stream.js.map +1 -0
- package/src/core/main-sync-log.d.ts +2 -1
- package/src/core/main-sync-log.js +9 -5
- package/src/core/main-sync-log.js.map +1 -1
- package/src/core/main-sync-refresh.d.ts +2 -1
- package/src/core/main-sync-refresh.js +2 -1
- package/src/core/main-sync-refresh.js.map +1 -1
- package/src/core/rejection-log.js +3 -2
- package/src/core/rejection-log.js.map +1 -1
- package/templates/ai-hook.sh +51 -10
- package/templates/guarantee-root.sh +110 -0
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 { allRuleNames, seedEntryForRule, sectionForRule, isHookGuard, DEFAULT_MATCH_RULES, RETIRED_CONFIG_KEYS, RETIRED_SCOPE_RULE, RepoRootFinder, writeTemplate, writeTemplateIfMissing } from '@webpieces/rules-config';\n\nimport { toError } from '../core/to-error';\nimport { SHIM_MARKER, shimPath, renderShim } from './shim';\n\n// Re-exported for back-compat (setup.spec.ts + external callers). The shim body + path now live in\n// ./shim (shared with the runtime self-heal in hook-core). See shim.ts for the single source of truth.\nexport { renderShim };\n\nconst CONFIG_FILENAME = 'webpieces.config.json';\nconst DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';\nconst DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';\nconst DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';\n\n// ---------------------------------------------------------------------------\n// The two independently-installable hooks. Each can land in a different settings\n// file (see InstallTarget) so a team can ship the guards while a developer keeps\n// the code-style rules local while iterating.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly matcher: string,\n readonly bin: string,\n ) {}\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n // Project (relative) targets point at the checked-in shim via $CLAUDE_PROJECT_DIR (the project\n // root Claude Code exports to hooks). Using $CLAUDE_PROJECT_DIR — NOT a bare `./…` — means the\n // hook resolves from ANY cwd (a monorepo subdir, or a nested clone under repositories/) instead\n // of `command not found` (exit 127) silently skipping the guard. It stays portable (no hardcoded\n // absolute path), and the shim still degrades gracefully when node_modules is absent. See\n // writeShim(); the git-repo-boundary decision (foreign clone → allow) then happens in the binary.\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 shimCommand(this.bin);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Single checked-in shim (.claude/webpieces/ai-hook.sh). Both project hooks point at it, passing\n// their bin name as the first arg. settings.json points here (not at the bare bin) so a missing bin\n// (fresh clone, package removed) yields a friendly \"run pnpm install\" line instead of the raw\n// `sh: No such file or directory` on every Write/Edit/Bash tool call. The bin name rides along in\n// the command string, so `command.includes(bin)` still detects/uninstalls each hook (hasHook /\n// removeHook). `.claude` is committed, so the shim survives even when node_modules does not.\n// The shim body + path live in ./shim (shared with the runtime self-heal in hook-core); only the\n// settings.json command string is built here.\n// ---------------------------------------------------------------------------\nfunction shimCommand(bin: string): string {\n // Invoke via `sh <file>` rather than executing the shim directly: `sh` reads a 0644 file fine, so\n // a missing executable bit on the checked-in shim (fresh clone, a filesystem that drops the bit,\n // git core.fileMode quirks) can NEVER break the hook with a raw `Permission denied` on every tool\n // call. $CLAUDE_PROJECT_DIR (exported to hooks by Claude Code) = the project root, so the shim\n // resolves from any cwd. Quoted to survive spaces in the path.\n return `sh \"$CLAUDE_PROJECT_DIR/${SHIM_MARKER}\" ${bin}`;\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// The shim is shared by both hooks — only safe to delete once no project settings file references\n// it anymore (i.e. the other hook was moved to global or uninstalled too).\nfunction shimReferenced(targets: InstallTarget[]): 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(SHIM_MARKER)));\n });\n}\n\nexport class InstallTarget {\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n ) {}\n}\n\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', 'Write|Edit|MultiEdit', 'wp-ai-rules-hook');\n// Guards match Bash (git/PR guards), Write|Edit|MultiEdit (file-scoped guards like\n// feature-branch-guard), AND Read — Read carries no guard, but the guards hook owns the\n// per-invocation audit log (guard-invocations.log), so matching Read lets it record every file the\n// AI opens (log-and-allow fast path in hook-core.ts; a Read is never blocked). This is what lets a\n// human later see whether the AI read a project's design.json before editing it.\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', 'Write|Edit|MultiEdit|Bash|Read', 'wp-ai-guards-hook');\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).\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),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// webpieces.config.json seeding + migration to the rules / hookGuards / commands layout.\n// ---------------------------------------------------------------------------\n// webpieces-disable no-any-unknown -- webpieces.config.json / settings.json are opaque consumer JSON\ntype Json = Record<string, unknown>;\ntype RuleEntry = Json;\ntype Section = Record<string, RuleEntry>;\n\ninterface ConfigFile {\n extends?: string;\n rules: Section;\n hookGuards: Section;\n commands: Json;\n excludePaths: string[];\n 'match-rules': Json[];\n rulesDir: string[];\n}\n\ninterface MigrateResult {\n config: ConfigFile;\n changes: string[];\n}\n\n// webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design\nfunction seedRule(ruleName: string): RuleEntry {\n // Both escape hatches are seeded (and REQUIRED) so every rule block shows them: 0 = active,\n // null = no branch scoping. A human/AI edits these to time-box or branch-scope a rule off.\n //\n // The ENTIRE entry comes from rules-config's seedEntryForRule() — the same module that owns the\n // schema the loader validates against, so the installer can never emit an entry the loader\n // rejects. It supplies: the recommended mode (the SAME recommendation the validator prints in its\n // copy-paste snippet, so seed and advice cannot disagree), both hatches, and a default for every\n // other schema-REQUIRED field. Seeding used to be a flat 'OFF' plus the two hatches, which was\n // wrong twice over: adopters got nothing enforced, AND the entry was missing required fields\n // (e.g. branch-creation-guard.autoReapMergedBranches), so the config failed to load on first run.\n return seedEntryForRule(ruleName);\n}\n\n// The guard-hint command strings live under `guardHints`. The flat `upsertPr`/`mergeComplete` keys this\n// used to seed are RETIRED and now fail validation — seeding them meant every freshly installed repo was\n// born on a shape the validator rejects.\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n guardHints: { prCreationOrPush: DEFAULT_UPSERT_PR, mergeInProgress: DEFAULT_MERGE_COMPLETE },\n };\n}\n\n// Required excludePaths block: ONE glob list suppressing hook enforcement per file path. Seeded empty\n// (enforce everywhere) — a client adds paths (e.g. \"repositories/**\") to exempt vendored trees.\n// webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design\nfunction seedExcludePaths(): string[] {\n return [];\n}\n\n// Bring an existing `excludePaths` forward to the single-list shape. Already a list → untouched.\n// Legacy `{ rules, guards }` → unioned (order preserved, duplicates dropped) and recorded as a change\n// so `wp-install-ai-hooks` is the migration path rather than a hand-edit. Anything else → seeded [].\n// webpieces-disable no-any-unknown -- `raw` is opaque consumer JSON until narrowed here\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateExcludePaths(raw: unknown, changes: string[]): string[] {\n if (Array.isArray(raw)) return (raw as string[]).filter(p => typeof p === 'string');\n if (typeof raw === 'object' && raw !== null) {\n // webpieces-disable no-any-unknown -- narrowing the opaque legacy block from consumer JSON\n const legacy = raw as Record<string, unknown>;\n const rules = Array.isArray(legacy['rules']) ? (legacy['rules'] as string[]) : [];\n const guards = Array.isArray(legacy['guards']) ? (legacy['guards'] as string[]) : [];\n const merged = [...new Set([...rules, ...guards].filter(p => typeof p === 'string'))];\n changes.push(`migrated excludePaths {rules,guards} -> one list (${merged.length} path(s))`);\n return merged;\n }\n changes.push('added excludePaths ([])');\n return [];\n}\n\n/** One retired flat command string and the guardHints field it becomes. Data-only (per CLAUDE.md). */\nclass GuardHintMove {\n retiredKey: string;\n hintKey: string;\n fallback: string;\n\n constructor(retiredKey: string, hintKey: string, fallback: string) {\n this.retiredKey = retiredKey;\n this.hintKey = hintKey;\n this.fallback = fallback;\n }\n}\n\n/**\n * Bring `commands` forward to the `guardHints` shape, moving the RETIRED flat `upsertPr`/`mergeComplete`\n * strings and DELETING them. Deleting is the point: the validator now rejects them, so leaving them behind\n * would keep the config failing after a \"successful\" sync.\n *\n * The consumer's own value wins over the default — a repo that renamed its gated command keeps that name.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateGuardHints(commands: Json, changes: string[]): void {\n const hints: Json = (typeof commands['guardHints'] === 'object' && commands['guardHints'] !== null)\n ? (commands['guardHints'] as Json) : {};\n const moves: readonly GuardHintMove[] = [\n new GuardHintMove('upsertPr', 'prCreationOrPush', DEFAULT_UPSERT_PR),\n new GuardHintMove('mergeComplete', 'mergeInProgress', DEFAULT_MERGE_COMPLETE),\n ];\n for (const move of moves) {\n const retiredKey = move.retiredKey;\n const hintKey = move.hintKey;\n const fallback = move.fallback;\n const carried = commands[retiredKey];\n if (carried !== undefined) {\n delete commands[retiredKey];\n if (hints[hintKey] === undefined) hints[hintKey] = carried;\n changes.push(`moved retired commands.${retiredKey} -> commands.guardHints.${hintKey}`);\n }\n if (hints[hintKey] === undefined) {\n hints[hintKey] = fallback;\n changes.push(`added commands.guardHints.${hintKey}`);\n }\n }\n commands['guardHints'] = hints;\n}\n\n/**\n * Apply the RETIRED rule/guard renames in place. These used to be rewritten silently at load time, so a\n * consumer's file kept the dead name forever; the loader now rejects it, which makes this the one command\n * that can fix the file. Skips a rename when the new name is already configured, so an explicit entry is\n * never clobbered by a stale one.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateRetiredRuleNames(section: Section, changes: string[]): void {\n for (const entry of RETIRED_CONFIG_KEYS) {\n if (entry.scope !== RETIRED_SCOPE_RULE) continue;\n if (!(entry.key in section)) continue;\n if (entry.movedTo in section) {\n delete section[entry.key];\n changes.push(`dropped retired \"${entry.key}\" (\"${entry.movedTo}\" is already configured)`);\n continue;\n }\n section[entry.movedTo] = section[entry.key];\n delete section[entry.key];\n changes.push(`renamed retired \"${entry.key}\" -> \"${entry.movedTo}\"`);\n }\n}\n\n// Deep-copy the framework's default match-rules (the no-fetch guard) into plain JSON for the config\n// file. Round-tripping through JSON turns the MatchRuleConfig instances into plain objects.\nfunction seedMatchRules(): Json[] {\n return JSON.parse(JSON.stringify(DEFAULT_MATCH_RULES)) as Json[];\n}\n\nfunction buildSeedConfig(): ConfigFile {\n const rules: Section = {};\n const hookGuards: Section = {};\n for (const name of allRuleNames()) {\n if (sectionForRule(name) === 'hookGuards') hookGuards[name] = seedRule(name);\n else rules[name] = seedRule(name);\n }\n return {\n rules, hookGuards, commands: seedCommands(), excludePaths: seedExcludePaths(),\n // Seed the required match-rules array with the framework's default no-fetch guard. A fresh\n // project gets contract-first enforcement out of the box; clients edit it and add more entries.\n 'match-rules': seedMatchRules(),\n rulesDir: [],\n };\n}\n\nfunction writeConfig(configPath: string, config: ConfigFile): void {\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n}\n\nfunction readConfig(configPath: string): Json {\n const raw = fs.readFileSync(configPath, 'utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as Json;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction asSection(value: Json[string]): Section {\n return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? (value as Section) : {};\n}\n\n// Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.\n// Returns a human-readable list of what changed (empty = already up to date).\nexport function migrate(existing: Json): MigrateResult {\n const changes: string[] = [];\n const rules: Section = asSection(existing['rules']);\n const hookGuards: Section = asSection(existing['hookGuards']);\n const commands: Json = (typeof existing['commands'] === 'object' && existing['commands'] !== null)\n ? (existing['commands'] as Json) : {};\n\n // Move a deprecated top-level pr-gate block under commands.\n if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {\n commands['pr-gate'] = existing['pr-gate'];\n changes.push('moved top-level \"pr-gate\" → commands[\"pr-gate\"]');\n }\n // Apply retired RENAMES first, so a renamed guard is placed and presence-checked under its new name\n // rather than being treated as unknown and re-added alongside its own stale entry.\n migrateRetiredRuleNames(rules, changes);\n migrateRetiredRuleNames(hookGuards, changes);\n\n // Move guards mistakenly left in rules into hookGuards.\n for (const name of Object.keys(rules)) {\n if (isHookGuard(name)) {\n hookGuards[name] = rules[name];\n delete rules[name];\n changes.push(`moved \"${name}\" from rules → hookGuards`);\n }\n }\n // Move code rules mistakenly placed in hookGuards back into rules.\n for (const name of Object.keys(hookGuards)) {\n if (!isHookGuard(name) && allRuleNames().includes(name)) {\n rules[name] = hookGuards[name];\n delete hookGuards[name];\n changes.push(`moved \"${name}\" from hookGuards → rules`);\n }\n }\n // Add any missing built-in into its correct section, ENFORCING at its recommended mode (not OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n const entry = seedRule(name);\n target[name] = entry;\n changes.push(`added \"${name}\" (${String(entry['mode'])}) to ${sectionForRule(name)}`);\n }\n }\n // Fill command defaults.\n if (commands['pr-gate'] === undefined) {\n commands['pr-gate'] = { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] };\n changes.push('added commands[\"pr-gate\"] (OFF)');\n }\n migrateGuardHints(commands, changes);\n\n // Seed the now-required excludePaths list (empty = enforce everywhere) if the config predates it,\n // and MIGRATE the legacy `{ rules: [], guards: [] }` object to the single list by unioning them.\n // The union is behaviour-preserving for every config we have seen (both lists set identically), and\n // widening is the safe direction anyway: a path either side excluded stays excluded.\n const excludePaths: string[] = migrateExcludePaths(existing['excludePaths'], changes);\n\n // Seed the now-required match-rules array (with the default no-fetch guard) if the config predates\n // it. A client that has already customized it keeps their array untouched.\n let matchRules: Json[];\n if (Array.isArray(existing['match-rules'])) {\n matchRules = existing['match-rules'] as Json[];\n } else {\n matchRules = seedMatchRules();\n changes.push('added \"match-rules\" (seeded with the no-fetch guard)');\n }\n\n const rulesDir: string[] = Array.isArray(existing['rulesDir']) ? (existing['rulesDir'] as string[]) : [];\n const config: ConfigFile = { rules, hookGuards, commands, excludePaths, 'match-rules': matchRules, rulesDir };\n if (typeof existing['extends'] === 'string') config.extends = existing['extends'];\n return { config, changes };\n}\n\n// Seed the config when it is missing, migrate it when it is not. ONE behaviour, always — there is no\n// \"migrate but stop here\" mode any more. The flag that used to select it was never NECESSARY (the validator prints\n// the exact edit for every error at once, and editing webpieces.config.json is always allowed through the\n// guard — the documented primary cure), it REFUSED to act when the config was missing (useless in the one\n// case automation would have helped), and it gave deny messages a second competing path when they are\n// supposed to end in exactly one action. Readers also mistook it for the shim-repair command, which it\n// never was — `wp-upgrade-shim` is that.\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 seedOrSyncConfig(projectRoot: string): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands); each rule seeded at its recommended mode — gradual where supported, so only code you change is enforced.`);\n console.log(' Enable the ones you want by changing \"mode\".');\n return;\n }\n const result = migrate(readConfig(configPath));\n if (result.changes.length === 0) {\n console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);\n return;\n }\n writeConfig(configPath, result.config);\n console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);\n for (const change of result.changes) console.log(` - ${change}`);\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code settings.json hook wiring.\n// ---------------------------------------------------------------------------\ninterface HookCommand { type: string; command: string; }\ninterface HookEntry { matcher: string; hooks: HookCommand[]; }\ninterface ClaudeSettings {\n hooks?: { PreToolUse?: HookEntry[] };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\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 referencing `bin`; returns true if anything was removed.\nfunction removeHook(settings: ClaudeSettings, bin: 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(bin));\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\nfunction addHook(settings: ClaudeSettings, matcher: string, command: string): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher, hooks: [{ type: 'command', command }] });\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 for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n const removed = removeHook(settings, hook.bin);\n const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;\n if (isChosen) {\n addHook(settings, hook.matcher, hook.commandFor(target, projectRoot));\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 (!shimReferenced(targets)) {\n removeShim(projectRoot);\n }\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\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\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label} [matcher: ${hook.matcher}]`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n for (const target of targets) console.log(` ${target.choice}) ${target.label}`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n const chosen = targets.find((t: InstallTarget) => t.choice === answer) ?? null;\n applyHook(hook, chosen, 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, 'webpieces.config-policy.md');\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 const chosen = targets.find((t: InstallTarget): boolean => t.choice === choice) ?? null;\n applyHook(RULES_HOOK, chosen, targets, projectRoot);\n applyHook(GUARDS_HOOK, chosen, targets, projectRoot);\n console.log(`\\nDone. Both hooks set to: ${targetName}.`);\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 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":"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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 { allRuleNames, seedEntryForRule, sectionForRule, isHookGuard, DEFAULT_MATCH_RULES, RETIRED_CONFIG_KEYS, RETIRED_SCOPE_RULE, RepoRootFinder, writeTemplate, writeTemplateIfMissing } from '@webpieces/rules-config';\n\nimport { toError } from '../core/to-error';\nimport { SHIM_MARKER, shimPath, renderShim } from './shim';\nimport { GUARANTEE_ROOT_MARKER, guaranteeRootPath, writeGuaranteeRoot } from './guarantee-root';\nimport {\n ClaudeSettings, HookCommand, HookEntry, HookRegistrationEntry, GUARANTEE_ROOT_ENTRY, GUARDS_BIN,\n GUARDS_MATCHER, RULES_BIN, RULES_MATCHER, addHookEntry, readSettings, shimCommand, writeSettings,\n} from './hook-registration';\n\n// Re-exported for back-compat (setup.spec.ts + external callers). The shim body + path now live in\n// ./shim (shared with the runtime self-heal in hook-core). See shim.ts for the single source of truth.\nexport { renderShim };\n\nconst CONFIG_FILENAME = 'webpieces.config.json';\nconst DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';\nconst DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';\nconst DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';\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. The L-1 hook is a third registration but is NOT independently installable — it\n// rides with the guards hook (applyGuaranteeRoot), because it exists to protect the RELATIVE\n// resolution of these two and is meaningless without them.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly matcher: string,\n readonly bin: string,\n ) {}\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n // Project targets get the RELATIVE shim command (see hook-registration.ts for why relative is the\n // whole point), and the L-1 hook registered beside them is what keeps a relative path from ever\n // failing to resolve — which per the hooks reference would be 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 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// 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 removeGuaranteeRootFile(projectRoot: string): void {\n const target = guaranteeRootPath(projectRoot);\n if (fs.existsSync(target)) fs.rmSync(target);\n}\n\n// Drop every PreToolUse command referencing the L-1 hook; returns true if anything was removed.\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 removeGuaranteeRootHook(settings: ClaudeSettings): boolean {\n return removeHookByMarker(settings, GUARANTEE_ROOT_MARKER);\n}\n\n/**\n * Install / move / uninstall the L-1 hook (guarantee-root.sh) alongside the GUARDS hook.\n *\n * It rides with the guards hook and not the rules hook because it judges `cd`, `cd` arrives on Bash,\n * and `Bash` is in the guards matcher. It is registered ABSOLUTE while the guard hooks are RELATIVE —\n * that asymmetry is the design, not an oversight (hook-registration.ts states it once).\n *\n * A GLOBAL (absolute) install gets none: those hooks name the bin path directly, so there is no\n * relative path that could fail to resolve, which is the only thing L-1 protects against.\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 applyGuaranteeRoot(chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void {\n const relative = chosen !== null && !chosen.absolute;\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n const removed = removeGuaranteeRootHook(settings);\n if (relative && chosen.settingsPath === target.settingsPath) {\n addHookEntry(settings, GUARANTEE_ROOT_ENTRY);\n writeSettings(target.settingsPath, settings);\n console.log(` ✅ L-1 guarantee-root hook (keeps the shell where the relative guard hooks can launch) → ${target.label}`);\n } else if (removed) {\n writeSettings(target.settingsPath, settings);\n }\n }\n if (relative) writeGuaranteeRoot(projectRoot);\n else if (!markerReferenced(targets, GUARANTEE_ROOT_MARKER)) removeGuaranteeRootFile(projectRoot);\n}\n\nexport class InstallTarget {\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n ) {}\n}\n\n// The matchers and bin names come from ./hook-registration, which is also what the drift check and\n// wp-upgrade-shim compare against — one spelling of the registration, or the installer and the\n// validator can disagree about what \"installed\" means.\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', RULES_MATCHER, RULES_BIN);\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', GUARDS_MATCHER, GUARDS_BIN);\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).\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),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// webpieces.config.json seeding + migration to the rules / hookGuards / commands layout.\n// ---------------------------------------------------------------------------\n// webpieces-disable no-any-unknown -- webpieces.config.json / settings.json are opaque consumer JSON\ntype Json = Record<string, unknown>;\ntype RuleEntry = Json;\ntype Section = Record<string, RuleEntry>;\n\ninterface ConfigFile {\n extends?: string;\n rules: Section;\n hookGuards: Section;\n commands: Json;\n excludePaths: string[];\n 'match-rules': Json[];\n rulesDir: string[];\n}\n\ninterface MigrateResult {\n config: ConfigFile;\n changes: string[];\n}\n\n// webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design\nfunction seedRule(ruleName: string): RuleEntry {\n // Both escape hatches are seeded (and REQUIRED) so every rule block shows them: 0 = active,\n // null = no branch scoping. A human/AI edits these to time-box or branch-scope a rule off.\n //\n // The ENTIRE entry comes from rules-config's seedEntryForRule() — the same module that owns the\n // schema the loader validates against, so the installer can never emit an entry the loader\n // rejects. It supplies: the recommended mode (the SAME recommendation the validator prints in its\n // copy-paste snippet, so seed and advice cannot disagree), both hatches, and a default for every\n // other schema-REQUIRED field. Seeding used to be a flat 'OFF' plus the two hatches, which was\n // wrong twice over: adopters got nothing enforced, AND the entry was missing required fields\n // (e.g. branch-creation-guard.autoReapMergedBranches), so the config failed to load on first run.\n return seedEntryForRule(ruleName);\n}\n\n// The guard-hint command strings live under `guardHints`. The flat `upsertPr`/`mergeComplete` keys this\n// used to seed are RETIRED and now fail validation — seeding them meant every freshly installed repo was\n// born on a shape the validator rejects.\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n guardHints: { prCreationOrPush: DEFAULT_UPSERT_PR, mergeInProgress: DEFAULT_MERGE_COMPLETE },\n };\n}\n\n// Required excludePaths block: ONE glob list suppressing hook enforcement per file path. Seeded empty\n// (enforce everywhere) — a client adds paths (e.g. \"repositories/**\") to exempt vendored trees.\n// webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design\nfunction seedExcludePaths(): string[] {\n return [];\n}\n\n// Bring an existing `excludePaths` forward to the single-list shape. Already a list → untouched.\n// Legacy `{ rules, guards }` → unioned (order preserved, duplicates dropped) and recorded as a change\n// so `wp-install-ai-hooks` is the migration path rather than a hand-edit. Anything else → seeded [].\n// webpieces-disable no-any-unknown -- `raw` is opaque consumer JSON until narrowed here\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateExcludePaths(raw: unknown, changes: string[]): string[] {\n if (Array.isArray(raw)) return (raw as string[]).filter(p => typeof p === 'string');\n if (typeof raw === 'object' && raw !== null) {\n // webpieces-disable no-any-unknown -- narrowing the opaque legacy block from consumer JSON\n const legacy = raw as Record<string, unknown>;\n const rules = Array.isArray(legacy['rules']) ? (legacy['rules'] as string[]) : [];\n const guards = Array.isArray(legacy['guards']) ? (legacy['guards'] as string[]) : [];\n const merged = [...new Set([...rules, ...guards].filter(p => typeof p === 'string'))];\n changes.push(`migrated excludePaths {rules,guards} -> one list (${merged.length} path(s))`);\n return merged;\n }\n changes.push('added excludePaths ([])');\n return [];\n}\n\n/** One retired flat command string and the guardHints field it becomes. Data-only (per CLAUDE.md). */\nclass GuardHintMove {\n retiredKey: string;\n hintKey: string;\n fallback: string;\n\n constructor(retiredKey: string, hintKey: string, fallback: string) {\n this.retiredKey = retiredKey;\n this.hintKey = hintKey;\n this.fallback = fallback;\n }\n}\n\n/**\n * Bring `commands` forward to the `guardHints` shape, moving the RETIRED flat `upsertPr`/`mergeComplete`\n * strings and DELETING them. Deleting is the point: the validator now rejects them, so leaving them behind\n * would keep the config failing after a \"successful\" sync.\n *\n * The consumer's own value wins over the default — a repo that renamed its gated command keeps that name.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateGuardHints(commands: Json, changes: string[]): void {\n const hints: Json = (typeof commands['guardHints'] === 'object' && commands['guardHints'] !== null)\n ? (commands['guardHints'] as Json) : {};\n const moves: readonly GuardHintMove[] = [\n new GuardHintMove('upsertPr', 'prCreationOrPush', DEFAULT_UPSERT_PR),\n new GuardHintMove('mergeComplete', 'mergeInProgress', DEFAULT_MERGE_COMPLETE),\n ];\n for (const move of moves) {\n const retiredKey = move.retiredKey;\n const hintKey = move.hintKey;\n const fallback = move.fallback;\n const carried = commands[retiredKey];\n if (carried !== undefined) {\n delete commands[retiredKey];\n if (hints[hintKey] === undefined) hints[hintKey] = carried;\n changes.push(`moved retired commands.${retiredKey} -> commands.guardHints.${hintKey}`);\n }\n if (hints[hintKey] === undefined) {\n hints[hintKey] = fallback;\n changes.push(`added commands.guardHints.${hintKey}`);\n }\n }\n commands['guardHints'] = hints;\n}\n\n/**\n * Apply the RETIRED rule/guard renames in place. These used to be rewritten silently at load time, so a\n * consumer's file kept the dead name forever; the loader now rejects it, which makes this the one command\n * that can fix the file. Skips a rename when the new name is already configured, so an explicit entry is\n * never clobbered by a stale one.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateRetiredRuleNames(section: Section, changes: string[]): void {\n for (const entry of RETIRED_CONFIG_KEYS) {\n if (entry.scope !== RETIRED_SCOPE_RULE) continue;\n if (!(entry.key in section)) continue;\n if (entry.movedTo in section) {\n delete section[entry.key];\n changes.push(`dropped retired \"${entry.key}\" (\"${entry.movedTo}\" is already configured)`);\n continue;\n }\n section[entry.movedTo] = section[entry.key];\n delete section[entry.key];\n changes.push(`renamed retired \"${entry.key}\" -> \"${entry.movedTo}\"`);\n }\n}\n\n// Deep-copy the framework's default match-rules (the no-fetch guard) into plain JSON for the config\n// file. Round-tripping through JSON turns the MatchRuleConfig instances into plain objects.\nfunction seedMatchRules(): Json[] {\n return JSON.parse(JSON.stringify(DEFAULT_MATCH_RULES)) as Json[];\n}\n\nfunction buildSeedConfig(): ConfigFile {\n const rules: Section = {};\n const hookGuards: Section = {};\n for (const name of allRuleNames()) {\n if (sectionForRule(name) === 'hookGuards') hookGuards[name] = seedRule(name);\n else rules[name] = seedRule(name);\n }\n return {\n rules, hookGuards, commands: seedCommands(), excludePaths: seedExcludePaths(),\n // Seed the required match-rules array with the framework's default no-fetch guard. A fresh\n // project gets contract-first enforcement out of the box; clients edit it and add more entries.\n 'match-rules': seedMatchRules(),\n rulesDir: [],\n };\n}\n\nfunction writeConfig(configPath: string, config: ConfigFile): void {\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n}\n\nfunction readConfig(configPath: string): Json {\n const raw = fs.readFileSync(configPath, 'utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as Json;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction asSection(value: Json[string]): Section {\n return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? (value as Section) : {};\n}\n\n// Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.\n// Returns a human-readable list of what changed (empty = already up to date).\nexport function migrate(existing: Json): MigrateResult {\n const changes: string[] = [];\n const rules: Section = asSection(existing['rules']);\n const hookGuards: Section = asSection(existing['hookGuards']);\n const commands: Json = (typeof existing['commands'] === 'object' && existing['commands'] !== null)\n ? (existing['commands'] as Json) : {};\n\n // Move a deprecated top-level pr-gate block under commands.\n if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {\n commands['pr-gate'] = existing['pr-gate'];\n changes.push('moved top-level \"pr-gate\" → commands[\"pr-gate\"]');\n }\n // Apply retired RENAMES first, so a renamed guard is placed and presence-checked under its new name\n // rather than being treated as unknown and re-added alongside its own stale entry.\n migrateRetiredRuleNames(rules, changes);\n migrateRetiredRuleNames(hookGuards, changes);\n\n // Move guards mistakenly left in rules into hookGuards.\n for (const name of Object.keys(rules)) {\n if (isHookGuard(name)) {\n hookGuards[name] = rules[name];\n delete rules[name];\n changes.push(`moved \"${name}\" from rules → hookGuards`);\n }\n }\n // Move code rules mistakenly placed in hookGuards back into rules.\n for (const name of Object.keys(hookGuards)) {\n if (!isHookGuard(name) && allRuleNames().includes(name)) {\n rules[name] = hookGuards[name];\n delete hookGuards[name];\n changes.push(`moved \"${name}\" from hookGuards → rules`);\n }\n }\n // Add any missing built-in into its correct section, ENFORCING at its recommended mode (not OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n const entry = seedRule(name);\n target[name] = entry;\n changes.push(`added \"${name}\" (${String(entry['mode'])}) to ${sectionForRule(name)}`);\n }\n }\n // Fill command defaults.\n if (commands['pr-gate'] === undefined) {\n commands['pr-gate'] = { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] };\n changes.push('added commands[\"pr-gate\"] (OFF)');\n }\n migrateGuardHints(commands, changes);\n\n // Seed the now-required excludePaths list (empty = enforce everywhere) if the config predates it,\n // and MIGRATE the legacy `{ rules: [], guards: [] }` object to the single list by unioning them.\n // The union is behaviour-preserving for every config we have seen (both lists set identically), and\n // widening is the safe direction anyway: a path either side excluded stays excluded.\n const excludePaths: string[] = migrateExcludePaths(existing['excludePaths'], changes);\n\n // Seed the now-required match-rules array (with the default no-fetch guard) if the config predates\n // it. A client that has already customized it keeps their array untouched.\n let matchRules: Json[];\n if (Array.isArray(existing['match-rules'])) {\n matchRules = existing['match-rules'] as Json[];\n } else {\n matchRules = seedMatchRules();\n changes.push('added \"match-rules\" (seeded with the no-fetch guard)');\n }\n\n const rulesDir: string[] = Array.isArray(existing['rulesDir']) ? (existing['rulesDir'] as string[]) : [];\n const config: ConfigFile = { rules, hookGuards, commands, excludePaths, 'match-rules': matchRules, rulesDir };\n if (typeof existing['extends'] === 'string') config.extends = existing['extends'];\n return { config, changes };\n}\n\n// Seed the config when it is missing, migrate it when it is not. ONE behaviour, always — there is no\n// \"migrate but stop here\" mode any more. The flag that used to select it was never NECESSARY (the validator prints\n// the exact edit for every error at once, and editing webpieces.config.json is always allowed through the\n// guard — the documented primary cure), it REFUSED to act when the config was missing (useless in the one\n// case automation would have helped), and it gave deny messages a second competing path when they are\n// supposed to end in exactly one action. Readers also mistook it for the shim-repair command, which it\n// never was — `wp-upgrade-shim` is that.\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 seedOrSyncConfig(projectRoot: string): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands); each rule seeded at its recommended mode — gradual where supported, so only code you change is enforced.`);\n console.log(' Enable the ones you want by changing \"mode\".');\n return;\n }\n const result = migrate(readConfig(configPath));\n if (result.changes.length === 0) {\n console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);\n return;\n }\n writeConfig(configPath, result.config);\n console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);\n for (const change of result.changes) console.log(` - ${change}`);\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 the old absolute\n// spelling beside the new relative one — two spellings of one registration.\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 for (const target of targets) {\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.matcher, hook.commandFor(target, projectRoot)));\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 L-1 hook rides with the GUARDS hook (it judges `cd`, which arrives on Bash). Doing it here\n // rather than at a separate call site means every existing caller of applyHook — the installer's\n // interactive and --target paths, and every test — gets the three-hook form with no second step.\n if (hook.bin === GUARDS_BIN) applyGuaranteeRoot(chosen, targets, projectRoot);\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\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\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label} [matcher: ${hook.matcher}]`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n for (const target of targets) console.log(` ${target.choice}) ${target.label}`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n const chosen = targets.find((t: InstallTarget) => t.choice === answer) ?? null;\n applyHook(hook, chosen, 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, 'webpieces.config-policy.md');\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 const chosen = targets.find((t: InstallTarget): boolean => t.choice === choice) ?? null;\n applyHook(RULES_HOOK, chosen, targets, projectRoot);\n applyHook(GUARDS_HOOK, chosen, targets, projectRoot);\n console.log(`\\nDone. Both hooks set to: ${targetName}.`);\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 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"]}
|
|
@@ -3,7 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.WP_LOG_SH = exports.RESOLVE_LOG_DIR_SH = exports.SHIM_LOG_FAULTS = exports.SHIM_LOG_VERDICTS = exports.SHIM_LOG_MAX_BYTES = exports.SHIM_LOG_FILE_PREV = exports.SHIM_LOG_FILE = void 0;
|
|
4
4
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
|
-
// THE L0 AUDIT LOG, in POSIX sh — the shim half of
|
|
6
|
+
// THE L0 AUDIT LOG, in POSIX sh — the shim half of
|
|
7
|
+
// `.webpieces/**/logs/<session>-<agent|coordinator>-<binName>-ai-hook-shim.log`. The stream prefix is
|
|
8
|
+
// the sh twin of ai-hook-rules' LogStream.fileName(): wp-ai-guards-hook and wp-ai-rules-hook are run
|
|
9
|
+
// IN PARALLEL by Claude Code on every file edit, so an unsplit name means two writers, one file, and
|
|
10
|
+
// torn appends above PIPE_BUF. A payload with no session_id renders 'unknown', never a bare name —
|
|
11
|
+
// there is no un-prefixed spelling on either side.
|
|
7
12
|
//
|
|
8
13
|
// Split out of ./shim.ts (which renders the shim body) purely so both stay readable; shim.ts splices
|
|
9
14
|
// these fragments in verbatim and re-exports the constants. Like l0-allowlist.ts, this module must
|
|
@@ -128,17 +133,29 @@ exports.RESOLVE_LOG_DIR_SH = `wp_resolve_log_dir() {
|
|
|
128
133
|
exports.WP_LOG_SH = `WP_TREE=""
|
|
129
134
|
WP_LOG_DIR=""
|
|
130
135
|
${exports.RESOLVE_LOG_DIR_SH}
|
|
136
|
+
wp_clean() { # one path segment from an UNTRUSTED payload id — twin of LogStream's segment()
|
|
137
|
+
printf '%s' "$1" | tr -c 'A-Za-z0-9._-' '_' | sed -e 's/\\.\\{2,\\}/_/g' -e 's/^\\.\\{1,\\}/_/' | cut -c1-64
|
|
138
|
+
}
|
|
131
139
|
wp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label
|
|
132
140
|
{
|
|
133
141
|
[ -n "$WP_LOG_DIR" ] || wp_resolve_log_dir
|
|
134
142
|
mkdir -p "$WP_LOG_DIR" 2>/dev/null || return 0
|
|
135
|
-
|
|
143
|
+
# Same flat scheme as LogStream.fileName(): <session>-<agent|coordinator>-<hook>-<base>. $BIN_NAME
|
|
144
|
+
# IS the hook discriminator here (wp-ai-guards-hook vs wp-ai-rules-hook), and Claude Code runs those
|
|
145
|
+
# two IN PARALLEL on every file edit — without this prefix they append to ONE file and tear above
|
|
146
|
+
# PIPE_BUF. An empty session id renders 'unknown' — this has no bare-name branch, matching
|
|
147
|
+
# LogStream.fileName(), which has none either.
|
|
148
|
+
# ALWAYS prefixed - a missing session_id renders as 'unknown', never as the shared bare name.
|
|
149
|
+
# Gating this on a non-empty id would drop both parallel hooks back onto one file, which is the
|
|
150
|
+
# torn-append case this exists to remove. Twin of LogStream.fileName(), which has no bare branch.
|
|
151
|
+
_wp_pfx="$(wp_clean "\${WP_SID:-unknown}")-$(wp_clean "\${WP_AID:-coordinator}")-$BIN_NAME-"
|
|
152
|
+
_wp_f="$WP_LOG_DIR/\${_wp_pfx}${exports.SHIM_LOG_FILE}"
|
|
136
153
|
# Rotate at the SAME 512 KB into the SAME .1.log sibling as every JS-side webpieces log. This runs
|
|
137
154
|
# on every tool call, so it is one wc and no more; a size we cannot read counts as 0 (no rotation).
|
|
138
155
|
_wp_sz="$(wc -c < "$_wp_f" 2>/dev/null | tr -d ' ')"
|
|
139
156
|
case "$_wp_sz" in ''|*[!0-9]*) _wp_sz=0 ;; esac
|
|
140
|
-
[ "$_wp_sz" -gt ${String(exports.SHIM_LOG_MAX_BYTES)} ] && mv -f "$_wp_f" "$WP_LOG_DIR
|
|
141
|
-
printf '%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n' "$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)" "$BIN_NAME" "$TOOL" "tree=$WP_TREE" "fault=$1" "$2" "$
|
|
157
|
+
[ "$_wp_sz" -gt ${String(exports.SHIM_LOG_MAX_BYTES)} ] && mv -f "$_wp_f" "$WP_LOG_DIR/\${_wp_pfx}${exports.SHIM_LOG_FILE_PREV}" 2>/dev/null
|
|
158
|
+
printf '%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n' "$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)" "$BIN_NAME" "$TOOL" "tree=$WP_TREE" "fault=$1" "$2" "$CMD_LOG" >> "$_wp_f"
|
|
142
159
|
} 2>/dev/null || true
|
|
143
160
|
}`;
|
|
144
161
|
//# sourceMappingURL=shim-audit-log.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shim-audit-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-audit-log.ts"],"names":[],"mappings":";;;AAAA,0DAAgG;AAEhG,8EAA8E;AAC9E,0FAA0F;AAC1F,EAAE;AACF,qGAAqG;AACrG,mGAAmG;AACnG,uGAAuG;AACvG,EAAE;AACF,sGAAsG;AACtG,sGAAsG;AACtG,uGAAuG;AACvG,oGAAoG;AACpG,sGAAsG;AACtG,sGAAsG;AACtG,qGAAqG;AACrG,yFAAyF;AACzF,EAAE;AACF,iCAAiC;AACjC,oGAAoG;AACpG,sGAAsG;AACtG,wDAAwD;AACxD,oEAAoE;AACpE,8EAA8E;AAE9E,mGAAmG;AACtF,QAAA,aAAa,GAAG,kBAAkB,CAAC;AACnC,QAAA,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACU,QAAA,kBAAkB,GAAG,GAAG,GAAG,IAAI,CAAC;AAE7C;;;;;;;;;;;;;;;GAeG;AACU,QAAA,iBAAiB,GAAG;IAC7B,gBAAgB,EAAE,gBAAgB,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY;IAC9E,MAAM,EAAE,YAAY,EAAE,aAAa;CAC7B,CAAC;AAEX;;;;GAIG;AACU,QAAA,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAC;AAElE;;;;;;;;;;;;;;;;;;;GAmBG;AACU,QAAA,kBAAkB,GAAG;;;;;2CAKS,gCAAiB,IAAI,6BAAc;;;;;;;;;;;;;;+BAc/C,gCAAiB,IAAI,6BAAc;;;;;+BAKnC,gCAAiB,IAAI,iCAAkB,aAAa,6BAAc;;EAE/F,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACU,QAAA,SAAS,GAAG;;EAEvB,0BAAkB;;;;;yBAKK,qBAAa;;;;;sBAKhB,MAAM,CAAC,0BAAkB,CAAC,qCAAqC,0BAAkB;;;EAGrG,CAAC","sourcesContent":["import { LOGS_STATE_DIR, WORKTREE_STATE_DIR, WEBPIECES_TMP_DIR } from '@webpieces/rules-config';\n\n// ---------------------------------------------------------------------------\n// THE L0 AUDIT LOG, in POSIX sh — the shim half of `.webpieces/**/logs/ai-hook-shim.log`.\n//\n// Split out of ./shim.ts (which renders the shim body) purely so both stay readable; shim.ts splices\n// these fragments in verbatim and re-exports the constants. Like l0-allowlist.ts, this module must\n// stay dependency-light: the shim it renders has to work on a tree too broken to load the rule engine.\n//\n// ─── What changed, and why it is not just a bigger log ─────────────────────────────────────────────\n// This log used to be a FAULT log wearing an audit log's name. `wp_log` fired only on the fail-closed\n// path (ALLOW-READ / ALLOW-CONFIG / ALLOW-CURE / DENY*), so a HEALTHY call — the overwhelming majority\n// — exec'd the bin and recorded nothing at all. You could therefore never answer \"what did L0 do to\n// this tool call?\", only \"what did L0 do on the calls where L0 was already broken\". Absence of a line\n// meant either \"healthy\" or \"the shim never ran\", and those are the two answers you most need to tell\n// apart. Every path now logs exactly one line, including the pass-through, so the file can be diffed\n// against the documented matrix in guards/L0-tooling.md rather than merely spot-checked.\n//\n// Two more defects went with it:\n// • it wrote to a hardcoded `$ROOT/.webpieces/logs`, so every worktree's lines landed in one flat\n// file (or, worse, in whichever tree happened to hold the shim) instead of the per-tree namespace\n// the L1 binary has used since the state-dir split;\n// • it had NO rotation, on a file now written on EVERY tool call.\n// ---------------------------------------------------------------------------\n\n/** The audit log's filename, and the sibling a rotation renames it to. Mirrors decision-log.ts. */\nexport const SHIM_LOG_FILE = 'ai-hook-shim.log';\nexport const SHIM_LOG_FILE_PREV = 'ai-hook-shim.1.log';\n\n/**\n * Rotation threshold, in bytes — 512 KB, the SAME number and the same `.1.log` naming as\n * decision-log.ts / rejection-log.ts / main-sync-log.ts. Deliberately identical rather than merely\n * similar: two log families in one directory with two different retention rules is a trap for whoever\n * later tries to reason about how much history they still have.\n */\nexport const SHIM_LOG_MAX_BYTES = 512 * 1024;\n\n/**\n * The verdict vocabulary one shim invocation can record, and how each maps to guards/L0-tooling.md.\n *\n * The three ALLOW-* and three DENY-* labels are the ones this log has always used and are kept\n * verbatim, so anything already grepping them keeps working. `PASS-BIN-*` is new: it is the healthy\n * case the log used to be silent about.\n *\n * PASS-BIN-ALLOW no sh-side fault; the bin ran and returned 0 → matrix row 1 (no fault → L1)\n * PASS-BIN-BLOCK no sh-side fault; the bin ran and returned 2 → matrix row 1; a LATER layer blocked\n * ALLOW-READ allowlist entry 1 (any Read) → PASS, terminal here (use case 10)\n * ALLOW-CONFIG allowlist entry 2 (webpieces.config.json) → PASS, terminal here\n * ALLOW-CURE allowlist entries 3-8 (a cure command) → ALLOW\n * DENY fault X, not on the allowlist → BLOCK_AI_CURE\n * DENY-STALE fault D, not on the allowlist → BLOCK_AI_CURE\n * DENY-BROKEN fault K, not on the allowlist → BLOCK_AI_CURE\n */\nexport const SHIM_LOG_VERDICTS = [\n 'PASS-BIN-ALLOW', 'PASS-BIN-BLOCK', 'ALLOW-READ', 'ALLOW-CONFIG', 'ALLOW-CURE',\n 'DENY', 'DENY-STALE', 'DENY-BROKEN',\n] as const;\n\n/**\n * The sh-side L0 fault codes, exactly as guards/L0-tooling.md names them. `-` means \"no sh-side fault\" —\n * the shim cannot classify S / C / Y, which the BINARY detects (and logs through its own streams), so\n * a `-` here is a statement about this layer only, not a claim that nothing was wrong.\n */\nexport const SHIM_LOG_FAULTS = ['D', 'X', 'U', 'K', '-'] as const;\n\n/**\n * Shell fragment: derive WHERE this call's log belongs — the sh TWIN of `DotWebpieces.local()` +\n * `worktreeName()` + `primaryRoot()` in @webpieces/rules-config.\n *\n * sh cannot import TypeScript, so this derivation is duplicated by necessity; the mitigation is\n * `shim-audit-log.spec.ts`, which runs THIS function through a real /bin/sh in real git worktrees and\n * asserts it returns exactly what `dotWebpieces.worktreeName()` returns. If the two ever disagree the\n * lock goes red rather than the logs quietly splitting in half.\n *\n * It asks git the SAME question the TS side asks — `--git-dir` vs `--git-common-dir`, which differ if\n * and only if this is a linked worktree — but in ONE `rev-parse` (it accepts both flags and prints a\n * line each) rather than two, because this runs on the blocking path of every tool call.\n *\n * The tree is derived from the PAYLOAD's `cwd` (Claude Code documents it as the working directory the\n * hook was invoked from), not from `$ROOT`. `$ROOT` is where the shim FILE lives and stays the anchor\n * for what the drift guard MEASURES — this fragment changes only where the log is WRITTEN.\n *\n * Fails soft, exactly like the TS side: when git cannot answer, the log collapses to\n * `<cwd>/.webpieces/logs`, which is the pre-change behaviour.\n */\nexport const RESOLVE_LOG_DIR_SH = `wp_resolve_log_dir() {\n _wp_rp=\"$(git -C \"$WP_CWD\" rev-parse --git-dir --git-common-dir 2>/dev/null)\"\n _wp_gd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 1p)\"\n _wp_cd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 2p)\"\n if [ -z \"$_wp_gd\" ] || [ -z \"$_wp_cd\" ]; then\n WP_TREE=primary; WP_LOG_DIR=\"$WP_CWD/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"; return 0\n fi\n # git prints a BARE .git from the primary clone and an absolute path from a linked worktree; the TS\n # twin runs path.resolve(cwd, printed), so do the same before comparing or taking a basename.\n case \"$_wp_gd\" in /*) : ;; *) _wp_gd=\"$WP_CWD/$_wp_gd\" ;; esac\n case \"$_wp_cd\" in /*) : ;; *) _wp_cd=\"$WP_CWD/$_wp_cd\" ;; esac\n # The primary clone's root is the parent of the SHARED git dir — declining any layout whose shared\n # dir is not named .git (a bare repo, --separate-git-dir), same test as primaryRoot().\n _wp_primary=\"$WP_CWD\"\n case \"$_wp_cd\" in\n */.git) [ -d \"\\${_wp_cd%/*}\" ] && _wp_primary=\"\\${_wp_cd%/*}\" ;;\n esac\n if [ \"$_wp_gd\" = \"$_wp_cd\" ]; then\n WP_TREE=primary\n WP_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"\n else\n # git's OWN name for the worktree (the basename of <primary>/.git/worktrees/<name>), not the\n # directory's basename — two worktrees under different parents may share a directory name.\n WP_TREE=\"\\${_wp_gd##*/}\"\n WP_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${WORKTREE_STATE_DIR}/$WP_TREE/${LOGS_STATE_DIR}\"\n fi\n}`;\n\n/**\n * Shell fragment: the audit-log writer itself — `wp_log <fault> <verdict>`, one tab-separated line.\n *\n * FORMAT (7 fields, tab-separated, append-only):\n * <iso-ts> <bin-name> <tool> tree=<name|primary> fault=<D|X|K|-> <VERDICT> <command>\n *\n * `tree=` and `fault=` are the two fields that make the file reconcilable against guards/L0-tooling.md:\n * the first says WHICH checkout produced the line (a shared log across seven worktrees is otherwise\n * unreadable), the second says which of the six documented faults the sh half detected. The verdict\n * keeps its historical spelling and stays adjacent to the command, so `grep 'DENY-STALE\\\\t'` still\n * finds what it always found.\n *\n * NEVER breaks or blocks the hook: the whole body is wrapped so a failure of any kind — unwritable\n * directory, read-only filesystem, missing `git` — is swallowed, and nothing is ever written to\n * stdout (stdout is the PreToolUse decision channel; a stray byte there corrupts allow/deny).\n *\n * The log dir is resolved LAZILY on first use so a call that never logs never pays for the git probe.\n */\nexport const WP_LOG_SH = `WP_TREE=\"\"\nWP_LOG_DIR=\"\"\n${RESOLVE_LOG_DIR_SH}\nwp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label\n {\n [ -n \"$WP_LOG_DIR\" ] || wp_resolve_log_dir\n mkdir -p \"$WP_LOG_DIR\" 2>/dev/null || return 0\n _wp_f=\"$WP_LOG_DIR/${SHIM_LOG_FILE}\"\n # Rotate at the SAME 512 KB into the SAME .1.log sibling as every JS-side webpieces log. This runs\n # on every tool call, so it is one wc and no more; a size we cannot read counts as 0 (no rotation).\n _wp_sz=\"$(wc -c < \"$_wp_f\" 2>/dev/null | tr -d ' ')\"\n case \"$_wp_sz\" in ''|*[!0-9]*) _wp_sz=0 ;; esac\n [ \"$_wp_sz\" -gt ${String(SHIM_LOG_MAX_BYTES)} ] && mv -f \"$_wp_f\" \"$WP_LOG_DIR/${SHIM_LOG_FILE_PREV}\" 2>/dev/null\n printf '%s\\\\t%s\\\\t%s\\\\t%s\\\\t%s\\\\t%s\\\\t%s\\\\n' \"$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)\" \"$BIN_NAME\" \"$TOOL\" \"tree=$WP_TREE\" \"fault=$1\" \"$2\" \"$CMD\" >> \"$_wp_f\"\n } 2>/dev/null || true\n}`;\n"]}
|
|
1
|
+
{"version":3,"file":"shim-audit-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-audit-log.ts"],"names":[],"mappings":";;;AAAA,0DAAgG;AAEhG,8EAA8E;AAC9E,mDAAmD;AACnD,sGAAsG;AACtG,qGAAqG;AACrG,qGAAqG;AACrG,mGAAmG;AACnG,mDAAmD;AACnD,EAAE;AACF,qGAAqG;AACrG,mGAAmG;AACnG,uGAAuG;AACvG,EAAE;AACF,sGAAsG;AACtG,sGAAsG;AACtG,uGAAuG;AACvG,oGAAoG;AACpG,sGAAsG;AACtG,sGAAsG;AACtG,qGAAqG;AACrG,yFAAyF;AACzF,EAAE;AACF,iCAAiC;AACjC,oGAAoG;AACpG,sGAAsG;AACtG,wDAAwD;AACxD,oEAAoE;AACpE,8EAA8E;AAE9E,mGAAmG;AACtF,QAAA,aAAa,GAAG,kBAAkB,CAAC;AACnC,QAAA,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACU,QAAA,kBAAkB,GAAG,GAAG,GAAG,IAAI,CAAC;AAE7C;;;;;;;;;;;;;;;GAeG;AACU,QAAA,iBAAiB,GAAG;IAC7B,gBAAgB,EAAE,gBAAgB,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY;IAC9E,MAAM,EAAE,YAAY,EAAE,aAAa;CAC7B,CAAC;AAEX;;;;GAIG;AACU,QAAA,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAC;AAElE;;;;;;;;;;;;;;;;;;;GAmBG;AACU,QAAA,kBAAkB,GAAG;;;;;2CAKS,gCAAiB,IAAI,6BAAc;;;;;;;;;;;;;;+BAc/C,gCAAiB,IAAI,6BAAc;;;;;+BAKnC,gCAAiB,IAAI,iCAAkB,aAAa,6BAAc;;EAE/F,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACU,QAAA,SAAS,GAAG;;EAEvB,0BAAkB;;;;;;;;;;;;;;;;;oCAiBgB,qBAAa;;;;;sBAK3B,MAAM,CAAC,0BAAkB,CAAC,gDAAgD,0BAAkB;;;EAGhH,CAAC","sourcesContent":["import { LOGS_STATE_DIR, WORKTREE_STATE_DIR, WEBPIECES_TMP_DIR } from '@webpieces/rules-config';\n\n// ---------------------------------------------------------------------------\n// THE L0 AUDIT LOG, in POSIX sh — the shim half of\n// `.webpieces/**/logs/<session>-<agent|coordinator>-<binName>-ai-hook-shim.log`. The stream prefix is\n// the sh twin of ai-hook-rules' LogStream.fileName(): wp-ai-guards-hook and wp-ai-rules-hook are run\n// IN PARALLEL by Claude Code on every file edit, so an unsplit name means two writers, one file, and\n// torn appends above PIPE_BUF. A payload with no session_id renders 'unknown', never a bare name —\n// there is no un-prefixed spelling on either side.\n//\n// Split out of ./shim.ts (which renders the shim body) purely so both stay readable; shim.ts splices\n// these fragments in verbatim and re-exports the constants. Like l0-allowlist.ts, this module must\n// stay dependency-light: the shim it renders has to work on a tree too broken to load the rule engine.\n//\n// ─── What changed, and why it is not just a bigger log ─────────────────────────────────────────────\n// This log used to be a FAULT log wearing an audit log's name. `wp_log` fired only on the fail-closed\n// path (ALLOW-READ / ALLOW-CONFIG / ALLOW-CURE / DENY*), so a HEALTHY call — the overwhelming majority\n// — exec'd the bin and recorded nothing at all. You could therefore never answer \"what did L0 do to\n// this tool call?\", only \"what did L0 do on the calls where L0 was already broken\". Absence of a line\n// meant either \"healthy\" or \"the shim never ran\", and those are the two answers you most need to tell\n// apart. Every path now logs exactly one line, including the pass-through, so the file can be diffed\n// against the documented matrix in guards/L0-tooling.md rather than merely spot-checked.\n//\n// Two more defects went with it:\n// • it wrote to a hardcoded `$ROOT/.webpieces/logs`, so every worktree's lines landed in one flat\n// file (or, worse, in whichever tree happened to hold the shim) instead of the per-tree namespace\n// the L1 binary has used since the state-dir split;\n// • it had NO rotation, on a file now written on EVERY tool call.\n// ---------------------------------------------------------------------------\n\n/** The audit log's filename, and the sibling a rotation renames it to. Mirrors decision-log.ts. */\nexport const SHIM_LOG_FILE = 'ai-hook-shim.log';\nexport const SHIM_LOG_FILE_PREV = 'ai-hook-shim.1.log';\n\n/**\n * Rotation threshold, in bytes — 512 KB, the SAME number and the same `.1.log` naming as\n * decision-log.ts / rejection-log.ts / main-sync-log.ts. Deliberately identical rather than merely\n * similar: two log families in one directory with two different retention rules is a trap for whoever\n * later tries to reason about how much history they still have.\n */\nexport const SHIM_LOG_MAX_BYTES = 512 * 1024;\n\n/**\n * The verdict vocabulary one shim invocation can record, and how each maps to guards/L0-tooling.md.\n *\n * The three ALLOW-* and three DENY-* labels are the ones this log has always used and are kept\n * verbatim, so anything already grepping them keeps working. `PASS-BIN-*` is new: it is the healthy\n * case the log used to be silent about.\n *\n * PASS-BIN-ALLOW no sh-side fault; the bin ran and returned 0 → matrix row 1 (no fault → L1)\n * PASS-BIN-BLOCK no sh-side fault; the bin ran and returned 2 → matrix row 1; a LATER layer blocked\n * ALLOW-READ allowlist entry 1 (any Read) → PASS, terminal here (use case 10)\n * ALLOW-CONFIG allowlist entry 2 (webpieces.config.json) → PASS, terminal here\n * ALLOW-CURE allowlist entries 3-8 (a cure command) → ALLOW\n * DENY fault X, not on the allowlist → BLOCK_AI_CURE\n * DENY-STALE fault D, not on the allowlist → BLOCK_AI_CURE\n * DENY-BROKEN fault K, not on the allowlist → BLOCK_AI_CURE\n */\nexport const SHIM_LOG_VERDICTS = [\n 'PASS-BIN-ALLOW', 'PASS-BIN-BLOCK', 'ALLOW-READ', 'ALLOW-CONFIG', 'ALLOW-CURE',\n 'DENY', 'DENY-STALE', 'DENY-BROKEN',\n] as const;\n\n/**\n * The sh-side L0 fault codes, exactly as guards/L0-tooling.md names them. `-` means \"no sh-side fault\" —\n * the shim cannot classify S / C / Y, which the BINARY detects (and logs through its own streams), so\n * a `-` here is a statement about this layer only, not a claim that nothing was wrong.\n */\nexport const SHIM_LOG_FAULTS = ['D', 'X', 'U', 'K', '-'] as const;\n\n/**\n * Shell fragment: derive WHERE this call's log belongs — the sh TWIN of `DotWebpieces.local()` +\n * `worktreeName()` + `primaryRoot()` in @webpieces/rules-config.\n *\n * sh cannot import TypeScript, so this derivation is duplicated by necessity; the mitigation is\n * `shim-audit-log.spec.ts`, which runs THIS function through a real /bin/sh in real git worktrees and\n * asserts it returns exactly what `dotWebpieces.worktreeName()` returns. If the two ever disagree the\n * lock goes red rather than the logs quietly splitting in half.\n *\n * It asks git the SAME question the TS side asks — `--git-dir` vs `--git-common-dir`, which differ if\n * and only if this is a linked worktree — but in ONE `rev-parse` (it accepts both flags and prints a\n * line each) rather than two, because this runs on the blocking path of every tool call.\n *\n * The tree is derived from the PAYLOAD's `cwd` (Claude Code documents it as the working directory the\n * hook was invoked from), not from `$ROOT`. `$ROOT` is where the shim FILE lives and stays the anchor\n * for what the drift guard MEASURES — this fragment changes only where the log is WRITTEN.\n *\n * Fails soft, exactly like the TS side: when git cannot answer, the log collapses to\n * `<cwd>/.webpieces/logs`, which is the pre-change behaviour.\n */\nexport const RESOLVE_LOG_DIR_SH = `wp_resolve_log_dir() {\n _wp_rp=\"$(git -C \"$WP_CWD\" rev-parse --git-dir --git-common-dir 2>/dev/null)\"\n _wp_gd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 1p)\"\n _wp_cd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 2p)\"\n if [ -z \"$_wp_gd\" ] || [ -z \"$_wp_cd\" ]; then\n WP_TREE=primary; WP_LOG_DIR=\"$WP_CWD/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"; return 0\n fi\n # git prints a BARE .git from the primary clone and an absolute path from a linked worktree; the TS\n # twin runs path.resolve(cwd, printed), so do the same before comparing or taking a basename.\n case \"$_wp_gd\" in /*) : ;; *) _wp_gd=\"$WP_CWD/$_wp_gd\" ;; esac\n case \"$_wp_cd\" in /*) : ;; *) _wp_cd=\"$WP_CWD/$_wp_cd\" ;; esac\n # The primary clone's root is the parent of the SHARED git dir — declining any layout whose shared\n # dir is not named .git (a bare repo, --separate-git-dir), same test as primaryRoot().\n _wp_primary=\"$WP_CWD\"\n case \"$_wp_cd\" in\n */.git) [ -d \"\\${_wp_cd%/*}\" ] && _wp_primary=\"\\${_wp_cd%/*}\" ;;\n esac\n if [ \"$_wp_gd\" = \"$_wp_cd\" ]; then\n WP_TREE=primary\n WP_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"\n else\n # git's OWN name for the worktree (the basename of <primary>/.git/worktrees/<name>), not the\n # directory's basename — two worktrees under different parents may share a directory name.\n WP_TREE=\"\\${_wp_gd##*/}\"\n WP_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${WORKTREE_STATE_DIR}/$WP_TREE/${LOGS_STATE_DIR}\"\n fi\n}`;\n\n/**\n * Shell fragment: the audit-log writer itself — `wp_log <fault> <verdict>`, one tab-separated line.\n *\n * FORMAT (7 fields, tab-separated, append-only):\n * <iso-ts> <bin-name> <tool> tree=<name|primary> fault=<D|X|K|-> <VERDICT> <command>\n *\n * `tree=` and `fault=` are the two fields that make the file reconcilable against guards/L0-tooling.md:\n * the first says WHICH checkout produced the line (a shared log across seven worktrees is otherwise\n * unreadable), the second says which of the six documented faults the sh half detected. The verdict\n * keeps its historical spelling and stays adjacent to the command, so `grep 'DENY-STALE\\\\t'` still\n * finds what it always found.\n *\n * NEVER breaks or blocks the hook: the whole body is wrapped so a failure of any kind — unwritable\n * directory, read-only filesystem, missing `git` — is swallowed, and nothing is ever written to\n * stdout (stdout is the PreToolUse decision channel; a stray byte there corrupts allow/deny).\n *\n * The log dir is resolved LAZILY on first use so a call that never logs never pays for the git probe.\n */\nexport const WP_LOG_SH = `WP_TREE=\"\"\nWP_LOG_DIR=\"\"\n${RESOLVE_LOG_DIR_SH}\nwp_clean() { # one path segment from an UNTRUSTED payload id — twin of LogStream's segment()\n printf '%s' \"$1\" | tr -c 'A-Za-z0-9._-' '_' | sed -e 's/\\\\.\\\\{2,\\\\}/_/g' -e 's/^\\\\.\\\\{1,\\\\}/_/' | cut -c1-64\n}\nwp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label\n {\n [ -n \"$WP_LOG_DIR\" ] || wp_resolve_log_dir\n mkdir -p \"$WP_LOG_DIR\" 2>/dev/null || return 0\n # Same flat scheme as LogStream.fileName(): <session>-<agent|coordinator>-<hook>-<base>. $BIN_NAME\n # IS the hook discriminator here (wp-ai-guards-hook vs wp-ai-rules-hook), and Claude Code runs those\n # two IN PARALLEL on every file edit — without this prefix they append to ONE file and tear above\n # PIPE_BUF. An empty session id renders 'unknown' — this has no bare-name branch, matching\n # LogStream.fileName(), which has none either.\n # ALWAYS prefixed - a missing session_id renders as 'unknown', never as the shared bare name.\n # Gating this on a non-empty id would drop both parallel hooks back onto one file, which is the\n # torn-append case this exists to remove. Twin of LogStream.fileName(), which has no bare branch.\n _wp_pfx=\"$(wp_clean \"\\${WP_SID:-unknown}\")-$(wp_clean \"\\${WP_AID:-coordinator}\")-$BIN_NAME-\"\n _wp_f=\"$WP_LOG_DIR/\\${_wp_pfx}${SHIM_LOG_FILE}\"\n # Rotate at the SAME 512 KB into the SAME .1.log sibling as every JS-side webpieces log. This runs\n # on every tool call, so it is one wc and no more; a size we cannot read counts as 0 (no rotation).\n _wp_sz=\"$(wc -c < \"$_wp_f\" 2>/dev/null | tr -d ' ')\"\n case \"$_wp_sz\" in ''|*[!0-9]*) _wp_sz=0 ;; esac\n [ \"$_wp_sz\" -gt ${String(SHIM_LOG_MAX_BYTES)} ] && mv -f \"$_wp_f\" \"$WP_LOG_DIR/\\${_wp_pfx}${SHIM_LOG_FILE_PREV}\" 2>/dev/null\n printf '%s\\\\t%s\\\\t%s\\\\t%s\\\\t%s\\\\t%s\\\\t%s\\\\n' \"$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)\" \"$BIN_NAME\" \"$TOOL\" \"tree=$WP_TREE\" \"fault=$1\" \"$2\" \"$CMD_LOG\" >> \"$_wp_f\"\n } 2>/dev/null || true\n}`;\n"]}
|
package/src/bin/shim.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export declare function healShim(cwd: string): void;
|
|
|
9
9
|
export declare function governingShimRoot(moduleDir?: string): string | null;
|
|
10
10
|
export declare function committedShimStale(root?: string | null): boolean;
|
|
11
11
|
export declare function isShimCureCommand(command: string): boolean;
|
|
12
|
-
export declare function shimStaleDenyReason(installedVersion: string, root
|
|
12
|
+
export declare function shimStaleDenyReason(installedVersion: string, root: string, drifted: readonly string[]): string;
|
|
13
13
|
export declare function installedShimRulesVersion(): string;
|
package/src/bin/shim.js
CHANGED
|
@@ -17,6 +17,7 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
17
17
|
const to_error_1 = require("../core/to-error");
|
|
18
18
|
const l0_allowlist_1 = require("./l0-allowlist");
|
|
19
19
|
const shim_audit_log_1 = require("./shim-audit-log");
|
|
20
|
+
const guarantee_root_1 = require("./guarantee-root");
|
|
20
21
|
// The allowlist moved to ./l0-allowlist (this module was over the file-size limit); re-exported here so
|
|
21
22
|
// every existing `from './shim'` import keeps working and there is still ONE name to import L0 by.
|
|
22
23
|
tslib_1.__exportStar(require("./l0-allowlist"), exports);
|
|
@@ -50,7 +51,7 @@ function shimPath(projectRoot) {
|
|
|
50
51
|
// HOW EVERY DENY MUST SPELL ITS CURE (added 2026-07-23, from a live audit-log post-mortem).
|
|
51
52
|
//
|
|
52
53
|
// The guards were right, the message was right, and the assistant STILL handed the block back to the
|
|
53
|
-
// human — because of one appended clause. From .webpieces/logs
|
|
54
|
+
// human — because of one appended clause. From .webpieces/logs/<stream>ai-hook-shim.log in a consumer repo:
|
|
54
55
|
//
|
|
55
56
|
// DENY-SHIM-STALE cp node_modules/@webpieces/ai-hook-rules/templates/ai-hook.sh .claude/webpieces/ai-hook.sh && git status --short
|
|
56
57
|
//
|
|
@@ -78,6 +79,35 @@ exports.NO_CHAINING_RULE = 'Run it EXACTLY as written - the allowlist matches th
|
|
|
78
79
|
'(even && git status) makes it a different command and it is rejected; that is not the guard ' +
|
|
79
80
|
'blocking its own cure. Only these may be added: a leading cd <dir> && (single-quote a path ' +
|
|
80
81
|
'containing spaces), a trailing 2>&1, and | tail -N.';
|
|
82
|
+
// Shell fragment: resolve the guard BIN by WALKING UP from ROOT, and remember WHERE it came from.
|
|
83
|
+
//
|
|
84
|
+
// THE BUG THIS CLOSES (it would have landed the day the hooks went relative). `ai-hook.sh` used to set
|
|
85
|
+
// `BIN="$ROOT/node_modules/.bin/$BIN_NAME"` — a LITERAL path with no upward walk, while Node's own
|
|
86
|
+
// resolver walks up. That was correct only while the hooks were registered ABSOLUTE, because then ROOT
|
|
87
|
+
// was always the primary clone and the bin was always there. The moment H2/H3 became relative, ROOT
|
|
88
|
+
// became the tree the call is in — and a nested worktree at `<primary>/.claude/worktrees/<name>` has NO
|
|
89
|
+
// node_modules of its own. Every subagent would have hard-blocked on fault X at its first tool call,
|
|
90
|
+
// fleet-wide, on the day of the flip. Walking up finds the primary's install, exactly as a `require()`
|
|
91
|
+
// from the same directory would; a SIBLING worktree finds nothing and correctly still faults X.
|
|
92
|
+
//
|
|
93
|
+
// BIN_ROOT is not a curiosity: walking up ALONE re-creates the version straddle documented above
|
|
94
|
+
// committedShimStale(), where the shim of one tree is paired with the binary of another and the cure
|
|
95
|
+
// can never converge. So the walk is paired with a check — DECLARED comes from `$ROOT/package.json`
|
|
96
|
+
// (the tree being judged) and INSTALLED comes from `$BIN_ROOT/node_modules` (the binary actually
|
|
97
|
+
// running). Equal → no fault, keep reusing the inherited bin, which is the common case and stays free.
|
|
98
|
+
// Different → fault D, cured by an install in THIS tree, which materialises its own node_modules.
|
|
99
|
+
const RESOLVE_BIN_SH = `BIN_ROOT="\$ROOT"
|
|
100
|
+
BIN="\$ROOT/node_modules/.bin/\$BIN_NAME"
|
|
101
|
+
WP_WALK="\$ROOT"
|
|
102
|
+
while [ ! -x "\$WP_WALK/node_modules/.bin/\$BIN_NAME" ]; do
|
|
103
|
+
WP_UP="\$(dirname -- "\$WP_WALK")"
|
|
104
|
+
[ "\$WP_UP" != "\$WP_WALK" ] || break
|
|
105
|
+
WP_WALK="\$WP_UP"
|
|
106
|
+
done
|
|
107
|
+
if [ -x "\$WP_WALK/node_modules/.bin/\$BIN_NAME" ]; then
|
|
108
|
+
BIN_ROOT="\$WP_WALK"
|
|
109
|
+
BIN="\$WP_WALK/node_modules/.bin/\$BIN_NAME"
|
|
110
|
+
fi`;
|
|
81
111
|
// Normal template literal (not String.raw): it carries #235's shell escapes verbatim (\${BIN_NAME},
|
|
82
112
|
// \$REASON, \\n for the deny JSON) AND my sed backslashes (doubled: \\(, \\), \\1, [^"\\\\]). The
|
|
83
113
|
// grep pattern is interpolated from INSTALLER_ALLOW_ERE (its value has no backslashes).
|
|
@@ -146,7 +176,7 @@ if [ -f "$ROOT/package.json" ]; then
|
|
|
146
176
|
esac
|
|
147
177
|
# The release the rest of this repo is on — what fault U's cure should pin to.
|
|
148
178
|
[ -n "$WP_PIN" ] || WP_PIN="$WP_DECL"
|
|
149
|
-
WP_MANIFEST="$
|
|
179
|
+
WP_MANIFEST="$BIN_ROOT/node_modules/@webpieces/$WP_NAME/package.json"
|
|
150
180
|
[ -f "$WP_MANIFEST" ] || continue
|
|
151
181
|
WP_INST="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' "$WP_MANIFEST" | head -n1)"
|
|
152
182
|
[ -n "$WP_INST" ] || continue
|
|
@@ -159,6 +189,16 @@ if [ -f "$ROOT/package.json" ]; then
|
|
|
159
189
|
done <<WPEOF
|
|
160
190
|
$(sed -n 's/.*"@webpieces\\/\\([A-Za-z0-9._-]*\\)"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1 \\2/p' "$ROOT/package.json")
|
|
161
191
|
WPEOF
|
|
192
|
+
fi
|
|
193
|
+
# THE CURE FOR A BORROWED node_modules RUNS IN THIS TREE, NOT WHEREVER THE BIN CAME FROM. A bare
|
|
194
|
+
# 'pnpm install' typed while the shell sits in the primary clone installs into the primary, changes
|
|
195
|
+
# nothing in the worktree being judged, and re-fires the identical fault — the four-cure straddle
|
|
196
|
+
# recorded above committedShimStale(). When the bin was inherited, prescribe the cd and say why.
|
|
197
|
+
WP_INSTALL_CMD="pnpm install"
|
|
198
|
+
WP_BORROW_NOTE=""
|
|
199
|
+
if [ "$BIN_ROOT" != "$ROOT" ]; then
|
|
200
|
+
WP_INSTALL_CMD="cd $ROOT && pnpm install"
|
|
201
|
+
WP_BORROW_NOTE=" NOTE: this tree ($ROOT) has NO node_modules of its own, so the guard binary was inherited from $BIN_ROOT by walking up - which is only correct while the two agree on the version. Run the install HERE, in this tree, so it gets its own node_modules at its own pin."
|
|
162
202
|
fi`;
|
|
163
203
|
// Shell fragment: run the installed guard bin and INSPECT its outcome, instead of exec'ing it.
|
|
164
204
|
//
|
|
@@ -215,8 +255,27 @@ fi`;
|
|
|
215
255
|
// It deliberately does NOT change what the drift guard MEASURES — that stays anchored to $ROOT, the
|
|
216
256
|
// tree the shim FILE lives in. Where a call is logged and what a call is judged against are separate
|
|
217
257
|
// questions and are kept separate here.
|
|
258
|
+
// TWO command variables, and the split is a SECURITY boundary — do not collapse them.
|
|
259
|
+
//
|
|
260
|
+
// $CMD is the DECISION input (the L0 allowlist greps it). Its pattern requires the CLOSING quote, so a
|
|
261
|
+
// JSON payload that escapes an embedded quote as \\" yields the EMPTY STRING for the whole command.
|
|
262
|
+
// That looks like a bug and is in fact the safe direction: an empty command matches no allowlist entry,
|
|
263
|
+
// so a quoted command falls through to the deny. FAIL CLOSED. Keep it that way.
|
|
264
|
+
//
|
|
265
|
+
// $CMD_LOG is the AUDIT input and must never reach a decision. It drops the closing quote from the
|
|
266
|
+
// pattern so it captures the command PREFIX instead of nothing.
|
|
267
|
+
//
|
|
268
|
+
// WHY THEY CANNOT BE ONE VARIABLE: every L0 allowlist ERE is anchored `^…[[:space:]]*$`, and trailing
|
|
269
|
+
// whitespace is tolerated — so `pnpm install "; rm -rf /"` would prefix-capture to `pnpm install `,
|
|
270
|
+
// which MATCHES, and the injection after the quote would ride through allowlisted. Measured 2026-08-06:
|
|
271
|
+
// 3,908 of 4,917 shim audit lines (79.5%) recorded an empty command, i.e. four out of five audit
|
|
272
|
+
// entries were blind. Fixing the LOG is worth doing; fixing the DECISION the same way is a hole.
|
|
218
273
|
const PARSE_PAYLOAD_SH = `CMD="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
|
|
274
|
+
CMD_LOG="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\).*/\\1/p')"
|
|
275
|
+
[ -n "\$CMD_LOG" ] || CMD_LOG="\$CMD"
|
|
219
276
|
TOOL="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
|
|
277
|
+
WP_SID="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
|
|
278
|
+
WP_AID="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"agent_id"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
|
|
220
279
|
FILE="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
|
|
221
280
|
WP_CWD="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
|
|
222
281
|
[ -n "\$WP_CWD" ] || WP_CWD="\$ROOT" # no cwd in the payload (older client, or a hand-run) → the shim's own tree`;
|
|
@@ -285,7 +344,7 @@ const DENY_REASON_SH = `if [ -n "\$BROKEN_BIN" ]; then
|
|
|
285
344
|
# Report (do NOT auto-clean) the orphaned pnpm staging dirs — a package pnpm was mid-way through
|
|
286
345
|
# writing is left behind as <name>_<pid>_<hash>. Their presence is the fingerprint of an install that
|
|
287
346
|
# was killed, which is what corrupts node_modules in the first place. Best-effort; never fatal.
|
|
288
|
-
STAGING_N="\$(ls "\$
|
|
347
|
+
STAGING_N="\$(ls "\$BIN_ROOT/node_modules" 2>/dev/null | grep -Ec '_[0-9a-f]+_[0-9a-f]+\$' || true)"
|
|
289
348
|
STAGING_NOTE=""
|
|
290
349
|
if [ "\${STAGING_N:-0}" -gt 0 ] 2>/dev/null; then
|
|
291
350
|
STAGING_NOTE=" Also found \$STAGING_N orphaned pnpm staging dirs (name_pid_hash) under node_modules - the fingerprint of an install that was killed mid-write." # only when N > 0
|
|
@@ -325,13 +384,13 @@ elif [ -n "\$DRIFT_PKG" ]; then
|
|
|
325
384
|
if (ip == "" && dp != "") print "newer"
|
|
326
385
|
}' 2>/dev/null)"
|
|
327
386
|
if [ "\$DRIFT_DIR" = older ]; then
|
|
328
|
-
REASON="❌ webpieces version drift: package.json pins \$DRIFT_PKG@\$DRIFT_DECLARED but node_modules has \$DRIFT_INSTALLED - node_modules is OLDER, so the pin is what you want. Every other call is blocked until they agree. Run EXACTLY: '
|
|
387
|
+
REASON="❌ webpieces version drift: package.json pins \$DRIFT_PKG@\$DRIFT_DECLARED but node_modules has \$DRIFT_INSTALLED - node_modules is OLDER, so the pin is what you want. Every other call is blocked until they agree. Run EXACTLY: '\$WP_INSTALL_CMD'.\${WP_BORROW_NOTE} ${exports.NO_CHAINING_RULE}"
|
|
329
388
|
else
|
|
330
389
|
# NEWER, or undecidable — the same three choices apply either way, so the only thing the ambiguous
|
|
331
390
|
# case changes is the claim about which side is stale.
|
|
332
391
|
DRIFT_NOTE="node_modules is NEWER, so the PIN is the stale side and a bare 'pnpm install' DOWNGRADES you to \$DRIFT_DECLARED"
|
|
333
392
|
[ "\$DRIFT_DIR" = newer ] || DRIFT_NOTE="these two versions could not be ordered automatically - compare them yourself: if node_modules is the NEWER side then the PIN is the stale side and a bare 'pnpm install' DOWNGRADES you to \$DRIFT_DECLARED"
|
|
334
|
-
REASON="❌ webpieces version drift: package.json pins \$DRIFT_PKG@\$DRIFT_DECLARED but node_modules has \$DRIFT_INSTALLED - \$DRIFT_NOTE. That may be exactly what you want. Every other call is blocked until they agree. Pick one: - move forward to what origin pins: run 'git pull origin main', then 'pnpm install'. - stay on this code deliberately: run 'pnpm install' (the downgrade is the point). - on a feature branch: run 'pnpm install' (aligns to YOUR branch pin - usually right)
|
|
393
|
+
REASON="❌ webpieces version drift: package.json pins \$DRIFT_PKG@\$DRIFT_DECLARED but node_modules has \$DRIFT_INSTALLED - \$DRIFT_NOTE. That may be exactly what you want. Every other call is blocked until they agree. Pick one: - move forward to what origin pins: run 'git pull origin main', then 'pnpm install'. - stay on this code deliberately: run 'pnpm install' (the downgrade is the point). - on a feature branch: run 'pnpm install' (aligns to YOUR branch pin - usually right).\${WP_BORROW_NOTE} ${exports.NO_CHAINING_RULE}"
|
|
335
394
|
fi
|
|
336
395
|
else
|
|
337
396
|
# A LINKED WORKTREE is the overwhelmingly common way to land here with a perfectly healthy repo:
|
|
@@ -367,20 +426,24 @@ function renderShim() {
|
|
|
367
426
|
# the hook has a stable entry point even when node_modules is absent. Safe to delete along with the
|
|
368
427
|
# matching .claude/settings.json entries if you remove @webpieces/ai-hook-rules.
|
|
369
428
|
#
|
|
370
|
-
# Usage (wired into .claude/settings.json
|
|
429
|
+
# Usage (wired into .claude/settings.json, RELATIVE so each git tree runs its own copy):
|
|
430
|
+
# sh ".claude/webpieces/ai-hook.sh" <bin-name>
|
|
371
431
|
BIN_NAME="$1"
|
|
372
432
|
shift
|
|
373
|
-
# Resolve the
|
|
433
|
+
# Resolve the tree relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the
|
|
374
434
|
# caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).
|
|
375
435
|
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
|
376
|
-
BIN
|
|
436
|
+
# The BIN is resolved by walking UP from ROOT (as Node does), and BIN_ROOT records which tree supplied
|
|
437
|
+
# it — the version-drift guard below compares THIS tree's pin against THAT tree's installed version.
|
|
438
|
+
${RESOLVE_BIN_SH}
|
|
377
439
|
${VERSION_DRIFT_GUARD_SH}
|
|
378
440
|
# Read the tool payload ONCE, up front. The shim no longer exec's the bin (see RUN_BIN_SH), so it must
|
|
379
441
|
# forward stdin to the bin itself — and it needs the payload again on the fail-closed path below.
|
|
380
442
|
PAYLOAD="$(cat)"
|
|
381
443
|
${PARSE_PAYLOAD_SH}
|
|
382
444
|
# Best-effort AUDIT TRAIL of what L0 did with this call — every call, not just the broken ones. One
|
|
383
|
-
# tab-separated line per invocation into this TREE's own
|
|
445
|
+
# tab-separated line per invocation into this TREE's own
|
|
446
|
+
# logs/<session>-<agent|coordinator>-<binName>-ai-hook-shim.log (gitignored), so the
|
|
384
447
|
# observed behaviour can be diffed against the matrix in guards/L0-tooling.md. NEVER breaks or blocks the
|
|
385
448
|
# hook: every write is swallowed, and nothing ever goes to stdout (stdout is the PreToolUse decision
|
|
386
449
|
# channel — a stray byte there would corrupt allow/deny).
|
|
@@ -554,8 +617,16 @@ function isShimCureCommand(command) {
|
|
|
554
617
|
const cmd = command.trim();
|
|
555
618
|
return l0_allowlist_1.INSTALL_HOOKS_ALLOW_JS.test(cmd) || l0_allowlist_1.UPGRADE_SHIM_ALLOW_JS.test(cmd) || l0_allowlist_1.RESTORE_SHIM_ALLOW_JS.test(cmd);
|
|
556
619
|
}
|
|
557
|
-
// The fail-closed deny text for a
|
|
558
|
-
// NO_CHAINING_RULE.
|
|
620
|
+
// The fail-closed deny text for a drifted MANAGED HOOK SURFACE, built from the single-source cure
|
|
621
|
+
// constants + NO_CHAINING_RULE.
|
|
622
|
+
//
|
|
623
|
+
// `drifted` names WHICH of the three managed things moved — .claude/webpieces/ai-hook.sh,
|
|
624
|
+
// .claude/webpieces/guarantee-root.sh, and the .claude/settings.json hook registration (see
|
|
625
|
+
// hook-registration.ts). It is REQUIRED, not optional: this used to be a shim-only message, and an
|
|
626
|
+
// optional list would let a caller silently keep emitting the one-file text after the surface grew to
|
|
627
|
+
// three — which is the "two spellings of one thing" shape the compatibility policy rejects.
|
|
628
|
+
//
|
|
629
|
+
// `installedVersion` names WHICH webpieces the cure re-arms to (the binary is that
|
|
559
630
|
// version); pass '' to omit the note rather than print an empty one. `root` is the tree the deciding
|
|
560
631
|
// binary GOVERNS (governingShimRoot) — naming it, and anchoring the cure to it with a leading
|
|
561
632
|
// `cd <root> &&` (which CD_PREFIX_*_ANCHORED already tolerates, locked by a unit test), is what keeps
|
|
@@ -578,8 +649,9 @@ function isShimCureCommand(command) {
|
|
|
578
649
|
// Windows-style path or an odd directory name must not be able to corrupt the decision. Locked by unit
|
|
579
650
|
// tests. An unusual root is also dropped from the `cd` cure rather than quoted (CD_PREFIX would reject it).
|
|
580
651
|
// webpieces-disable no-function-outside-class -- pure string builder over exported constants; the single source of the self-guard deny text now that the sh copy is gone.
|
|
581
|
-
function shimStaleDenyReason(installedVersion, root
|
|
652
|
+
function shimStaleDenyReason(installedVersion, root, drifted) {
|
|
582
653
|
const verNote = installedVersion ? ` (installed version ${installedVersion})` : '';
|
|
654
|
+
const what = drifted.join(', ');
|
|
583
655
|
const safeRoot = root.replace(/["\\]/g, '');
|
|
584
656
|
const projectDir = rules_config_1.claudeEnv.projectDirForLog().replace(/["\\]/g, '');
|
|
585
657
|
// Tested against the RAW root, never the stripped one: stripping is a display-safety measure, and
|
|
@@ -595,7 +667,7 @@ function shimStaleDenyReason(installedVersion, root = '') {
|
|
|
595
667
|
const upgrade = cdOk ? `cd ${safeRoot} && ${l0_allowlist_1.UPGRADE_SHIM_CMD}` : l0_allowlist_1.UPGRADE_SHIM_CMD;
|
|
596
668
|
// OPTION 2 is a relative-path `cp`, so it is even MORE cwd-sensitive than OPTION 1 — anchor it too.
|
|
597
669
|
const restore = cdOk ? `cd ${safeRoot} && ${l0_allowlist_1.RESTORE_SHIM_CMD}` : l0_allowlist_1.RESTORE_SHIM_CMD;
|
|
598
|
-
return `❌ webpieces-managed
|
|
670
|
+
return `❌ webpieces-managed hook surface was changed: ${what} no longer matches what the INSTALLED @webpieces/ai-hook-rules${verNote} expects (reverted, hand-edited, or predating this binary - a settings.json still on the OLD two-absolute-hook form reports here too).${rootNote} webpieces manages THREE things together and they only work as a set: ${exports.SHIM_MARKER} (the guard shim, registered RELATIVE so each git tree runs its own release), ${guarantee_root_1.GUARANTEE_ROOT_MARKER} (the L-1 hook, registered ABSOLUTE, which refuses any cd that would park the shell where the relative hooks cannot launch - without it an unresolvable hook is a SILENT UNGUARDED ALLOW), and the .claude/settings.json entries that register them. They are GENERATED and committed by webpieces - they must NOT be reverted or edited by hand, and the fail-closed logic cannot be trusted while any of them differs. Every OTHER tool call is blocked until all three match again. THIS IS NOT A DEADLOCK: both options below are explicitly ALLOWED through while this guard is up, so run one YOURSELF now - do not hand it back to the human. OPTION 1 (preferred, and the ONLY option that repairs all three - it regenerates both .sh files AND rewrites the settings.json registration to the three-hook form, removing the old absolute entries; it touches no config, and it imports only fs/path so it runs on a broken tree; needs installed @webpieces/ai-hook-rules 0.4.408 or newer) - run EXACTLY this command: '${upgrade}'. OPTION 2 (a PARTIAL fallback - it repairs ONE of the three, ${exports.SHIM_MARKER}, and nothing else; pick it only when the installed @webpieces/ai-hook-rules is OLDER than 0.4.408 so wp-upgrade-shim does not exist yet, then upgrade @webpieces and run OPTION 1 to finish the job. Claude Code's own permission prompt may ask you to confirm the file overwrite, and that prompt is NOT this guard) - run EXACTLY this command: '${restore}'. Do NOT use the bare '${l0_allowlist_1.INSTALL_HOOKS_CMD}' here: it also migrates your config and PROMPTS for a hook target twice, which hangs a non-interactive session. ${exports.NO_CHAINING_RULE} Do NOT revert these files again - if you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json instead.`;
|
|
599
671
|
}
|
|
600
672
|
// The installed @webpieces/ai-hook-rules version, for shimStaleDenyReason's note. The binary IS this
|
|
601
673
|
// package, so it reads its OWN package.json (two dirs up from src/bin). Best-effort: '' on any failure,
|