@webpieces/ai-hook-rules 0.4.539 → 0.4.541
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/bin/setup.d.ts +1 -1
- package/src/bin/setup.js +29 -14
- package/src/bin/setup.js.map +1 -1
- package/src/core/rules/stale-main-bash-guard.d.ts +53 -1
- package/src/core/rules/stale-main-bash-guard.js +98 -6
- package/src/core/rules/stale-main-bash-guard.js.map +1 -1
- package/src/core/runner.js +22 -18
- package/src/core/runner.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/ai-hook-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.541",
|
|
4
4
|
"description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"directory": "packages/tooling/ai-hook-rules"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@webpieces/rules-config": "0.4.
|
|
35
|
+
"@webpieces/rules-config": "0.4.541"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
package/src/bin/setup.d.ts
CHANGED
package/src/bin/setup.js
CHANGED
|
@@ -132,10 +132,31 @@ function seedCommands() {
|
|
|
132
132
|
mergeComplete: DEFAULT_MERGE_COMPLETE,
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
|
-
// Required excludePaths block:
|
|
136
|
-
//
|
|
135
|
+
// Required excludePaths block: ONE glob list suppressing hook enforcement per file path. Seeded empty
|
|
136
|
+
// (enforce everywhere) — a client adds paths (e.g. "repositories/**") to exempt vendored trees.
|
|
137
|
+
// webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design
|
|
137
138
|
function seedExcludePaths() {
|
|
138
|
-
return
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
// Bring an existing `excludePaths` forward to the single-list shape. Already a list → untouched.
|
|
142
|
+
// Legacy `{ rules, guards }` → unioned (order preserved, duplicates dropped) and recorded as a change
|
|
143
|
+
// so `wp-install-ai-hooks` is the migration path rather than a hand-edit. Anything else → seeded [].
|
|
144
|
+
// webpieces-disable no-any-unknown -- `raw` is opaque consumer JSON until narrowed here
|
|
145
|
+
// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design
|
|
146
|
+
function migrateExcludePaths(raw, changes) {
|
|
147
|
+
if (Array.isArray(raw))
|
|
148
|
+
return raw.filter(p => typeof p === 'string');
|
|
149
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
150
|
+
// webpieces-disable no-any-unknown -- narrowing the opaque legacy block from consumer JSON
|
|
151
|
+
const legacy = raw;
|
|
152
|
+
const rules = Array.isArray(legacy['rules']) ? legacy['rules'] : [];
|
|
153
|
+
const guards = Array.isArray(legacy['guards']) ? legacy['guards'] : [];
|
|
154
|
+
const merged = [...new Set([...rules, ...guards].filter(p => typeof p === 'string'))];
|
|
155
|
+
changes.push(`migrated excludePaths {rules,guards} -> one list (${merged.length} path(s))`);
|
|
156
|
+
return merged;
|
|
157
|
+
}
|
|
158
|
+
changes.push('added excludePaths ([])');
|
|
159
|
+
return [];
|
|
139
160
|
}
|
|
140
161
|
// Deep-copy the framework's default match-rules (the no-fetch guard) into plain JSON for the config
|
|
141
162
|
// file. Round-tripping through JSON turns the MatchRuleConfig instances into plain objects.
|
|
@@ -226,17 +247,11 @@ function migrate(existing) {
|
|
|
226
247
|
commands['mergeComplete'] = DEFAULT_MERGE_COMPLETE;
|
|
227
248
|
changes.push('added commands.mergeComplete');
|
|
228
249
|
}
|
|
229
|
-
// Seed the now-required excludePaths
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
changes.push('added excludePaths.rules ([])');
|
|
235
|
-
}
|
|
236
|
-
if (excludePaths['guards'] === undefined) {
|
|
237
|
-
excludePaths['guards'] = [];
|
|
238
|
-
changes.push('added excludePaths.guards ([])');
|
|
239
|
-
}
|
|
250
|
+
// Seed the now-required excludePaths list (empty = enforce everywhere) if the config predates it,
|
|
251
|
+
// and MIGRATE the legacy `{ rules: [], guards: [] }` object to the single list by unioning them.
|
|
252
|
+
// The union is behaviour-preserving for every config we have seen (both lists set identically), and
|
|
253
|
+
// widening is the safe direction anyway: a path either side excluded stays excluded.
|
|
254
|
+
const excludePaths = migrateExcludePaths(existing['excludePaths'], changes);
|
|
240
255
|
// Seed the now-required match-rules array (with the default no-fetch guard) if the config predates
|
|
241
256
|
// it. A client that has already customized it keeps their array untouched.
|
|
242
257
|
let matchRules;
|
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, sectionForRule, isHookGuard, DEFAULT_MATCH_RULES, 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: Json;\n 'match-rules': Json[];\n rulesDir: string[];\n}\n\ninterface MigrateResult {\n config: ConfigFile;\n changes: string[];\n}\n\nfunction seedRule(): 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 return { mode: 'OFF', turnOffRuleUntilEpoch: 0, turnOffRuleWhileOnBranch: null };\n}\n\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n upsertPr: DEFAULT_UPSERT_PR,\n mergeComplete: DEFAULT_MERGE_COMPLETE,\n };\n}\n\n// Required excludePaths block: two glob lists that suppress hook enforcement per file path. Seeded\n// empty (enforce everywhere) — a client adds paths (e.g. \"repositories/**\") to exempt vendored trees.\nfunction seedExcludePaths(): Json {\n return { rules: [], guards: [] };\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();\n else rules[name] = seedRule();\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 // 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 (OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n target[name] = seedRule();\n changes.push(`added \"${name}\" (OFF) 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 if (commands['upsertPr'] === undefined) { commands['upsertPr'] = DEFAULT_UPSERT_PR; changes.push('added commands.upsertPr'); }\n if (commands['mergeComplete'] === undefined) { commands['mergeComplete'] = DEFAULT_MERGE_COMPLETE; changes.push('added commands.mergeComplete'); }\n\n // Seed the now-required excludePaths block (empty = enforce everywhere) if the config predates it.\n const excludePaths: Json = (typeof existing['excludePaths'] === 'object' && existing['excludePaths'] !== null && !Array.isArray(existing['excludePaths']))\n ? (existing['excludePaths'] as Json) : {};\n if (excludePaths['rules'] === undefined) { excludePaths['rules'] = []; changes.push('added excludePaths.rules ([])'); }\n if (excludePaths['guards'] === undefined) { excludePaths['guards'] = []; changes.push('added excludePaths.guards ([])'); }\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\nfunction seedOrSyncConfig(projectRoot: string, syncOnly: boolean): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n if (syncOnly) {\n console.log(` [ai-hooks] No ${CONFIG_FILENAME} found — nothing to sync.`);\n return;\n }\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands), all rules OFF.`);\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 const syncOnly = args.includes('--sync');\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, syncOnly);\n if (syncOnly) return;\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, sectionForRule, isHookGuard, DEFAULT_MATCH_RULES, 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\nfunction seedRule(): 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 return { mode: 'OFF', turnOffRuleUntilEpoch: 0, turnOffRuleWhileOnBranch: null };\n}\n\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n upsertPr: DEFAULT_UPSERT_PR,\n mergeComplete: 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// 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();\n else rules[name] = seedRule();\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 // 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 (OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n target[name] = seedRule();\n changes.push(`added \"${name}\" (OFF) 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 if (commands['upsertPr'] === undefined) { commands['upsertPr'] = DEFAULT_UPSERT_PR; changes.push('added commands.upsertPr'); }\n if (commands['mergeComplete'] === undefined) { commands['mergeComplete'] = DEFAULT_MERGE_COMPLETE; changes.push('added commands.mergeComplete'); }\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\nfunction seedOrSyncConfig(projectRoot: string, syncOnly: boolean): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n if (syncOnly) {\n console.log(` [ai-hooks] No ${CONFIG_FILENAME} found — nothing to sync.`);\n return;\n }\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands), all rules OFF.`);\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 const syncOnly = args.includes('--sync');\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, syncOnly);\n if (syncOnly) return;\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,44 @@ import type { BashContext, Violation } from '../types';
|
|
|
3
3
|
import { BashRuleBase } from '../rule-base';
|
|
4
4
|
import { FixHint } from '../fix-hint';
|
|
5
5
|
/**
|
|
6
|
-
* The BASH half of the STALE-MAIN protection (read-stale-guard's State A)
|
|
6
|
+
* The BASH half of the STALE-MAIN protection (read-stale-guard's State A), in two halves of its own:
|
|
7
|
+
* a PREVENTIVE check that stops a session landing on a stale `main`, and the REACTIVE check that
|
|
8
|
+
* contains the damage once it is already there.
|
|
9
|
+
*
|
|
10
|
+
* ── PREVENTIVE: a bare `git checkout main` is blocked; the pull must ride along ──────────────────
|
|
11
|
+
*
|
|
12
|
+
* Everything below this paragraph fires only once the session is ALREADY sitting on a stale `main`.
|
|
13
|
+
* Nothing stopped it ARRIVING there, and arriving is one keystroke. In the incident that added this
|
|
14
|
+
* half, an agent ran `git checkout main` after a merge, in a clone whose local `main` was **157
|
|
15
|
+
* commits behind** origin. That checkout did not merely produce stale files — it reverted:
|
|
16
|
+
*
|
|
17
|
+
* 1. `package.json`'s `@webpieces` pin, to a version OLDER than the installed `node_modules`;
|
|
18
|
+
* 2. `.claude/webpieces/ai-hook.sh` — the version-drift guard ITSELF — to a 157-commit-old copy
|
|
19
|
+
* whose message stated the drift BACKWARDS ("your installed webpieces is older than required")
|
|
20
|
+
* and named a single cure, `pnpm install`;
|
|
21
|
+
* 3. and so the agent's judgment: it ran that `pnpm install`, DOWNGRADING `node_modules` to match
|
|
22
|
+
* the stale pin, and had to undo it with the `git pull` that should have come first.
|
|
23
|
+
*
|
|
24
|
+
* The shim on current main already diagnoses drift correctly — it distinguishes "the pin is newer"
|
|
25
|
+
* from "the pin is stale, and `pnpm install` would downgrade you". None of that helped, because the
|
|
26
|
+
* checkout had replaced the shim with the version that could not say it. **A guard a stale checkout
|
|
27
|
+
* can revert cannot be relied on to catch a stale checkout**, which is why this check is preventive
|
|
28
|
+
* and why it lives here rather than in a second rule: same failure, one step earlier, one switch.
|
|
29
|
+
*
|
|
30
|
+
* It matches on command TEXT alone and asks git nothing. That is not laziness — this runs BEFORE the
|
|
31
|
+
* checkout, so the only `main` it could measure is the one it is about to leave. The interesting
|
|
32
|
+
* `main` does not exist yet, and consulting HEAD-at-hook-time is the exact trap
|
|
33
|
+
* `redirect-how-to-merge-main` documents at length. Pairing is unconditionally correct instead: when
|
|
34
|
+
* `main` is already current the chained pull is a sub-second no-op, so no exception is worth carving.
|
|
35
|
+
*
|
|
36
|
+
* BLOCKED `git checkout main`, `git switch main` — with or without flags — when no `git pull`
|
|
37
|
+
* appears anywhere in the SAME command.
|
|
38
|
+
* ALLOWED `git checkout main && git pull origin main`, the pairing this forces, which is the
|
|
39
|
+
* exact line the post-merge cleanup flow already prescribes.
|
|
40
|
+
* ALLOWED `git checkout -b <x> origin/main` (current by construction), `git checkout <sha>`,
|
|
41
|
+
* `git checkout -- <file>`, and any other branch.
|
|
42
|
+
*
|
|
43
|
+
* ── REACTIVE: content-reading Bash on a stale `main` ─────────────────────────────────────────────
|
|
7
44
|
*
|
|
8
45
|
* read-stale-guard blocks the Read tool when local `main` is behind origin/main — but it looks at
|
|
9
46
|
* nothing else, deliberately: "every cure is a Bash command, so Bash is the escape hatch — never
|
|
@@ -35,12 +72,27 @@ import { FixHint } from '../fix-hint';
|
|
|
35
72
|
export declare class StaleMainBashGuardRule extends BashRuleBase<StaleMainBashGuardConfig> {
|
|
36
73
|
constructor(config: StaleMainBashGuardConfig);
|
|
37
74
|
private readonly scanner;
|
|
75
|
+
private readonly recovery;
|
|
38
76
|
readonly description: string;
|
|
39
77
|
readonly defaultOptions: {
|
|
40
78
|
hangTimeoutMinutes: number;
|
|
41
79
|
};
|
|
42
80
|
readonly fixHint: FixHint;
|
|
43
81
|
check(ctx: BashContext): readonly Violation[];
|
|
82
|
+
/**
|
|
83
|
+
* The first segment that switches to the `main` BRANCH with no `git pull` anywhere in the same
|
|
84
|
+
* command, or null. The pull is looked for across the WHOLE command, not the matched segment,
|
|
85
|
+
* because `git checkout main && git pull origin main` splits into two segments and the pairing is
|
|
86
|
+
* the point.
|
|
87
|
+
*/
|
|
88
|
+
private bareCheckoutOfMain;
|
|
89
|
+
/**
|
|
90
|
+
* True only for landing ON the branch. `-b`/`-B`/`-c`/`-C` CREATE a branch, so
|
|
91
|
+
* `git checkout -b x origin/main` is current by construction and never blocked; a `--` turns the
|
|
92
|
+
* rest into pathspecs, so `git checkout -- main` restores a FILE named main and moves no branch.
|
|
93
|
+
*/
|
|
94
|
+
private switchesToMainBranch;
|
|
95
|
+
private pairingMessage;
|
|
44
96
|
private staleContentRead;
|
|
45
97
|
private contains;
|
|
46
98
|
private isDirty;
|
|
@@ -12,8 +12,46 @@ const decision_log_1 = require("../decision-log");
|
|
|
12
12
|
const command_scan_1 = require("../command-scan");
|
|
13
13
|
const stale_main_message_1 = require("./stale-main-message");
|
|
14
14
|
const content_read_scan_1 = require("./content-read-scan");
|
|
15
|
+
const tree_recovery_1 = require("./tree-recovery");
|
|
15
16
|
/**
|
|
16
|
-
* The BASH half of the STALE-MAIN protection (read-stale-guard's State A)
|
|
17
|
+
* The BASH half of the STALE-MAIN protection (read-stale-guard's State A), in two halves of its own:
|
|
18
|
+
* a PREVENTIVE check that stops a session landing on a stale `main`, and the REACTIVE check that
|
|
19
|
+
* contains the damage once it is already there.
|
|
20
|
+
*
|
|
21
|
+
* ── PREVENTIVE: a bare `git checkout main` is blocked; the pull must ride along ──────────────────
|
|
22
|
+
*
|
|
23
|
+
* Everything below this paragraph fires only once the session is ALREADY sitting on a stale `main`.
|
|
24
|
+
* Nothing stopped it ARRIVING there, and arriving is one keystroke. In the incident that added this
|
|
25
|
+
* half, an agent ran `git checkout main` after a merge, in a clone whose local `main` was **157
|
|
26
|
+
* commits behind** origin. That checkout did not merely produce stale files — it reverted:
|
|
27
|
+
*
|
|
28
|
+
* 1. `package.json`'s `@webpieces` pin, to a version OLDER than the installed `node_modules`;
|
|
29
|
+
* 2. `.claude/webpieces/ai-hook.sh` — the version-drift guard ITSELF — to a 157-commit-old copy
|
|
30
|
+
* whose message stated the drift BACKWARDS ("your installed webpieces is older than required")
|
|
31
|
+
* and named a single cure, `pnpm install`;
|
|
32
|
+
* 3. and so the agent's judgment: it ran that `pnpm install`, DOWNGRADING `node_modules` to match
|
|
33
|
+
* the stale pin, and had to undo it with the `git pull` that should have come first.
|
|
34
|
+
*
|
|
35
|
+
* The shim on current main already diagnoses drift correctly — it distinguishes "the pin is newer"
|
|
36
|
+
* from "the pin is stale, and `pnpm install` would downgrade you". None of that helped, because the
|
|
37
|
+
* checkout had replaced the shim with the version that could not say it. **A guard a stale checkout
|
|
38
|
+
* can revert cannot be relied on to catch a stale checkout**, which is why this check is preventive
|
|
39
|
+
* and why it lives here rather than in a second rule: same failure, one step earlier, one switch.
|
|
40
|
+
*
|
|
41
|
+
* It matches on command TEXT alone and asks git nothing. That is not laziness — this runs BEFORE the
|
|
42
|
+
* checkout, so the only `main` it could measure is the one it is about to leave. The interesting
|
|
43
|
+
* `main` does not exist yet, and consulting HEAD-at-hook-time is the exact trap
|
|
44
|
+
* `redirect-how-to-merge-main` documents at length. Pairing is unconditionally correct instead: when
|
|
45
|
+
* `main` is already current the chained pull is a sub-second no-op, so no exception is worth carving.
|
|
46
|
+
*
|
|
47
|
+
* BLOCKED `git checkout main`, `git switch main` — with or without flags — when no `git pull`
|
|
48
|
+
* appears anywhere in the SAME command.
|
|
49
|
+
* ALLOWED `git checkout main && git pull origin main`, the pairing this forces, which is the
|
|
50
|
+
* exact line the post-merge cleanup flow already prescribes.
|
|
51
|
+
* ALLOWED `git checkout -b <x> origin/main` (current by construction), `git checkout <sha>`,
|
|
52
|
+
* `git checkout -- <file>`, and any other branch.
|
|
53
|
+
*
|
|
54
|
+
* ── REACTIVE: content-reading Bash on a stale `main` ─────────────────────────────────────────────
|
|
17
55
|
*
|
|
18
56
|
* read-stale-guard blocks the Read tool when local `main` is behind origin/main — but it looks at
|
|
19
57
|
* nothing else, deliberately: "every cure is a Bash command, so Bash is the escape hatch — never
|
|
@@ -45,17 +83,28 @@ const content_read_scan_1 = require("./content-read-scan");
|
|
|
45
83
|
class StaleMainBashGuardRule extends rule_base_1.BashRuleBase {
|
|
46
84
|
constructor(config) { super(config, 'stale-main-bash-guard'); }
|
|
47
85
|
scanner = new command_scan_1.CommandScanner();
|
|
48
|
-
|
|
49
|
-
|
|
86
|
+
recovery = new tree_recovery_1.TreeRecovery();
|
|
87
|
+
description = 'Block a bare `git checkout main` (chain the pull into the same command), and block ' +
|
|
88
|
+
'content-reading Bash (cat/grep/ls/…) while local main is behind origin/main — so a session ' +
|
|
89
|
+
'neither lands on a stale main nor reasons over one through the side door the Read block leaves.';
|
|
50
90
|
defaultOptions = {
|
|
51
91
|
hangTimeoutMinutes: rules_config_1.DEFAULT_HANG_TIMEOUT_MINUTES,
|
|
52
92
|
};
|
|
53
|
-
fixHint = new fix_hint_1.FixHint('
|
|
54
|
-
new fix_hint_1.Option('git
|
|
55
|
-
new fix_hint_1.Option('
|
|
93
|
+
fixHint = new fix_hint_1.FixHint('Landing on `main` without pulling, or reading files while main is behind origin/main, both give you stale content.', 'Pair the checkout with the pull, or update main and re-run:', [
|
|
94
|
+
new fix_hint_1.Option('git checkout main && git pull origin main (the pull must be in the SAME command).', true),
|
|
95
|
+
new fix_hint_1.Option('Already on main: git pull --ff-only origin main (then re-run). If that fatals with "Cannot fast-forward to multiple branches", .git/FETCH_HEAD has a duplicate line — run git fetch --prune origin main first.'),
|
|
96
|
+
new fix_hint_1.Option('In a linked worktree `git checkout main` FATALS ("main is already checked out at <primary clone>") — branch off fresh main instead: git fetch origin main && git checkout -b <name> origin/main'),
|
|
97
|
+
new fix_hint_1.Option('NOT blocked: `git checkout <sha>`, `git checkout -b <x> origin/main`, `git checkout -- <file>`, any other branch. Also still allowed: builds, tests, installs, the pull itself, all git/gh METADATA (status|log|diff|show|branch), every Write/Edit, and reading webpieces.config.json.'),
|
|
56
98
|
new fix_hint_1.Option('Disable in webpieces.config.json under hookGuards → stale-main-bash-guard (mode OFF) if intentional.'),
|
|
57
99
|
]);
|
|
58
100
|
check(ctx) {
|
|
101
|
+
// PREVENTIVE half, FIRST and unconditional. Deliberately ahead of every fail-open bailout
|
|
102
|
+
// below: those all ask "is the main we are ON stale?", and this asks about the main we are
|
|
103
|
+
// about to MOVE TO — a different branch, and one no cache can describe yet.
|
|
104
|
+
const bare = this.bareCheckoutOfMain(ctx);
|
|
105
|
+
if (bare !== null) {
|
|
106
|
+
return this.block(ctx, 'any', `bare checkout of main (${bare})`, this.pairingMessage(ctx), '-');
|
|
107
|
+
}
|
|
59
108
|
const branch = this.currentBranch(ctx.workspaceRoot);
|
|
60
109
|
if (branch === null)
|
|
61
110
|
return this.allow(ctx, branch, 'branch-undeterminable (fail-open)');
|
|
@@ -87,6 +136,49 @@ class StaleMainBashGuardRule extends rule_base_1.BashRuleBase {
|
|
|
87
136
|
return this.allow(ctx, branch, 'not-a-content-read (cure/build/metadata)', cache);
|
|
88
137
|
return this.block(ctx, branch, `stale-main content read (${reader})`, this.staleMessage(ctx.workspaceRoot), cache);
|
|
89
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* The first segment that switches to the `main` BRANCH with no `git pull` anywhere in the same
|
|
141
|
+
* command, or null. The pull is looked for across the WHOLE command, not the matched segment,
|
|
142
|
+
* because `git checkout main && git pull origin main` splits into two segments and the pairing is
|
|
143
|
+
* the point.
|
|
144
|
+
*/
|
|
145
|
+
bareCheckoutOfMain(ctx) {
|
|
146
|
+
for (const segment of this.scanner.commandSegments(ctx.command)) {
|
|
147
|
+
if (!this.scanner.invokesGit(segment, 'checkout') && !this.scanner.invokesGit(segment, 'switch'))
|
|
148
|
+
continue;
|
|
149
|
+
if (!this.switchesToMainBranch(segment))
|
|
150
|
+
continue;
|
|
151
|
+
return this.scanner.commandInvokesAnyGit(ctx.command, ['pull']) ? null : segment;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* True only for landing ON the branch. `-b`/`-B`/`-c`/`-C` CREATE a branch, so
|
|
157
|
+
* `git checkout -b x origin/main` is current by construction and never blocked; a `--` turns the
|
|
158
|
+
* rest into pathspecs, so `git checkout -- main` restores a FILE named main and moves no branch.
|
|
159
|
+
*/
|
|
160
|
+
switchesToMainBranch(segment) {
|
|
161
|
+
const words = this.scanner.words(segment);
|
|
162
|
+
for (let i = 0; i < words.length; i++) {
|
|
163
|
+
const word = words[i];
|
|
164
|
+
if (word === '--')
|
|
165
|
+
return false;
|
|
166
|
+
if (/^-[bBcC]$/.test(word))
|
|
167
|
+
return false;
|
|
168
|
+
if (i > 1 && word === 'main')
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
pairingMessage(ctx) {
|
|
174
|
+
const steps = this.recovery.updateMainSteps(this.recovery.kindOf(ctx.workspaceRoot)).join('\n');
|
|
175
|
+
return 'Blocked: a bare `git checkout main` lands you on whatever local `main` you last had. '
|
|
176
|
+
+ 'That is not only stale FILES — it also reverts `package.json`\'s @webpieces pin and the '
|
|
177
|
+
+ 'guard shim under `.claude/webpieces/`, so the very hook that would diagnose the resulting '
|
|
178
|
+
+ 'version drift is replaced by an older copy that reports it BACKWARDS and names the cure '
|
|
179
|
+
+ 'that makes it worse. Chain the pull into the same command, leaving no window in which you '
|
|
180
|
+
+ 'are on a stale main:\n' + steps;
|
|
181
|
+
}
|
|
90
182
|
// The first segment that would read stale workspace content, or null when none does. The RAW
|
|
91
183
|
// command is scanned, not commandCode: this is a blocklist-shaped guard, so stripping quoted
|
|
92
184
|
// prose can only ever block LESS (see BashContext.commandCode).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stale-main-bash-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/stale-main-bash-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\n\nimport {\n StaleMainBashGuardConfig,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n readMainSyncStatus,\n MainSyncStatus,\n} from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\nimport { toError } from '../to-error';\nimport { triggerMainSyncRefresh } from '../main-sync-refresh';\nimport { logGuardDecision, GuardDecision } from '../decision-log';\nimport { CommandScanner } from '../command-scan';\nimport { StaleMainMessage } from './stale-main-message';\nimport { ContentReadScan } from './content-read-scan';\n\n/**\n * The BASH half of the STALE-MAIN protection (read-stale-guard's State A).\n *\n * read-stale-guard blocks the Read tool when local `main` is behind origin/main — but it looks at\n * nothing else, deliberately: \"every cure is a Bash command, so Bash is the escape hatch — never\n * wedge it.\" That reasoning is right about the CURE and wrong about `cat`/`grep`/`ls`. In the\n * incident this closes, an agent sat on a `main` 18 commits behind origin/main (108 files, +8069\n * −3692 upstream), had its Read tool blocked exactly as designed, and then spent the whole session\n * `ls`-ing, `grep`-ing and `cat`-ing the same stale tree through the side door — describing a CI\n * workflow set that was missing a 186-line workflow which existed upstream. The logs read\n * \"read-stale-guard handled\", which is worse than no guard: it looks covered.\n *\n * So this guard blocks CONTENT-READING Bash only, never the whole shell. Builds, tests, installs,\n * `git pull`, git METADATA (log/diff/show/status) — all still run. What is blocked is a command that\n * would put stale FILE CONTENT into context: `cat`/`head`/`grep`/`rg`/`sed`/`awk`/`ls`/`find`/… of a\n * path inside this workspace, and `git grep` / `git show <rev>:<path>` against a local rev. The same\n * line merged-branch-bash-guard already draws for State B, scoped tighter because State A's cure is\n * one command away and there is no reason to stop anything else.\n *\n * A piped consumer reads stdin, not the tree: `git log --oneline | grep fix` is allowed, because the\n * bytes came from git metadata, not from a stale file. That is why the scan needs the pipe flag.\n *\n * FAIL-OPEN, with read-stale-guard's own escape valves, so it can never wedge a session:\n * - branch undeterminable / not on `main` / no cache / cache for another branch → allow\n * - `originMain` unknown (offline) → allow\n * - origin/main already an ancestor of HEAD (ancestry, NOT equality) → allow the instant the pull lands\n * - DIRTY tree → allow: the pull is not a clean fast-forward, and resolving that means reading the\n * very files in conflict. Never trap the agent away from its own rescue.\n * - reading `webpieces.config.json` (the mode-OFF escape hatch) and `.webpieces/**` → allow\n */\nexport class StaleMainBashGuardRule extends BashRuleBase<StaleMainBashGuardConfig> {\n constructor(config: StaleMainBashGuardConfig) { super(config, 'stale-main-bash-guard'); }\n\n private readonly scanner = new CommandScanner();\n\n readonly description =\n 'Block content-reading Bash (cat/grep/ls/…) while local main is behind origin/main, so a ' +\n 'session cannot reason over a stale tree through the side door the Read-tool block leaves open.';\n override readonly defaultOptions = {\n hangTimeoutMinutes: DEFAULT_HANG_TIMEOUT_MINUTES,\n };\n readonly fixHint = new FixHint(\n 'You are on main and main is behind origin/main — reading files here gives you stale content.',\n 'Update main first, then re-run your command:',\n [\n new Option('git pull --ff-only origin main (then re-run). If that fatals with \"Cannot fast-forward to multiple branches\", .git/FETCH_HEAD has a duplicate line — run git fetch --prune origin main first.', true),\n new Option('Still allowed right now: builds, tests, installs, the pull itself, all git/gh METADATA (status|log|diff|show|branch), every Write/Edit, and reading webpieces.config.json.'),\n new Option('Disable in webpieces.config.json under hookGuards → stale-main-bash-guard (mode OFF) if intentional.'),\n ],\n );\n\n check(ctx: BashContext): readonly Violation[] {\n const branch = this.currentBranch(ctx.workspaceRoot);\n if (branch === null) return this.allow(ctx, branch, 'branch-undeterminable (fail-open)');\n\n // Keep the shared cache warm for the next call. Detached; never blocks this command.\n triggerMainSyncRefresh(ctx.workspaceRoot, this.config.hangTimeoutMinutes ?? DEFAULT_HANG_TIMEOUT_MINUTES);\n\n // State A is on `main` only. A merged feature branch is merged-branch-bash-guard's job.\n if (branch !== 'main') return this.allow(ctx, branch, 'not-on-main (state B is another guard)');\n\n const status = readMainSyncStatus(ctx.workspaceRoot);\n if (status === null) return this.allow(ctx, branch, 'no-sync-cache (fail-open)', 'cache=none');\n\n const cache = this.cacheSummary(status);\n if (status.branch !== 'main') return this.allow(ctx, branch, 'stale-cross-branch-cache (fail-open)', cache);\n // Offline / origin unresolvable — we have nothing to be stale RELATIVE TO.\n if (status.originMain === '') return this.allow(ctx, branch, 'origin-main-unknown (fail-open)', cache);\n\n // Ancestry, not equality: the moment the pull lands (or we are simply ahead), we are current.\n if (this.contains(ctx.workspaceRoot, status.originMain)) {\n return this.allow(ctx, branch, 'local-main-contains-origin (up to date)', cache);\n }\n\n // A dirty tree means the pull is not a clean fast-forward. Do not cut the agent off from the\n // files it must read to resolve that — the same valve read-stale-guard opens.\n if (this.isDirty(ctx.workspaceRoot)) {\n return this.allow(ctx, branch, 'dirty-tree-on-main (fail-open)', cache);\n }\n\n const reader = this.staleContentRead(ctx);\n if (reader === null) return this.allow(ctx, branch, 'not-a-content-read (cure/build/metadata)', cache);\n\n return this.block(ctx, branch, `stale-main content read (${reader})`, this.staleMessage(ctx.workspaceRoot), cache);\n }\n\n // The first segment that would read stale workspace content, or null when none does. The RAW\n // command is scanned, not commandCode: this is a blocklist-shaped guard, so stripping quoted\n // prose can only ever block LESS (see BashContext.commandCode).\n private staleContentRead(ctx: BashContext): string | null {\n const scan = new ContentReadScan(this.scanner, ctx.workspaceRoot, ctx.effectiveCwd);\n for (const segment of this.scanner.segmentsWithPipes(ctx.command)) {\n const hit = scan.readsStaleContent(segment);\n if (hit !== null) return hit;\n }\n return null;\n }\n\n // Is `commit` already contained in HEAD? Exit code IS the answer, so spawnSync: 0 = ancestor,\n // 1 = genuinely behind, anything else = git could not tell → fail OPEN. (Mirrors read-stale-guard.)\n private contains(workspaceRoot: string, commit: string): boolean {\n const result = spawnSync('git', ['merge-base', '--is-ancestor', commit, 'HEAD'], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n });\n if (result.status === 0) return true;\n if (result.status === 1) return false;\n return true;\n }\n\n private isDirty(workspaceRoot: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git status --porcelain', {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n });\n return out.trim().length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return true; // cannot tell → assume dirty, the fail-OPEN direction here\n }\n }\n\n private staleMessage(workspaceRoot: string): string {\n return new StaleMainMessage(workspaceRoot).forBash(this.behindCount(workspaceRoot));\n }\n\n private behindCount(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git rev-list --count HEAD..origin/main', {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n return /^\\d+$/.test(out) ? out : '?';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '?';\n }\n }\n\n private cacheSummary(status: MainSyncStatus): string {\n return `cache=${status.branch} localMain=${status.localMain.slice(0, 8)} originMain=${status.originMain.slice(0, 8)} ts=${status.timestamp}`;\n }\n\n private allow(ctx: BashContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW', reason, cache);\n return [];\n }\n\n private block(ctx: BashContext, branch: string, reason: string, message: string, cache: string): readonly Violation[] {\n this.logDecision(ctx, branch, 'BLOCK', reason, cache);\n return [new V(1, this.truncate(ctx.command), message)];\n }\n\n private truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n }\n\n private logDecision(ctx: BashContext, branch: string | null, verdict: 'ALLOW' | 'BLOCK', reason: string, cache: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision('stale-main-bash-guard', 'Bash', ctx.command, branch ?? 'unknown', verdict, reason, cache),\n );\n }\n\n private currentBranch(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"stale-main-bash-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/stale-main-bash-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\n\nimport {\n StaleMainBashGuardConfig,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n readMainSyncStatus,\n MainSyncStatus,\n} from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\nimport { toError } from '../to-error';\nimport { triggerMainSyncRefresh } from '../main-sync-refresh';\nimport { logGuardDecision, GuardDecision } from '../decision-log';\nimport { CommandScanner } from '../command-scan';\nimport { StaleMainMessage } from './stale-main-message';\nimport { ContentReadScan } from './content-read-scan';\nimport { TreeRecovery } from './tree-recovery';\n\n/**\n * The BASH half of the STALE-MAIN protection (read-stale-guard's State A), in two halves of its own:\n * a PREVENTIVE check that stops a session landing on a stale `main`, and the REACTIVE check that\n * contains the damage once it is already there.\n *\n * ── PREVENTIVE: a bare `git checkout main` is blocked; the pull must ride along ──────────────────\n *\n * Everything below this paragraph fires only once the session is ALREADY sitting on a stale `main`.\n * Nothing stopped it ARRIVING there, and arriving is one keystroke. In the incident that added this\n * half, an agent ran `git checkout main` after a merge, in a clone whose local `main` was **157\n * commits behind** origin. That checkout did not merely produce stale files — it reverted:\n *\n * 1. `package.json`'s `@webpieces` pin, to a version OLDER than the installed `node_modules`;\n * 2. `.claude/webpieces/ai-hook.sh` — the version-drift guard ITSELF — to a 157-commit-old copy\n * whose message stated the drift BACKWARDS (\"your installed webpieces is older than required\")\n * and named a single cure, `pnpm install`;\n * 3. and so the agent's judgment: it ran that `pnpm install`, DOWNGRADING `node_modules` to match\n * the stale pin, and had to undo it with the `git pull` that should have come first.\n *\n * The shim on current main already diagnoses drift correctly — it distinguishes \"the pin is newer\"\n * from \"the pin is stale, and `pnpm install` would downgrade you\". None of that helped, because the\n * checkout had replaced the shim with the version that could not say it. **A guard a stale checkout\n * can revert cannot be relied on to catch a stale checkout**, which is why this check is preventive\n * and why it lives here rather than in a second rule: same failure, one step earlier, one switch.\n *\n * It matches on command TEXT alone and asks git nothing. That is not laziness — this runs BEFORE the\n * checkout, so the only `main` it could measure is the one it is about to leave. The interesting\n * `main` does not exist yet, and consulting HEAD-at-hook-time is the exact trap\n * `redirect-how-to-merge-main` documents at length. Pairing is unconditionally correct instead: when\n * `main` is already current the chained pull is a sub-second no-op, so no exception is worth carving.\n *\n * BLOCKED `git checkout main`, `git switch main` — with or without flags — when no `git pull`\n * appears anywhere in the SAME command.\n * ALLOWED `git checkout main && git pull origin main`, the pairing this forces, which is the\n * exact line the post-merge cleanup flow already prescribes.\n * ALLOWED `git checkout -b <x> origin/main` (current by construction), `git checkout <sha>`,\n * `git checkout -- <file>`, and any other branch.\n *\n * ── REACTIVE: content-reading Bash on a stale `main` ─────────────────────────────────────────────\n *\n * read-stale-guard blocks the Read tool when local `main` is behind origin/main — but it looks at\n * nothing else, deliberately: \"every cure is a Bash command, so Bash is the escape hatch — never\n * wedge it.\" That reasoning is right about the CURE and wrong about `cat`/`grep`/`ls`. In the\n * incident this closes, an agent sat on a `main` 18 commits behind origin/main (108 files, +8069\n * −3692 upstream), had its Read tool blocked exactly as designed, and then spent the whole session\n * `ls`-ing, `grep`-ing and `cat`-ing the same stale tree through the side door — describing a CI\n * workflow set that was missing a 186-line workflow which existed upstream. The logs read\n * \"read-stale-guard handled\", which is worse than no guard: it looks covered.\n *\n * So this guard blocks CONTENT-READING Bash only, never the whole shell. Builds, tests, installs,\n * `git pull`, git METADATA (log/diff/show/status) — all still run. What is blocked is a command that\n * would put stale FILE CONTENT into context: `cat`/`head`/`grep`/`rg`/`sed`/`awk`/`ls`/`find`/… of a\n * path inside this workspace, and `git grep` / `git show <rev>:<path>` against a local rev. The same\n * line merged-branch-bash-guard already draws for State B, scoped tighter because State A's cure is\n * one command away and there is no reason to stop anything else.\n *\n * A piped consumer reads stdin, not the tree: `git log --oneline | grep fix` is allowed, because the\n * bytes came from git metadata, not from a stale file. That is why the scan needs the pipe flag.\n *\n * FAIL-OPEN, with read-stale-guard's own escape valves, so it can never wedge a session:\n * - branch undeterminable / not on `main` / no cache / cache for another branch → allow\n * - `originMain` unknown (offline) → allow\n * - origin/main already an ancestor of HEAD (ancestry, NOT equality) → allow the instant the pull lands\n * - DIRTY tree → allow: the pull is not a clean fast-forward, and resolving that means reading the\n * very files in conflict. Never trap the agent away from its own rescue.\n * - reading `webpieces.config.json` (the mode-OFF escape hatch) and `.webpieces/**` → allow\n */\nexport class StaleMainBashGuardRule extends BashRuleBase<StaleMainBashGuardConfig> {\n constructor(config: StaleMainBashGuardConfig) { super(config, 'stale-main-bash-guard'); }\n\n private readonly scanner = new CommandScanner();\n private readonly recovery = new TreeRecovery();\n\n readonly description =\n 'Block a bare `git checkout main` (chain the pull into the same command), and block ' +\n 'content-reading Bash (cat/grep/ls/…) while local main is behind origin/main — so a session ' +\n 'neither lands on a stale main nor reasons over one through the side door the Read block leaves.';\n override readonly defaultOptions = {\n hangTimeoutMinutes: DEFAULT_HANG_TIMEOUT_MINUTES,\n };\n readonly fixHint = new FixHint(\n 'Landing on `main` without pulling, or reading files while main is behind origin/main, both give you stale content.',\n 'Pair the checkout with the pull, or update main and re-run:',\n [\n new Option('git checkout main && git pull origin main (the pull must be in the SAME command).', true),\n new Option('Already on main: git pull --ff-only origin main (then re-run). If that fatals with \"Cannot fast-forward to multiple branches\", .git/FETCH_HEAD has a duplicate line — run git fetch --prune origin main first.'),\n new Option('In a linked worktree `git checkout main` FATALS (\"main is already checked out at <primary clone>\") — branch off fresh main instead: git fetch origin main && git checkout -b <name> origin/main'),\n new Option('NOT blocked: `git checkout <sha>`, `git checkout -b <x> origin/main`, `git checkout -- <file>`, any other branch. Also still allowed: builds, tests, installs, the pull itself, all git/gh METADATA (status|log|diff|show|branch), every Write/Edit, and reading webpieces.config.json.'),\n new Option('Disable in webpieces.config.json under hookGuards → stale-main-bash-guard (mode OFF) if intentional.'),\n ],\n );\n\n check(ctx: BashContext): readonly Violation[] {\n // PREVENTIVE half, FIRST and unconditional. Deliberately ahead of every fail-open bailout\n // below: those all ask \"is the main we are ON stale?\", and this asks about the main we are\n // about to MOVE TO — a different branch, and one no cache can describe yet.\n const bare = this.bareCheckoutOfMain(ctx);\n if (bare !== null) {\n return this.block(ctx, 'any', `bare checkout of main (${bare})`, this.pairingMessage(ctx), '-');\n }\n\n const branch = this.currentBranch(ctx.workspaceRoot);\n if (branch === null) return this.allow(ctx, branch, 'branch-undeterminable (fail-open)');\n\n // Keep the shared cache warm for the next call. Detached; never blocks this command.\n triggerMainSyncRefresh(ctx.workspaceRoot, this.config.hangTimeoutMinutes ?? DEFAULT_HANG_TIMEOUT_MINUTES);\n\n // State A is on `main` only. A merged feature branch is merged-branch-bash-guard's job.\n if (branch !== 'main') return this.allow(ctx, branch, 'not-on-main (state B is another guard)');\n\n const status = readMainSyncStatus(ctx.workspaceRoot);\n if (status === null) return this.allow(ctx, branch, 'no-sync-cache (fail-open)', 'cache=none');\n\n const cache = this.cacheSummary(status);\n if (status.branch !== 'main') return this.allow(ctx, branch, 'stale-cross-branch-cache (fail-open)', cache);\n // Offline / origin unresolvable — we have nothing to be stale RELATIVE TO.\n if (status.originMain === '') return this.allow(ctx, branch, 'origin-main-unknown (fail-open)', cache);\n\n // Ancestry, not equality: the moment the pull lands (or we are simply ahead), we are current.\n if (this.contains(ctx.workspaceRoot, status.originMain)) {\n return this.allow(ctx, branch, 'local-main-contains-origin (up to date)', cache);\n }\n\n // A dirty tree means the pull is not a clean fast-forward. Do not cut the agent off from the\n // files it must read to resolve that — the same valve read-stale-guard opens.\n if (this.isDirty(ctx.workspaceRoot)) {\n return this.allow(ctx, branch, 'dirty-tree-on-main (fail-open)', cache);\n }\n\n const reader = this.staleContentRead(ctx);\n if (reader === null) return this.allow(ctx, branch, 'not-a-content-read (cure/build/metadata)', cache);\n\n return this.block(ctx, branch, `stale-main content read (${reader})`, this.staleMessage(ctx.workspaceRoot), cache);\n }\n\n /**\n * The first segment that switches to the `main` BRANCH with no `git pull` anywhere in the same\n * command, or null. The pull is looked for across the WHOLE command, not the matched segment,\n * because `git checkout main && git pull origin main` splits into two segments and the pairing is\n * the point.\n */\n private bareCheckoutOfMain(ctx: BashContext): string | null {\n for (const segment of this.scanner.commandSegments(ctx.command)) {\n if (!this.scanner.invokesGit(segment, 'checkout') && !this.scanner.invokesGit(segment, 'switch')) continue;\n if (!this.switchesToMainBranch(segment)) continue;\n return this.scanner.commandInvokesAnyGit(ctx.command, ['pull']) ? null : segment;\n }\n return null;\n }\n\n /**\n * True only for landing ON the branch. `-b`/`-B`/`-c`/`-C` CREATE a branch, so\n * `git checkout -b x origin/main` is current by construction and never blocked; a `--` turns the\n * rest into pathspecs, so `git checkout -- main` restores a FILE named main and moves no branch.\n */\n private switchesToMainBranch(segment: string): boolean {\n const words = this.scanner.words(segment);\n for (let i = 0; i < words.length; i++) {\n const word = words[i];\n if (word === '--') return false;\n if (/^-[bBcC]$/.test(word)) return false;\n if (i > 1 && word === 'main') return true;\n }\n return false;\n }\n\n private pairingMessage(ctx: BashContext): string {\n const steps = this.recovery.updateMainSteps(this.recovery.kindOf(ctx.workspaceRoot)).join('\\n');\n return 'Blocked: a bare `git checkout main` lands you on whatever local `main` you last had. '\n + 'That is not only stale FILES — it also reverts `package.json`\\'s @webpieces pin and the '\n + 'guard shim under `.claude/webpieces/`, so the very hook that would diagnose the resulting '\n + 'version drift is replaced by an older copy that reports it BACKWARDS and names the cure '\n + 'that makes it worse. Chain the pull into the same command, leaving no window in which you '\n + 'are on a stale main:\\n' + steps;\n }\n\n // The first segment that would read stale workspace content, or null when none does. The RAW\n // command is scanned, not commandCode: this is a blocklist-shaped guard, so stripping quoted\n // prose can only ever block LESS (see BashContext.commandCode).\n private staleContentRead(ctx: BashContext): string | null {\n const scan = new ContentReadScan(this.scanner, ctx.workspaceRoot, ctx.effectiveCwd);\n for (const segment of this.scanner.segmentsWithPipes(ctx.command)) {\n const hit = scan.readsStaleContent(segment);\n if (hit !== null) return hit;\n }\n return null;\n }\n\n // Is `commit` already contained in HEAD? Exit code IS the answer, so spawnSync: 0 = ancestor,\n // 1 = genuinely behind, anything else = git could not tell → fail OPEN. (Mirrors read-stale-guard.)\n private contains(workspaceRoot: string, commit: string): boolean {\n const result = spawnSync('git', ['merge-base', '--is-ancestor', commit, 'HEAD'], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n });\n if (result.status === 0) return true;\n if (result.status === 1) return false;\n return true;\n }\n\n private isDirty(workspaceRoot: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git status --porcelain', {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n });\n return out.trim().length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return true; // cannot tell → assume dirty, the fail-OPEN direction here\n }\n }\n\n private staleMessage(workspaceRoot: string): string {\n return new StaleMainMessage(workspaceRoot).forBash(this.behindCount(workspaceRoot));\n }\n\n private behindCount(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git rev-list --count HEAD..origin/main', {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n return /^\\d+$/.test(out) ? out : '?';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '?';\n }\n }\n\n private cacheSummary(status: MainSyncStatus): string {\n return `cache=${status.branch} localMain=${status.localMain.slice(0, 8)} originMain=${status.originMain.slice(0, 8)} ts=${status.timestamp}`;\n }\n\n private allow(ctx: BashContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW', reason, cache);\n return [];\n }\n\n private block(ctx: BashContext, branch: string, reason: string, message: string, cache: string): readonly Violation[] {\n this.logDecision(ctx, branch, 'BLOCK', reason, cache);\n return [new V(1, this.truncate(ctx.command), message)];\n }\n\n private truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n }\n\n private logDecision(ctx: BashContext, branch: string | null, verdict: 'ALLOW' | 'BLOCK', reason: string, cache: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision('stale-main-bash-guard', 'Bash', ctx.command, branch ?? 'unknown', verdict, reason, cache),\n );\n }\n\n private currentBranch(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
|
package/src/core/runner.js
CHANGED
|
@@ -33,15 +33,13 @@ function filterByMode(rules, mode) {
|
|
|
33
33
|
return rules.filter((r) => (0, rules_config_1.isHookGuard)(r.name));
|
|
34
34
|
return rules.filter((r) => !(0, rules_config_1.isHookGuard)(r.name));
|
|
35
35
|
}
|
|
36
|
-
// Drop
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// Only file tools reach here; bash git/PR guards (no file path) are never affected.
|
|
36
|
+
// Drop every rule excluded for this path (webpieces.config.json → excludePaths). ONE glob list: a path
|
|
37
|
+
// listed there is hands-off for code-style rules and file-scoped guards alike, because webpieces either
|
|
38
|
+
// governs a path or it does not. Per-rule carve-outs live in the rule's own `excludePaths`.
|
|
40
39
|
function filterByExcludedPaths(rules, relativePath, ex) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
40
|
+
if (ex.paths.some((p) => (0, load_rules_1.globMatches)(p, relativePath)))
|
|
41
|
+
return [];
|
|
42
|
+
return rules;
|
|
45
43
|
}
|
|
46
44
|
// The cwd a command actually runs from, after its own leading `cd`/`pushd` run. Thin delegate kept
|
|
47
45
|
// for the callers (and specs) that only need the directory; the full tree classification — primary
|
|
@@ -171,10 +169,17 @@ function isL0CureCommand(command) {
|
|
|
171
169
|
return shim_1.L0_ALLOW_JS.test(command.trim());
|
|
172
170
|
}
|
|
173
171
|
// Force-to-root: git/gh commands must run from the repo root of the tree they act on, where the guards
|
|
174
|
-
// can reason about git state coherently.
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
172
|
+
// can reason about git state coherently.
|
|
173
|
+
//
|
|
174
|
+
// ONE variable decides it: `tree.effectiveCwd` — the directory the command actually runs in, which is
|
|
175
|
+
// the shell's cwd unless the command leads with `cd <dir> &&`. Root or not-root, nothing else.
|
|
176
|
+
//
|
|
177
|
+
// It used to be `shellAtRoot || cdsToRoot`, two variables OR'd, and that produced opposite verdicts for
|
|
178
|
+
// the same destination: `git status` with the shell in packages/http/ was BLOCKED, while
|
|
179
|
+
// `cd packages/http && git status` from the root was ALLOWED, because shellAtRoot short-circuited
|
|
180
|
+
// before the destination was ever considered. The point of this guard is to keep the agent's git work
|
|
181
|
+
// at the root — an agent that cd's INTO a subdir has the same broken mental model as one that is
|
|
182
|
+
// stranded there, so it gets the same answer now.
|
|
178
183
|
//
|
|
179
184
|
// The remedy is emitted as ONE runnable line, `cd <root> && <the original command>`, because `cd` does
|
|
180
185
|
// not persist between tool calls: telling the agent to "cd first, then re-run" costs a turn and the
|
|
@@ -182,12 +187,11 @@ function isL0CureCommand(command) {
|
|
|
182
187
|
// print the very command it had just rejected.
|
|
183
188
|
// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape
|
|
184
189
|
function gitFromSubdirBlock(command, tree) {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
if (!isGitOrGhCommand(command) || shellAtRoot || cdsToRoot)
|
|
190
|
+
const targetAtRoot = path.resolve(tree.effectiveCwd) === path.resolve(tree.root);
|
|
191
|
+
if (!isGitOrGhCommand(command) || targetAtRoot)
|
|
188
192
|
return null;
|
|
189
193
|
const report = `❌ Run git/gh commands from the repo root, not a subdirectory.\n` +
|
|
190
|
-
`
|
|
194
|
+
` Command runs in: ${tree.effectiveCwd}\n` +
|
|
191
195
|
` Judged against: ${tree.root}\n` +
|
|
192
196
|
` Run EXACTLY this instead (one line — \`cd\` does NOT persist between tool calls):\n` +
|
|
193
197
|
` ${(0, effective_tree_1.atRoot)(tree.root, command)}\n` +
|
|
@@ -261,7 +265,7 @@ function runBashInternal(command, cwd, mode) {
|
|
|
261
265
|
(0, decision_log_1.logGuardDecision)(workspaceRoot, new decision_log_1.GuardDecision('-', 'Bash', command, (0, decision_log_1.branchForLog)(workspaceRoot), 'ALLOW', 'foreign git repo (out of scope)'));
|
|
262
266
|
return null;
|
|
263
267
|
}
|
|
264
|
-
// Honour excludePaths
|
|
268
|
+
// Honour excludePaths on the bash path too (not just Read/Edit): a command whose effective
|
|
265
269
|
// cwd sits under an excluded tree (e.g. repositories/**) drops the whole guard set — matching how
|
|
266
270
|
// runInternal/runRead treat file paths. The relative path is '' when there is no `cd` (root), which
|
|
267
271
|
// matches no exclusion glob, so a plain command at the repo root is unaffected.
|
|
@@ -294,7 +298,7 @@ function runBashInternal(command, cwd, mode) {
|
|
|
294
298
|
}
|
|
295
299
|
const ruleNames = groups.map((g) => g.ruleName).join(',');
|
|
296
300
|
(0, decision_log_1.logGuardDecision)(tree.root, new decision_log_1.GuardDecision(ruleNames, 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), 'BLOCK', 'bash-guard block'));
|
|
297
|
-
const report = (0, report_1.formatReport)(commandLabel(command), groups, report_1.BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.
|
|
301
|
+
const report = (0, report_1.formatReport)(commandLabel(command), groups, report_1.BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths);
|
|
298
302
|
return new types_1.BlockedResult(report);
|
|
299
303
|
}
|
|
300
304
|
// The bash report's subject line. It used to be the literal string `<bash>`, which told the agent
|
package/src/core/runner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/runner.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { loadAndValidate, LoadedConfig, WebpiecesRulesConfig, ExcludePaths, isHookGuard, DEFAULT_HANG_TIMEOUT_MINUTES, RepoRootFinder } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { EffectiveTree, EffectiveTreeResolver, atRoot } from './effective-tree';\nimport { loadRules, loadMatchRules, globMatches } from './load-rules';\nimport { MatchRule } from './rules/match-rule';\nimport { triggerMainSyncRefresh } from './main-sync-refresh';\nimport { logGuardDecision, GuardDecision, branchForLog } from './decision-log';\nimport { toError } from './to-error';\nimport { formatReport, READ_SUBJECT, BASH_SUBJECT } from './report';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { L0_ALLOW_JS } from '../bin/shim';\nimport { CONFIG_MISSING_REPORT, CONFIG_OUT_OF_SYNC_HEADER, writeGuardMatrixDoc, guardMatrixPointer } from './l0-matrix';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult, HookMode,\n Rule, Violation, RuleGroup, RuleFailError, InformAiError,\n EditContext, FileContext, BashContext,\n} from './types';\n\n// Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a\n// disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the\n// openclaw plugin adapter, a single before_tool_call hook). isHookGuard is the shared classifier in\n// @webpieces/rules-config.\nfunction filterByMode(rules: readonly Rule[], mode: HookMode): readonly Rule[] {\n if (mode === 'all') return rules;\n if (mode === 'guards') return rules.filter((r: Rule): boolean => isHookGuard(r.name));\n return rules.filter((r: Rule): boolean => !isHookGuard(r.name));\n}\n\n// Drop rules whose category is excluded for this file path (webpieces.config.json → excludePaths).\n// Two independent glob lists: `guards` suppresses file-scoped guards (e.g. feature-branch-guard),\n// `rules` suppresses code-style rules — so a vendored tree can be exempt from one but not the other.\n// Only file tools reach here; bash git/PR guards (no file path) are never affected.\nexport function filterByExcludedPaths(rules: readonly Rule[], relativePath: string, ex: ExcludePaths): readonly Rule[] {\n return rules.filter((r: Rule): boolean => {\n const patterns = isHookGuard(r.name) ? ex.guards : ex.rules;\n return !patterns.some((p: string): boolean => globMatches(p, relativePath));\n });\n}\n\n// The cwd a command actually runs from, after its own leading `cd`/`pushd` run. Thin delegate kept\n// for the callers (and specs) that only need the directory; the full tree classification — primary\n// clone vs linked worktree vs nested clone vs outside any repo — is EffectiveTreeResolver.resolve().\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nexport function effectiveBashCwd(command: string, cwd: string): string {\n return new EffectiveTreeResolver().effectiveCwd(command, cwd);\n}\n\n// A git or gh invocation anywhere in the command (start, or after a ;/&&/|| separator or pipe).\nconst GIT_OR_GH_RE = /(?:^|[;&|]\\s*)(?:git|gh)\\b/;\nexport function isGitOrGhCommand(command: string): boolean {\n return GIT_OR_GH_RE.test(command);\n}\n\n// Fire-and-forget the detached refresher when feature-branch-guard is loaded and active, so the\n// cache (.webpieces/main-sync-status.json) stays fresh as the AI works. The guard rule itself also\n// triggers this on Write/Edit; this covers the Bash path so the cache is warm on every command.\nfunction maybeRefreshMainSync(rules: readonly Rule[], workspaceRoot: string): void {\n const guard = rules.find((r: Rule): boolean => r.name === 'feature-branch-guard');\n if (guard && guard.shouldRun()) {\n triggerMainSyncRefresh(workspaceRoot, DEFAULT_HANG_TIMEOUT_MINUTES);\n }\n}\n\n// Fault C (webpieces.config.json missing) — the deny text lives in ./l0-matrix beside the rest of the\n// L0 fault table, so the message and the allowlist can never prescribe different cures. `cwd` is used\n// only to drop the matrix doc where the AI can read it (the config root does not exist yet here).\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction configMissingBlock(cwd: string): BlockedResult {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n return new BlockedResult(CONFIG_MISSING_REPORT + guardMatrixPointer(writeGuardMatrixDoc(root)));\n}\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode = 'all',\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd, mode);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode,\n): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return configMissingBlock(cwd);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Always allow edits to webpieces.config.json — it's the fix target when out of sync\n if (path.resolve(input.filePath) === path.resolve(loaded.configPath)) {\n return null;\n }\n\n // Built-in/custom rules PLUS the client-authored match-rules (content guards). Match-rules run only\n // in the file-edit path (they are code-style, so filterByMode keeps them out of the bash/guards path).\n const allRules = [...loadRules(loaded.rulesConfig, workspaceRoot), ...loadMatchRules(loaded.matchRules)];\n const modeRules = filterByMode(allRules, mode);\n if (modeRules.length === 0) return null;\n\n // Suppress enforcement for files under this category's excludePaths (e.g. vendored repos under\n // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole\n // rule set and is fully hands-off — no violations AND no config-sync nag on those files.\n const relativePath = path.relative(workspaceRoot, input.filePath);\n const rules = filterByExcludedPaths(modeRules, relativePath, loaded.excludePaths);\n if (rules.length === 0) return null;\n\n // Config-sync applies only to built-in/custom rules; match-rules have their own validated section\n // (loadAndValidate already rejected an invalid `match-rules`), so they must not trip the sync nag.\n const outOfSync = checkConfigSync(rules.filter((r: Rule) => !(r instanceof MatchRule)), loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n\n const editGroups = runEditRules(rules, contexts.editContexts);\n const fileGroups = runFileRules(rules, contexts.fileContext);\n const allGroups = [...editGroups, ...fileGroups];\n\n if (allGroups.length === 0) return null;\n\n const report = formatReport(relativePath, allGroups);\n return new BlockedResult(report);\n}\n\nexport function runBash(command: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n return runBashInternal(command, cwd, mode);\n}\n\n// The name of the ONLY rule permitted to block a Read. Reads are the highest-blast-radius tool\n// there is, so this path is an explicit single-rule allowlist rather than the general rule loop.\nconst READ_SCOPED_GUARDS: ReadonlySet<string> = new Set(['read-stale-guard']);\n\n/**\n * The Read path. Deliberately NOT `run()`:\n *\n * - NO config-sync check. A rule present in code but missing from webpieces.config.json blocks\n * every Write/Edit/Bash by design — but applying that to Read would mean an upgrade that adds\n * any new rule instantly blocks the agent from reading the very config file it must edit to fix\n * it. Reads must never carry that failure mode.\n * - NO general rule loop. Only READ_SCOPED_GUARDS run, so no code-style rule can ever see a Read.\n * - Fails OPEN everywhere, including on a thrown rule (the caller catches and allows).\n *\n * Returns null (allow) unless the one guard fires.\n */\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runRead(filePath: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n // Code-style mode has nothing to say about a read.\n if (mode === 'rules') return null;\n\n const loaded = loadAndValidate(cwd);\n // No config → nothing to enforce. Unlike the edit path we do NOT block: an unconfigured repo\n // must still be readable.\n if (loaded.configPath === null) return null;\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Same git-repo-boundary governance as bash, through the SAME resolver: a read inside a different\n // clone is out of scope. (No command to parse here, so the shell cwd IS the effective cwd.)\n if (new EffectiveTreeResolver().resolve('', cwd, workspaceRoot).kind === 'foreign') return null;\n\n const relativePath = path.relative(workspaceRoot, filePath);\n const all = loadRules(loaded.rulesConfig, workspaceRoot);\n const rules = filterByExcludedPaths(\n all.filter((r: Rule): boolean => READ_SCOPED_GUARDS.has(r.name)),\n relativePath,\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const ctx = new FileContext('Read', filePath, relativePath, workspaceRoot, 0, 0, 0, 0);\n const groups = runFileRules(rules, ctx);\n if (groups.length === 0) return null;\n\n return new BlockedResult(formatReport(relativePath, groups, READ_SUBJECT));\n}\n\n// L0 cure bypass — every command on THE L0 allowlist passes here, ahead of any config load. A\n// webpieces.config.json that is ahead of the installed validator (new rule tokens the published\n// binary doesn't know yet) makes loadAndValidate() throw and would deny `pnpm install` — the very\n// command that updates the validator (deadlock).\n//\n// This used to test INSTALLER_ALLOW_JS alone, which made the config faults (C = config missing,\n// Y = config out of sync) accept a bare `pnpm install` while denying `rm -rf node_modules && pnpm\n// install` — the one cure that works when node_modules is CORRUPT rather than merely stale. Same\n// intent, opposite verdict, for no reason anyone recorded. L0_ALLOW_JS is the single list every L0\n// fault consults; see its block comment in ../bin/shim. Each alternative is still anchored at both\n// ends, so `pnpm install && rm -rf /` still falls to the guards.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction isL0CureCommand(command: string): boolean {\n return L0_ALLOW_JS.test(command.trim());\n}\n\n// Force-to-root: git/gh commands must run from the repo root of the tree they act on, where the guards\n// can reason about git state coherently. Fires ONLY when the shell is STUCK in a governed subdir — the\n// shell persists in a subdir AND the command does not `cd` to a tree root (`tree.root`, which is the\n// linked worktree's root when the command cd's into one). A command that explicitly cd's to a root is\n// judged by the normal guards, never here.\n//\n// The remedy is emitted as ONE runnable line, `cd <root> && <the original command>`, because `cd` does\n// not persist between tool calls: telling the agent to \"cd first, then re-run\" costs a turn and the\n// next call starts back in the old directory anyway. The bare-`cd` advice is what made this guard\n// print the very command it had just rejected.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction gitFromSubdirBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const shellAtRoot = path.resolve(tree.shellCwd) === path.resolve(tree.root);\n const cdsToRoot = path.resolve(tree.effectiveCwd) === path.resolve(tree.root);\n if (!isGitOrGhCommand(command) || shellAtRoot || cdsToRoot) return null;\n const report =\n `❌ Run git/gh commands from the repo root, not a subdirectory.\\n` +\n ` You are in: ${tree.shellCwd}\\n` +\n ` Judged against: ${tree.root}\\n` +\n ` Run EXACTLY this instead (one line — \\`cd\\` does NOT persist between tool calls):\\n` +\n ` ${atRoot(tree.root, command)}\\n` +\n ` A leading \\`cd <path> &&\\` is ACCEPTED by the guards — it cannot change what the command\\n` +\n ` does to the repo. (The webpieces guards evaluate the repo's git state at its root.)`;\n logGuardDecision(tree.root, new GuardDecision('force-to-root', 'Bash', command, branchForLog(tree.root), 'BLOCK', 'git/gh from subdir'));\n return new BlockedResult(report);\n}\n\n// The L0 cure bypass's audit line. Anchored at the repo root that owns `.webpieces` — RepoRootFinder\n// (config-walk-up first, then git toplevel) is the authority for that, and it is correct in a linked\n// worktree because each worktree checks out its own webpieces.config.json. This runs BEFORE\n// loadAndValidate, which is why it resolves the root itself rather than using workspaceRoot.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction logL0CureBypass(command: string, cwd: string): void {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n logGuardDecision(root, new GuardDecision('-', 'Bash', command, branchForLog(root), 'ALLOW', 'L0 cure bypass (always allowed)'));\n}\n\n/**\n * Load the config for the bash path — but do NOT let an unloadable config trap the tools needed to\n * repair it.\n *\n * loadAndValidate throws an InformAiError when webpieces.config.json is unparseable (a real syntax\n * error, or leftover `<<<<<<< HEAD` markers mid-merge) or fails validation. That throw propagates to\n * the hook adapter, which fails CLOSED and denies the command — correct for work, since a config that\n * did not load means no guards ran. But it denied `cat`/`grep`/`sed -n` on webpieces.config.json too,\n * i.e. it blocked the only way to see the problem it was reporting. Observed live, twice.\n *\n * So: on a load failure, a provably-inert INSPECTION command is allowed through (returns null, \"no\n * block\"), matching the escape hatch every other layer already grants this file. Everything else —\n * every write, every git/gh command, every build — still hits the same hard failure as before. The\n * bypass cannot be widened by accident; see ReadOnlyInspectionScan for how narrow \"inert\" is.\n *\n * Returns the loaded config, or null meaning \"allow this command without guards\".\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction loadConfigOrAllowInspection(command: string, cwd: string): LoadedConfig | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrown unchanged unless the command is provably inert\n try {\n return loadAndValidate(cwd);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof InformAiError && new ReadOnlyInspectionScan().isReadOnlyInspection(command)) {\n return null;\n }\n throw error;\n }\n}\n\nfunction runBashInternal(command: string, cwd: string, mode: HookMode): BlockedResult | null {\n if (isL0CureCommand(command)) {\n logL0CureBypass(command, cwd);\n return null;\n }\n\n const loaded = loadConfigOrAllowInspection(command, cwd);\n // null = the config would not load AND this command only inspects → allow, see the helper.\n if (loaded === null) return null;\n if (loaded.configPath === null) return configMissingBlock(cwd);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // WHICH TREE does this command act on? Not necessarily the shell's cwd — an agent working in a\n // linked worktree writes `cd <worktree> && …` because `cd` does not persist between tool calls.\n // ONE resolver answers this for the guards AND for force-to-root below, so the two can never\n // disagree about which tree you are in.\n const tree = new EffectiveTreeResolver().resolve(command, cwd, workspaceRoot);\n\n // Git-repo-boundary governance: the command runs inside a DIFFERENT git repo than this\n // webpieces.config governs (e.g. a clone under repositories/). Out of scope → allow, hands-off.\n // Intentional, not a silent hole. A LINKED WORKTREE of this repo is deliberately NOT foreign — it\n // is the same project, so the guards run against THAT tree's branch and cache.\n if (tree.kind === 'foreign') {\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW', 'foreign git repo (out of scope)'));\n return null;\n }\n\n // Honour excludePaths.guards on the bash path too (not just Read/Edit): a command whose effective\n // cwd sits under an excluded tree (e.g. repositories/**) drops the whole guard set — matching how\n // runInternal/runRead treat file paths. The relative path is '' when there is no `cd` (root), which\n // matches no exclusion glob, so a plain command at the repo root is unaffected.\n const rules = filterByExcludedPaths(\n filterByMode(loadRules(loaded.rulesConfig, workspaceRoot), mode),\n path.relative(workspaceRoot, tree.effectiveCwd),\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const subdirBlock = gitFromSubdirBlock(command, tree);\n if (subdirBlock) return subdirBlock;\n\n // Keep the feature-branch-guard cache warm on EVERY command (not just Write/Edit): the AI runs\n // far more bash than edits, so refreshing here means the guard's next file-edit check reads a\n // fresh status. Detached + fire-and-forget — never blocks the command. Only when the guard is\n // loaded (guards/all mode) and enabled, so a project that opted out never triggers git fetches.\n // Keyed on the JUDGED tree, so a worktree's cache is refreshed rather than the primary clone's.\n maybeRefreshMainSync(rules, tree.root);\n\n const ctx = buildBashContext(command, tree);\n const groups = runBashRules(rules, ctx);\n if (groups.length === 0) {\n // Record the ALLOW only for git/gh commands — the operations the bash guards actually reason\n // about (branch create, commit, push, merge, PR). Skipping ls/cat/grep keeps the audit log\n // focused (the whole point of the log is \"why did/didn't a guard fire?\"). Blocks are always\n // logged below.\n if (/\\b(?:git|gh)\\b/.test(command)) {\n logGuardDecision(tree.root, new GuardDecision('-', 'Bash', command, branchForLog(tree.root), 'ALLOW', 'no bash-guard block'));\n }\n return null;\n }\n\n const ruleNames = groups.map((g: RuleGroup): string => g.ruleName).join(',');\n logGuardDecision(tree.root, new GuardDecision(ruleNames, 'Bash', command, branchForLog(tree.root), 'BLOCK', 'bash-guard block'));\n const report = formatReport(commandLabel(command), groups, BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.guards);\n return new BlockedResult(report);\n}\n\n// The bash report's subject line. It used to be the literal string `<bash>`, which told the agent\n// nothing; the command itself is what was blocked, so name it — truncated, because a heredoc-bearing\n// command can run to thousands of characters and the violation lines already carry the detail.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction commandLabel(command: string): string {\n const oneLine = command.replace(/\\s+/g, ' ').trim();\n const MAX = 100;\n return oneLine.length <= MAX ? oneLine : oneLine.slice(0, MAX) + '…';\n}\n\n// When a push/PR block fires AND the config exempts vendored/nested trees, surface the escape hatch the\n// AI cannot otherwise discover: git/gh run UNGUARDED inside those trees if it cd's there first (each is\n// governed by its own repo, not this one). Scoped to pr-creation-or-push-guard — for the other guards\n// \"cd into an exempt tree\" is not the remedy — and emitted only when such trees are actually configured,\n// so a repo without exemptions never sees the noise.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction exemptTreesHint(groups: readonly RuleGroup[], exemptGuards: readonly string[]): string {\n if (exemptGuards.length === 0) return '';\n if (!groups.some((g: RuleGroup): boolean => g.ruleName === 'pr-creation-or-push-guard')) return '';\n return `\\n\\nℹ️ Working in a nested repo under one of these exempt trees? cd into it first and run git/gh `\n + `normally there — the webpieces guards do NOT govern them (each is its own repo): ${exemptGuards.join(', ')}.`;\n}\n\n// The set of rule names explicitly present in webpieces.config.json (every key except rulesDir).\nfunction configuredRuleNames(config: WebpiecesRulesConfig): ReadonlySet<string> {\n return new Set(Object.keys(config).filter((k: string) => k !== 'rulesDir'));\n}\n\nfunction checkConfigSync(rules: readonly Rule[], config: WebpiecesRulesConfig): BlockedResult | null {\n const configured = configuredRuleNames(config);\n const unconfiguredRules = rules.filter((r: Rule) => !configured.has(r.name));\n if (unconfiguredRules.length === 0) return null;\n\n const lines = [\n // Fault Y's header lives in ./l0-matrix beside the rest of the L0 fault table (same reason as\n // CONFIG_MISSING_REPORT: one place states what this fault is and what cures it).\n CONFIG_OUT_OF_SYNC_HEADER,\n '',\n 'Tell the human: the following rules need to be configured. Ask for each one:',\n ' - Should this rule be ON, OFF, NEW_AND_MODIFIED_CODE, or NEW_AND_MODIFIED_FILES?',\n ' - What values do you want for the options listed below?',\n 'Then update webpieces.config.json and retry.',\n '',\n 'Do NOT proceed until webpieces.config.json has an entry for every rule below.',\n '',\n ];\n\n for (const rule of unconfiguredRules) {\n lines.push(`--- ${rule.name} ---`);\n lines.push(`Description: ${rule.description}`);\n const opts = rule.defaultOptions;\n const optKeys = Object.keys(opts);\n if (optKeys.length > 0) {\n lines.push(`Available options (suggested defaults shown):`);\n for (const key of optKeys) {\n lines.push(` ${key}: ${JSON.stringify(opts[key])}`);\n }\n } else {\n lines.push('Available options: none beyond mode');\n }\n lines.push(`Example entry for webpieces.config.json:`);\n lines.push(` \"${rule.name}\": { \"mode\": \"ON\" }`);\n lines.push('');\n }\n\n return new BlockedResult(lines.join('\\n'));\n}\n\n// N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may\n// EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees:\n// - a thrown RuleFailError → an expected, well-formed violation (its line/snippet/fixHints kept);\n// - a thrown plain Error → a \"crashed\" violation (a bug, surfaced not swallowed).\nexport function runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return rule.check(ctx);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) {\n return [violationFromRuleFail(error)];\n }\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\n// A thrown RuleFailError carries its own AI-facing message + optional location and fix hints. Fold the\n// fix hints into the message because Violation has no fixHint field (RuleGroup's fixHint comes from the\n// rule definition, not a per-throw value).\nfunction violationFromRuleFail(error: RuleFailError): Violation {\n const hints = error.fixHints.length > 0 ? `\\n Fix: ${error.fixHints.join('\\n Fix: ')}` : '';\n return new Violation(error.line ?? 0, error.snippet ?? '', error.aiMessage + hints);\n}\n\nfunction ruleMatchesFile(rule: Rule, relativePath: string): boolean {\n for (const pattern of rule.files) {\n if (globMatches(pattern, relativePath)) return true;\n }\n return false;\n}\n\nfunction runBashRules(rules: readonly Rule[], bashContext: BashContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'bash') continue;\n if (!rule.shouldRun()) continue;\n const vs = runRuleCheck(rule, bashContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n\nfunction runEditRules(rules: readonly Rule[], editContexts: readonly EditContext[]): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'edit') continue;\n if (!rule.shouldRun()) continue;\n const allViolations: Violation[] = [];\n for (const ctx of editContexts) {\n if (!ruleMatchesFile(rule, ctx.relativePath)) continue;\n const vs = runRuleCheck(rule, ctx);\n for (const v of vs) {\n const copy = new Violation(v.line, v.snippet, v.message);\n copy.editIndex = ctx.editIndex;\n copy.editCount = ctx.editCount;\n allViolations.push(copy);\n }\n }\n if (allViolations.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, allViolations,\n ));\n }\n }\n return groups;\n}\n\nfunction runFileRules(rules: readonly Rule[], fileContext: FileContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'file') continue;\n if (!rule.shouldRun()) continue;\n if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;\n const vs = runRuleCheck(rule, fileContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/runner.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { loadAndValidate, LoadedConfig, WebpiecesRulesConfig, ExcludePaths, isHookGuard, DEFAULT_HANG_TIMEOUT_MINUTES, RepoRootFinder } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { EffectiveTree, EffectiveTreeResolver, atRoot } from './effective-tree';\nimport { loadRules, loadMatchRules, globMatches } from './load-rules';\nimport { MatchRule } from './rules/match-rule';\nimport { triggerMainSyncRefresh } from './main-sync-refresh';\nimport { logGuardDecision, GuardDecision, branchForLog } from './decision-log';\nimport { toError } from './to-error';\nimport { formatReport, READ_SUBJECT, BASH_SUBJECT } from './report';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { L0_ALLOW_JS } from '../bin/shim';\nimport { CONFIG_MISSING_REPORT, CONFIG_OUT_OF_SYNC_HEADER, writeGuardMatrixDoc, guardMatrixPointer } from './l0-matrix';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult, HookMode,\n Rule, Violation, RuleGroup, RuleFailError, InformAiError,\n EditContext, FileContext, BashContext,\n} from './types';\n\n// Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a\n// disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the\n// openclaw plugin adapter, a single before_tool_call hook). isHookGuard is the shared classifier in\n// @webpieces/rules-config.\nfunction filterByMode(rules: readonly Rule[], mode: HookMode): readonly Rule[] {\n if (mode === 'all') return rules;\n if (mode === 'guards') return rules.filter((r: Rule): boolean => isHookGuard(r.name));\n return rules.filter((r: Rule): boolean => !isHookGuard(r.name));\n}\n\n// Drop every rule excluded for this path (webpieces.config.json → excludePaths). ONE glob list: a path\n// listed there is hands-off for code-style rules and file-scoped guards alike, because webpieces either\n// governs a path or it does not. Per-rule carve-outs live in the rule's own `excludePaths`.\nexport function filterByExcludedPaths(rules: readonly Rule[], relativePath: string, ex: ExcludePaths): readonly Rule[] {\n if (ex.paths.some((p: string): boolean => globMatches(p, relativePath))) return [];\n return rules;\n}\n\n// The cwd a command actually runs from, after its own leading `cd`/`pushd` run. Thin delegate kept\n// for the callers (and specs) that only need the directory; the full tree classification — primary\n// clone vs linked worktree vs nested clone vs outside any repo — is EffectiveTreeResolver.resolve().\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nexport function effectiveBashCwd(command: string, cwd: string): string {\n return new EffectiveTreeResolver().effectiveCwd(command, cwd);\n}\n\n// A git or gh invocation anywhere in the command (start, or after a ;/&&/|| separator or pipe).\nconst GIT_OR_GH_RE = /(?:^|[;&|]\\s*)(?:git|gh)\\b/;\nexport function isGitOrGhCommand(command: string): boolean {\n return GIT_OR_GH_RE.test(command);\n}\n\n// Fire-and-forget the detached refresher when feature-branch-guard is loaded and active, so the\n// cache (.webpieces/main-sync-status.json) stays fresh as the AI works. The guard rule itself also\n// triggers this on Write/Edit; this covers the Bash path so the cache is warm on every command.\nfunction maybeRefreshMainSync(rules: readonly Rule[], workspaceRoot: string): void {\n const guard = rules.find((r: Rule): boolean => r.name === 'feature-branch-guard');\n if (guard && guard.shouldRun()) {\n triggerMainSyncRefresh(workspaceRoot, DEFAULT_HANG_TIMEOUT_MINUTES);\n }\n}\n\n// Fault C (webpieces.config.json missing) — the deny text lives in ./l0-matrix beside the rest of the\n// L0 fault table, so the message and the allowlist can never prescribe different cures. `cwd` is used\n// only to drop the matrix doc where the AI can read it (the config root does not exist yet here).\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction configMissingBlock(cwd: string): BlockedResult {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n return new BlockedResult(CONFIG_MISSING_REPORT + guardMatrixPointer(writeGuardMatrixDoc(root)));\n}\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode = 'all',\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd, mode);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode,\n): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return configMissingBlock(cwd);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Always allow edits to webpieces.config.json — it's the fix target when out of sync\n if (path.resolve(input.filePath) === path.resolve(loaded.configPath)) {\n return null;\n }\n\n // Built-in/custom rules PLUS the client-authored match-rules (content guards). Match-rules run only\n // in the file-edit path (they are code-style, so filterByMode keeps them out of the bash/guards path).\n const allRules = [...loadRules(loaded.rulesConfig, workspaceRoot), ...loadMatchRules(loaded.matchRules)];\n const modeRules = filterByMode(allRules, mode);\n if (modeRules.length === 0) return null;\n\n // Suppress enforcement for files under this category's excludePaths (e.g. vendored repos under\n // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole\n // rule set and is fully hands-off — no violations AND no config-sync nag on those files.\n const relativePath = path.relative(workspaceRoot, input.filePath);\n const rules = filterByExcludedPaths(modeRules, relativePath, loaded.excludePaths);\n if (rules.length === 0) return null;\n\n // Config-sync applies only to built-in/custom rules; match-rules have their own validated section\n // (loadAndValidate already rejected an invalid `match-rules`), so they must not trip the sync nag.\n const outOfSync = checkConfigSync(rules.filter((r: Rule) => !(r instanceof MatchRule)), loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n\n const editGroups = runEditRules(rules, contexts.editContexts);\n const fileGroups = runFileRules(rules, contexts.fileContext);\n const allGroups = [...editGroups, ...fileGroups];\n\n if (allGroups.length === 0) return null;\n\n const report = formatReport(relativePath, allGroups);\n return new BlockedResult(report);\n}\n\nexport function runBash(command: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n return runBashInternal(command, cwd, mode);\n}\n\n// The name of the ONLY rule permitted to block a Read. Reads are the highest-blast-radius tool\n// there is, so this path is an explicit single-rule allowlist rather than the general rule loop.\nconst READ_SCOPED_GUARDS: ReadonlySet<string> = new Set(['read-stale-guard']);\n\n/**\n * The Read path. Deliberately NOT `run()`:\n *\n * - NO config-sync check. A rule present in code but missing from webpieces.config.json blocks\n * every Write/Edit/Bash by design — but applying that to Read would mean an upgrade that adds\n * any new rule instantly blocks the agent from reading the very config file it must edit to fix\n * it. Reads must never carry that failure mode.\n * - NO general rule loop. Only READ_SCOPED_GUARDS run, so no code-style rule can ever see a Read.\n * - Fails OPEN everywhere, including on a thrown rule (the caller catches and allows).\n *\n * Returns null (allow) unless the one guard fires.\n */\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runRead(filePath: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n // Code-style mode has nothing to say about a read.\n if (mode === 'rules') return null;\n\n const loaded = loadAndValidate(cwd);\n // No config → nothing to enforce. Unlike the edit path we do NOT block: an unconfigured repo\n // must still be readable.\n if (loaded.configPath === null) return null;\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Same git-repo-boundary governance as bash, through the SAME resolver: a read inside a different\n // clone is out of scope. (No command to parse here, so the shell cwd IS the effective cwd.)\n if (new EffectiveTreeResolver().resolve('', cwd, workspaceRoot).kind === 'foreign') return null;\n\n const relativePath = path.relative(workspaceRoot, filePath);\n const all = loadRules(loaded.rulesConfig, workspaceRoot);\n const rules = filterByExcludedPaths(\n all.filter((r: Rule): boolean => READ_SCOPED_GUARDS.has(r.name)),\n relativePath,\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const ctx = new FileContext('Read', filePath, relativePath, workspaceRoot, 0, 0, 0, 0);\n const groups = runFileRules(rules, ctx);\n if (groups.length === 0) return null;\n\n return new BlockedResult(formatReport(relativePath, groups, READ_SUBJECT));\n}\n\n// L0 cure bypass — every command on THE L0 allowlist passes here, ahead of any config load. A\n// webpieces.config.json that is ahead of the installed validator (new rule tokens the published\n// binary doesn't know yet) makes loadAndValidate() throw and would deny `pnpm install` — the very\n// command that updates the validator (deadlock).\n//\n// This used to test INSTALLER_ALLOW_JS alone, which made the config faults (C = config missing,\n// Y = config out of sync) accept a bare `pnpm install` while denying `rm -rf node_modules && pnpm\n// install` — the one cure that works when node_modules is CORRUPT rather than merely stale. Same\n// intent, opposite verdict, for no reason anyone recorded. L0_ALLOW_JS is the single list every L0\n// fault consults; see its block comment in ../bin/shim. Each alternative is still anchored at both\n// ends, so `pnpm install && rm -rf /` still falls to the guards.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction isL0CureCommand(command: string): boolean {\n return L0_ALLOW_JS.test(command.trim());\n}\n\n// Force-to-root: git/gh commands must run from the repo root of the tree they act on, where the guards\n// can reason about git state coherently.\n//\n// ONE variable decides it: `tree.effectiveCwd` — the directory the command actually runs in, which is\n// the shell's cwd unless the command leads with `cd <dir> &&`. Root or not-root, nothing else.\n//\n// It used to be `shellAtRoot || cdsToRoot`, two variables OR'd, and that produced opposite verdicts for\n// the same destination: `git status` with the shell in packages/http/ was BLOCKED, while\n// `cd packages/http && git status` from the root was ALLOWED, because shellAtRoot short-circuited\n// before the destination was ever considered. The point of this guard is to keep the agent's git work\n// at the root — an agent that cd's INTO a subdir has the same broken mental model as one that is\n// stranded there, so it gets the same answer now.\n//\n// The remedy is emitted as ONE runnable line, `cd <root> && <the original command>`, because `cd` does\n// not persist between tool calls: telling the agent to \"cd first, then re-run\" costs a turn and the\n// next call starts back in the old directory anyway. The bare-`cd` advice is what made this guard\n// print the very command it had just rejected.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction gitFromSubdirBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const targetAtRoot = path.resolve(tree.effectiveCwd) === path.resolve(tree.root);\n if (!isGitOrGhCommand(command) || targetAtRoot) return null;\n const report =\n `❌ Run git/gh commands from the repo root, not a subdirectory.\\n` +\n ` Command runs in: ${tree.effectiveCwd}\\n` +\n ` Judged against: ${tree.root}\\n` +\n ` Run EXACTLY this instead (one line — \\`cd\\` does NOT persist between tool calls):\\n` +\n ` ${atRoot(tree.root, command)}\\n` +\n ` A leading \\`cd <path> &&\\` is ACCEPTED by the guards — it cannot change what the command\\n` +\n ` does to the repo. (The webpieces guards evaluate the repo's git state at its root.)`;\n logGuardDecision(tree.root, new GuardDecision('force-to-root', 'Bash', command, branchForLog(tree.root), 'BLOCK', 'git/gh from subdir'));\n return new BlockedResult(report);\n}\n\n// The L0 cure bypass's audit line. Anchored at the repo root that owns `.webpieces` — RepoRootFinder\n// (config-walk-up first, then git toplevel) is the authority for that, and it is correct in a linked\n// worktree because each worktree checks out its own webpieces.config.json. This runs BEFORE\n// loadAndValidate, which is why it resolves the root itself rather than using workspaceRoot.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction logL0CureBypass(command: string, cwd: string): void {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n logGuardDecision(root, new GuardDecision('-', 'Bash', command, branchForLog(root), 'ALLOW', 'L0 cure bypass (always allowed)'));\n}\n\n/**\n * Load the config for the bash path — but do NOT let an unloadable config trap the tools needed to\n * repair it.\n *\n * loadAndValidate throws an InformAiError when webpieces.config.json is unparseable (a real syntax\n * error, or leftover `<<<<<<< HEAD` markers mid-merge) or fails validation. That throw propagates to\n * the hook adapter, which fails CLOSED and denies the command — correct for work, since a config that\n * did not load means no guards ran. But it denied `cat`/`grep`/`sed -n` on webpieces.config.json too,\n * i.e. it blocked the only way to see the problem it was reporting. Observed live, twice.\n *\n * So: on a load failure, a provably-inert INSPECTION command is allowed through (returns null, \"no\n * block\"), matching the escape hatch every other layer already grants this file. Everything else —\n * every write, every git/gh command, every build — still hits the same hard failure as before. The\n * bypass cannot be widened by accident; see ReadOnlyInspectionScan for how narrow \"inert\" is.\n *\n * Returns the loaded config, or null meaning \"allow this command without guards\".\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction loadConfigOrAllowInspection(command: string, cwd: string): LoadedConfig | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrown unchanged unless the command is provably inert\n try {\n return loadAndValidate(cwd);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof InformAiError && new ReadOnlyInspectionScan().isReadOnlyInspection(command)) {\n return null;\n }\n throw error;\n }\n}\n\nfunction runBashInternal(command: string, cwd: string, mode: HookMode): BlockedResult | null {\n if (isL0CureCommand(command)) {\n logL0CureBypass(command, cwd);\n return null;\n }\n\n const loaded = loadConfigOrAllowInspection(command, cwd);\n // null = the config would not load AND this command only inspects → allow, see the helper.\n if (loaded === null) return null;\n if (loaded.configPath === null) return configMissingBlock(cwd);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // WHICH TREE does this command act on? Not necessarily the shell's cwd — an agent working in a\n // linked worktree writes `cd <worktree> && …` because `cd` does not persist between tool calls.\n // ONE resolver answers this for the guards AND for force-to-root below, so the two can never\n // disagree about which tree you are in.\n const tree = new EffectiveTreeResolver().resolve(command, cwd, workspaceRoot);\n\n // Git-repo-boundary governance: the command runs inside a DIFFERENT git repo than this\n // webpieces.config governs (e.g. a clone under repositories/). Out of scope → allow, hands-off.\n // Intentional, not a silent hole. A LINKED WORKTREE of this repo is deliberately NOT foreign — it\n // is the same project, so the guards run against THAT tree's branch and cache.\n if (tree.kind === 'foreign') {\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW', 'foreign git repo (out of scope)'));\n return null;\n }\n\n // Honour excludePaths on the bash path too (not just Read/Edit): a command whose effective\n // cwd sits under an excluded tree (e.g. repositories/**) drops the whole guard set — matching how\n // runInternal/runRead treat file paths. The relative path is '' when there is no `cd` (root), which\n // matches no exclusion glob, so a plain command at the repo root is unaffected.\n const rules = filterByExcludedPaths(\n filterByMode(loadRules(loaded.rulesConfig, workspaceRoot), mode),\n path.relative(workspaceRoot, tree.effectiveCwd),\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const subdirBlock = gitFromSubdirBlock(command, tree);\n if (subdirBlock) return subdirBlock;\n\n // Keep the feature-branch-guard cache warm on EVERY command (not just Write/Edit): the AI runs\n // far more bash than edits, so refreshing here means the guard's next file-edit check reads a\n // fresh status. Detached + fire-and-forget — never blocks the command. Only when the guard is\n // loaded (guards/all mode) and enabled, so a project that opted out never triggers git fetches.\n // Keyed on the JUDGED tree, so a worktree's cache is refreshed rather than the primary clone's.\n maybeRefreshMainSync(rules, tree.root);\n\n const ctx = buildBashContext(command, tree);\n const groups = runBashRules(rules, ctx);\n if (groups.length === 0) {\n // Record the ALLOW only for git/gh commands — the operations the bash guards actually reason\n // about (branch create, commit, push, merge, PR). Skipping ls/cat/grep keeps the audit log\n // focused (the whole point of the log is \"why did/didn't a guard fire?\"). Blocks are always\n // logged below.\n if (/\\b(?:git|gh)\\b/.test(command)) {\n logGuardDecision(tree.root, new GuardDecision('-', 'Bash', command, branchForLog(tree.root), 'ALLOW', 'no bash-guard block'));\n }\n return null;\n }\n\n const ruleNames = groups.map((g: RuleGroup): string => g.ruleName).join(',');\n logGuardDecision(tree.root, new GuardDecision(ruleNames, 'Bash', command, branchForLog(tree.root), 'BLOCK', 'bash-guard block'));\n const report = formatReport(commandLabel(command), groups, BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths);\n return new BlockedResult(report);\n}\n\n// The bash report's subject line. It used to be the literal string `<bash>`, which told the agent\n// nothing; the command itself is what was blocked, so name it — truncated, because a heredoc-bearing\n// command can run to thousands of characters and the violation lines already carry the detail.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction commandLabel(command: string): string {\n const oneLine = command.replace(/\\s+/g, ' ').trim();\n const MAX = 100;\n return oneLine.length <= MAX ? oneLine : oneLine.slice(0, MAX) + '…';\n}\n\n// When a push/PR block fires AND the config exempts vendored/nested trees, surface the escape hatch the\n// AI cannot otherwise discover: git/gh run UNGUARDED inside those trees if it cd's there first (each is\n// governed by its own repo, not this one). Scoped to pr-creation-or-push-guard — for the other guards\n// \"cd into an exempt tree\" is not the remedy — and emitted only when such trees are actually configured,\n// so a repo without exemptions never sees the noise.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction exemptTreesHint(groups: readonly RuleGroup[], exemptGuards: readonly string[]): string {\n if (exemptGuards.length === 0) return '';\n if (!groups.some((g: RuleGroup): boolean => g.ruleName === 'pr-creation-or-push-guard')) return '';\n return `\\n\\nℹ️ Working in a nested repo under one of these exempt trees? cd into it first and run git/gh `\n + `normally there — the webpieces guards do NOT govern them (each is its own repo): ${exemptGuards.join(', ')}.`;\n}\n\n// The set of rule names explicitly present in webpieces.config.json (every key except rulesDir).\nfunction configuredRuleNames(config: WebpiecesRulesConfig): ReadonlySet<string> {\n return new Set(Object.keys(config).filter((k: string) => k !== 'rulesDir'));\n}\n\nfunction checkConfigSync(rules: readonly Rule[], config: WebpiecesRulesConfig): BlockedResult | null {\n const configured = configuredRuleNames(config);\n const unconfiguredRules = rules.filter((r: Rule) => !configured.has(r.name));\n if (unconfiguredRules.length === 0) return null;\n\n const lines = [\n // Fault Y's header lives in ./l0-matrix beside the rest of the L0 fault table (same reason as\n // CONFIG_MISSING_REPORT: one place states what this fault is and what cures it).\n CONFIG_OUT_OF_SYNC_HEADER,\n '',\n 'Tell the human: the following rules need to be configured. Ask for each one:',\n ' - Should this rule be ON, OFF, NEW_AND_MODIFIED_CODE, or NEW_AND_MODIFIED_FILES?',\n ' - What values do you want for the options listed below?',\n 'Then update webpieces.config.json and retry.',\n '',\n 'Do NOT proceed until webpieces.config.json has an entry for every rule below.',\n '',\n ];\n\n for (const rule of unconfiguredRules) {\n lines.push(`--- ${rule.name} ---`);\n lines.push(`Description: ${rule.description}`);\n const opts = rule.defaultOptions;\n const optKeys = Object.keys(opts);\n if (optKeys.length > 0) {\n lines.push(`Available options (suggested defaults shown):`);\n for (const key of optKeys) {\n lines.push(` ${key}: ${JSON.stringify(opts[key])}`);\n }\n } else {\n lines.push('Available options: none beyond mode');\n }\n lines.push(`Example entry for webpieces.config.json:`);\n lines.push(` \"${rule.name}\": { \"mode\": \"ON\" }`);\n lines.push('');\n }\n\n return new BlockedResult(lines.join('\\n'));\n}\n\n// N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may\n// EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees:\n// - a thrown RuleFailError → an expected, well-formed violation (its line/snippet/fixHints kept);\n// - a thrown plain Error → a \"crashed\" violation (a bug, surfaced not swallowed).\nexport function runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return rule.check(ctx);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) {\n return [violationFromRuleFail(error)];\n }\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\n// A thrown RuleFailError carries its own AI-facing message + optional location and fix hints. Fold the\n// fix hints into the message because Violation has no fixHint field (RuleGroup's fixHint comes from the\n// rule definition, not a per-throw value).\nfunction violationFromRuleFail(error: RuleFailError): Violation {\n const hints = error.fixHints.length > 0 ? `\\n Fix: ${error.fixHints.join('\\n Fix: ')}` : '';\n return new Violation(error.line ?? 0, error.snippet ?? '', error.aiMessage + hints);\n}\n\nfunction ruleMatchesFile(rule: Rule, relativePath: string): boolean {\n for (const pattern of rule.files) {\n if (globMatches(pattern, relativePath)) return true;\n }\n return false;\n}\n\nfunction runBashRules(rules: readonly Rule[], bashContext: BashContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'bash') continue;\n if (!rule.shouldRun()) continue;\n const vs = runRuleCheck(rule, bashContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n\nfunction runEditRules(rules: readonly Rule[], editContexts: readonly EditContext[]): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'edit') continue;\n if (!rule.shouldRun()) continue;\n const allViolations: Violation[] = [];\n for (const ctx of editContexts) {\n if (!ruleMatchesFile(rule, ctx.relativePath)) continue;\n const vs = runRuleCheck(rule, ctx);\n for (const v of vs) {\n const copy = new Violation(v.line, v.snippet, v.message);\n copy.editIndex = ctx.editIndex;\n copy.editCount = ctx.editCount;\n allViolations.push(copy);\n }\n }\n if (allViolations.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, allViolations,\n ));\n }\n }\n return groups;\n}\n\nfunction runFileRules(rules: readonly Rule[], fileContext: FileContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'file') continue;\n if (!rule.shouldRun()) continue;\n if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;\n const vs = runRuleCheck(rule, fileContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n"]}
|