@webpieces/ai-hook-rules 0.4.543 → 0.4.545

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.
Files changed (35) hide show
  1. package/package.json +2 -2
  2. package/src/bin/l0-allowlist.js +7 -4
  3. package/src/bin/l0-allowlist.js.map +1 -1
  4. package/src/bin/shim.js +2 -1
  5. package/src/bin/shim.js.map +1 -1
  6. package/src/core/decision-log.js +3 -1
  7. package/src/core/decision-log.js.map +1 -1
  8. package/src/core/effective-tree.d.ts +22 -7
  9. package/src/core/effective-tree.js +3 -2
  10. package/src/core/effective-tree.js.map +1 -1
  11. package/src/core/rules/branch-creation-guard.js +7 -3
  12. package/src/core/rules/branch-creation-guard.js.map +1 -1
  13. package/src/core/rules/content-read-scan.d.ts +1 -1
  14. package/src/core/rules/content-read-scan.js +1 -1
  15. package/src/core/rules/content-read-scan.js.map +1 -1
  16. package/src/core/rules/feature-branch-guard.js +5 -4
  17. package/src/core/rules/feature-branch-guard.js.map +1 -1
  18. package/src/core/rules/merged-branch-bash-guard.js +6 -5
  19. package/src/core/rules/merged-branch-bash-guard.js.map +1 -1
  20. package/src/core/rules/merged-branch-message.d.ts +3 -3
  21. package/src/core/rules/merged-branch-message.js +3 -3
  22. package/src/core/rules/merged-branch-message.js.map +1 -1
  23. package/src/core/rules/read-stale-guard.js +9 -5
  24. package/src/core/rules/read-stale-guard.js.map +1 -1
  25. package/src/core/rules/stale-main-bash-guard.js +4 -1
  26. package/src/core/rules/stale-main-bash-guard.js.map +1 -1
  27. package/src/core/rules/tree-recovery.d.ts +1 -1
  28. package/src/core/rules/tree-recovery.js +1 -1
  29. package/src/core/rules/tree-recovery.js.map +1 -1
  30. package/src/core/runner.js +9 -7
  31. package/src/core/runner.js.map +1 -1
  32. package/src/core/sync-main.d.ts +5 -0
  33. package/src/core/sync-main.js +32 -6
  34. package/src/core/sync-main.js.map +1 -1
  35. package/templates/ai-hook.sh +3 -3
@@ -183,10 +183,11 @@ function isL0CureCommand(command) {
183
183
  // at the root — an agent that cd's INTO a subdir has the same broken mental model as one that is
184
184
  // stranded there, so it gets the same answer now.
185
185
  //
186
- // The remedy is emitted as ONE runnable line, `cd <root> && <the original command>`, because `cd` does
187
- // not persist between tool calls: telling the agent to "cd first, then re-run" costs a turn and the
188
- // next call starts back in the old directory anyway. The bare-`cd` advice is what made this guard
189
- // print the very command it had just rejected.
186
+ // The remedy is emitted as ONE runnable line, `cd <root> && <the original command>`, rather than as
187
+ // "cd first, then re-run". That advice is what made this guard print the very command it had just
188
+ // rejected, and it is unreliable in both directions: a `cd` INTO this repo sticks (so the next call
189
+ // may start somewhere unexpected), while a `cd` OUT of it is reset by the harness (so a separate
190
+ // `cd <worktree>` call buys nothing). One self-contained line is correct either way.
190
191
  // 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
191
192
  function gitFromSubdirBlock(command, tree) {
192
193
  const targetAtRoot = path.resolve(tree.effectiveCwd) === path.resolve(tree.root);
@@ -195,7 +196,8 @@ function gitFromSubdirBlock(command, tree) {
195
196
  const report = `❌ Run git/gh commands from the repo root, not a subdirectory.\n` +
196
197
  ` Command runs in: ${tree.effectiveCwd}\n` +
197
198
  ` Judged against: ${tree.root}\n` +
198
- ` Run EXACTLY this instead (one line \`cd\` does NOT persist between tool calls):\n` +
199
+ ` Run EXACTLY this instead, as ONE line (a bare \`cd\` in a separate call is not equivalent —\n` +
200
+ ` a \`cd\` inside this repo STICKS for later calls, and a \`cd\` out of it is reset by the harness):\n` +
199
201
  ` ${(0, effective_tree_1.atRoot)(tree.root, command)}\n` +
200
202
  ` A leading \`cd <path> &&\` is ACCEPTED by the guards — it cannot change what the command\n` +
201
203
  ` does to the repo. (The webpieces guards evaluate the repo's git state at its root.)`;
@@ -254,8 +256,8 @@ function runBashInternal(command, cwd, mode) {
254
256
  if (loaded.configPath === null)
255
257
  return configMissingBlock(cwd);
256
258
  const workspaceRoot = path.dirname(loaded.configPath);
257
- // WHICH TREE does this command act on? Not necessarily the shell's cwd an agent working in a
258
- // linked worktree writes `cd <worktree> && …` because `cd` does not persist between tool calls.
259
+ // WHICH TREE does this command act on? Not the shell's cwd: a `cd` OUT of the workspace is reset
260
+ // by the harness and one INSIDE it persists, so neither can be assumed (see EffectiveTree).
259
261
  // ONE resolver answers this for the guards AND for force-to-root below, so the two can never
260
262
  // disagree about which tree you are in.
261
263
  const tree = new effective_tree_1.EffectiveTreeResolver().resolve(command, cwd, workspaceRoot);
@@ -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 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`.\n// This is L1's FILTER (not a table row) — see GUARD_MATRIX.md at the repo root.\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. L1 row 4 — see GUARD_MATRIX.md at the repo root for the table\n// and the use cases; change this predicate and that file is stale until you update it.\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"]}
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`.\n// This is L1's FILTER (not a table row) — see GUARD_MATRIX.md at the repo root.\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. L1 row 4 — see GUARD_MATRIX.md at the repo root for the table\n// and the use cases; change this predicate and that file is stale until you update it.\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>`, rather than as\n// \"cd first, then re-run\". That advice is what made this guard print the very command it had just\n// rejected, and it is unreliable in both directions: a `cd` INTO this repo sticks (so the next call\n// may start somewhere unexpected), while a `cd` OUT of it is reset by the harness (so a separate\n// `cd <worktree>` call buys nothing). One self-contained line is correct either way.\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, as ONE line (a bare \\`cd\\` in a separate call is not equivalent —\\n` +\n ` a \\`cd\\` inside this repo STICKS for later calls, and a \\`cd\\` out of it is reset by the harness):\\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 the shell's cwd: a `cd` OUT of the workspace is reset\n // by the harness and one INSIDE it persists, so neither can be assumed (see EffectiveTree).\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"]}
@@ -12,3 +12,8 @@
12
12
  * argv: [, , repoRoot, hangTimeoutMinutes]
13
13
  */
14
14
  export declare function main(): void;
15
+ /**
16
+ * The refresh itself, argv-free so it can be driven by a test. `main()` is only the argv parse — a
17
+ * test importing another module's `main` is what no-process-exit-outside-main exists to prevent.
18
+ */
19
+ export declare function refreshMainSync(repoRoot: string, hangTimeoutMinutes: number, argvDetail?: string): void;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.main = main;
4
+ exports.refreshMainSync = refreshMainSync;
4
5
  const rules_config_1 = require("@webpieces/rules-config");
5
6
  const to_error_1 = require("./to-error");
6
7
  const main_sync_log_1 = require("./main-sync-log");
@@ -18,12 +19,18 @@ const main_sync_log_1 = require("./main-sync-log");
18
19
  * argv: [, , repoRoot, hangTimeoutMinutes]
19
20
  */
20
21
  function main() {
21
- const repoRoot = process.argv[2] ?? process.cwd();
22
- const hangTimeoutMinutes = Number(process.argv[3]) || rules_config_1.DEFAULT_HANG_TIMEOUT_MINUTES;
22
+ refreshMainSync(process.argv[2] ?? process.cwd(), Number(process.argv[3]) || rules_config_1.DEFAULT_HANG_TIMEOUT_MINUTES, process.argv.slice(2).join(' '));
23
+ }
24
+ /**
25
+ * The refresh itself, argv-free so it can be driven by a test. `main()` is only the argv parse — a
26
+ * test importing another module's `main` is what no-process-exit-outside-main exists to prevent.
27
+ */
28
+ // webpieces-disable no-function-outside-class -- module-level entry point of this detached refresher, matching the file's existing shape
29
+ function refreshMainSync(repoRoot, hangTimeoutMinutes, argvDetail = '') {
23
30
  const startedMs = Date.now();
24
31
  // First action: prove the detached child actually started. If guard-async-work.log has no START line
25
32
  // for a spawn, the child never launched (or died before this point).
26
- (0, main_sync_log_1.logSyncEvent)(repoRoot, new main_sync_log_1.SyncLogEvent('START', process.pid, '-', `argv=${process.argv.slice(2).join(' ')}`));
33
+ (0, main_sync_log_1.logSyncEvent)(repoRoot, new main_sync_log_1.SyncLogEvent('START', process.pid, '-', `argv=${argvDetail}`));
27
34
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
28
35
  try {
29
36
  // ONE atomic acquire — not a check followed by a write. The old check-then-write pair had a
@@ -35,8 +42,12 @@ function main() {
35
42
  }
36
43
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
37
44
  try {
38
- const status = (0, rules_config_1.computeMainSyncStatus)(repoRoot);
39
- (0, rules_config_1.writeMainSyncStatus)(repoRoot, status);
45
+ // EVERY worktree's branch, not just ours. The cache is shared by the whole repo but used
46
+ // to describe only the branch of whichever tree won this lock, so every other worktree's
47
+ // guards read a cross-branch cache and abstained. One fetch + one `gh` call now arms all
48
+ // of them; the map is rebuilt from the live worktree set, so dead branches drop out.
49
+ const file = (0, rules_config_1.computeAllMainSyncStatuses)(repoRoot);
50
+ (0, rules_config_1.writeMainSyncStatusFile)(repoRoot, file);
40
51
  // Second slow signal, same lock, same detached run: which local branches are dead. One bulk
41
52
  // `gh pr list --state merged` call. The branch-creation-guard reads the result to enforce its
42
53
  // cap without ever touching the network itself. Deliberately allowed to go stale.
@@ -49,7 +60,8 @@ function main() {
49
60
  // to be asked. Reuses the verdicts we JUST computed (no second `gh` call).
50
61
  const reaped = autoReap(repoRoot, cache);
51
62
  // FINISH after a successful write — START-without-FINISH means we were killed mid-run.
52
- (0, main_sync_log_1.logSyncEvent)(repoRoot, new main_sync_log_1.SyncLogEvent('FINISH', process.pid, status.branch, `merged=${String(status.branchAlreadyMerged)} mergedPr=${status.mergedPr} forkPoint=${String(status.hasForkPoint)} conflict=${String(status.conflict)} deletableBranches=${String(cache.deletable.length)} reaped=${String(reaped)} ms=${String(Date.now() - startedMs)}`));
63
+ const names = Object.keys(file.branches);
64
+ (0, main_sync_log_1.logSyncEvent)(repoRoot, new main_sync_log_1.SyncLogEvent('FINISH', process.pid, names.join(','), `branches=${String(names.length)} ${names.map(summarize(file)).join(' ')} deletableBranches=${String(cache.deletable.length)} reaped=${String(reaped)} ms=${String(Date.now() - startedMs)}`));
53
65
  }
54
66
  finally {
55
67
  // Always flip the lock off so a compute failure can't wedge the guard until the
@@ -64,6 +76,20 @@ function main() {
64
76
  (0, main_sync_log_1.logSyncEvent)(repoRoot, new main_sync_log_1.SyncLogEvent('ERROR', process.pid, '-', `${error.message} | ${error.stack ?? ''}`));
65
77
  }
66
78
  }
79
+ /**
80
+ * One `branch(merged/forkPoint/conflict)` token per recorded branch, for the FINISH log line. The log
81
+ * is the only evidence that a given worktree's branch was actually armed by a refresh, so it has to
82
+ * name every branch written, not just the tree the refresher happened to run in.
83
+ */
84
+ // webpieces-disable no-function-outside-class -- module-level log formatter of this detached main(), matching the file's existing shape
85
+ function summarize(file) {
86
+ return (branch) => {
87
+ const status = file.branches[branch];
88
+ if (status === undefined)
89
+ return `${branch}(?)`;
90
+ return `${branch}(merged=${String(status.branchAlreadyMerged)}:${status.mergedPr} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)})`;
91
+ };
92
+ }
67
93
  /**
68
94
  * Delete the branches the verdicts just declared dead. Returns how many actually went.
69
95
  *
@@ -1 +1 @@
1
- {"version":3,"file":"sync-main.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/sync-main.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BranchReaper,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n MergedBranchesCache,\n MergedBranchesService,\n ReapResult,\n loadAndValidate,\n computeMainSyncStatus,\n writeMainSyncStatus,\n writeMainSyncLock,\n tryAcquireMainSyncLock,\n finishedLock,\n} from '@webpieces/rules-config';\n\nimport { toError } from './to-error';\nimport { logSyncEvent, SyncLogEvent } from './main-sync-log';\n\n/**\n * The detached, fire-and-forget refresher spawned (by file path, not a bin) from\n * main-sync-refresh.ts. It does the SLOW work (merged-PR lookup + git fetch + merge-base +\n * same-file-overlap) and writes `.webpieces/main-sync-status.json` so the next hook call reads it\n * instantly. Nobody reads our exit code or output — we run after the spawning hook has returned.\n *\n * Concurrency: a lock file (`.webpieces/main-sync.lock.json`) holds `inprocess`/`finished` + a start\n * epoch + the holder's pid, taken with an ATOMIC exclusive create. If another refresher already holds\n * it and is alive, we exit immediately (don't pile up `git fetch`es). If the holder is finished, dead,\n * or older than hangTimeoutMinutes, the lock is reclaimed and we proceed.\n *\n * argv: [, , repoRoot, hangTimeoutMinutes]\n */\nexport function main(): void {\n const repoRoot = process.argv[2] ?? process.cwd();\n const hangTimeoutMinutes = Number(process.argv[3]) || DEFAULT_HANG_TIMEOUT_MINUTES;\n const startedMs = Date.now();\n\n // First action: prove the detached child actually started. If guard-async-work.log has no START line\n // for a spawn, the child never launched (or died before this point).\n logSyncEvent(repoRoot, new SyncLogEvent('START', process.pid, '-', `argv=${process.argv.slice(2).join(' ')}`));\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // ONE atomic acquire — not a check followed by a write. The old check-then-write pair had a\n // gap in which two detached children could both pass the check and then both run `git fetch`.\n const lock = tryAcquireMainSyncLock(repoRoot, hangTimeoutMinutes);\n if (!lock) {\n logSyncEvent(repoRoot, new SyncLogEvent('SKIP_INPROGRESS', process.pid, '-', 'another refresh is in progress'));\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const status = computeMainSyncStatus(repoRoot);\n writeMainSyncStatus(repoRoot, status);\n\n // Second slow signal, same lock, same detached run: which local branches are dead. One bulk\n // `gh pr list --state merged` call. The branch-creation-guard reads the result to enforce its\n // cap without ever touching the network itself. Deliberately allowed to go stale.\n const mergedBranches = new MergedBranchesService();\n const cache = mergedBranches.computeMergedBranches(repoRoot);\n mergedBranches.writeMergedBranches(repoRoot, cache);\n\n // Third step, same detached run: actually DELETE the dead branches. Reporting them was\n // never enough — the reap was only ever a `git branch -D` string in a fix hint, which an\n // agent reads as destructive and stalls on, so nothing was ever cleaned. Here nobody has\n // to be asked. Reuses the verdicts we JUST computed (no second `gh` call).\n const reaped = autoReap(repoRoot, cache);\n\n // FINISH after a successful write — START-without-FINISH means we were killed mid-run.\n logSyncEvent(repoRoot, new SyncLogEvent(\n 'FINISH', process.pid, status.branch,\n `merged=${String(status.branchAlreadyMerged)} mergedPr=${status.mergedPr} forkPoint=${String(status.hasForkPoint)} conflict=${String(status.conflict)} deletableBranches=${String(cache.deletable.length)} reaped=${String(reaped)} ms=${String(Date.now() - startedMs)}`,\n ));\n } finally {\n // Always flip the lock off so a compute failure can't wedge the guard until the\n // staleness reclaim kicks in.\n writeMainSyncLock(repoRoot, finishedLock(lock.started));\n }\n } catch (err: unknown) {\n const error = toError(err);\n // Detached: swallow so a transient git/fs error never leaves poison state (the next hook call\n // spawns a fresh refresher) — but record WHY it died so the failure isn't invisible.\n logSyncEvent(repoRoot, new SyncLogEvent('ERROR', process.pid, '-', `${error.message} | ${error.stack ?? ''}`));\n }\n}\n\n/**\n * Delete the branches the verdicts just declared dead. Returns how many actually went.\n *\n * WHY it is safe to do this unattended: every candidate is provably dead (merged PR / squash backup\n * of a merged branch / zero commits of its own), `main` and any worktree-held branch are excluded\n * upstream, and each delete is logged with the branch's pre-delete SHA plus the exact command that\n * restores it. WHY it is safe to do it HERE: this refresher already recomputed those verdicts on\n * this very run, so it is acting on evidence seconds old, not on the deliberately-stale cache file.\n *\n * Swallows everything. We are detached and fire-and-forget: cleanup failing must never damage the\n * main-sync status this process exists to produce — but every failure is logged, because a silent\n * background deletion is exactly what nobody should have to trust.\n */\n// webpieces-disable no-function-outside-class -- module-level helper of this detached main(), matching the file's existing shape\nfunction autoReap(repoRoot: string, cache: MergedBranchesCache): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const config = loadAndValidate(repoRoot).rulesConfig['branch-creation-guard'];\n // Strictly opt-IN: only an explicit `true` reaps. `autoReapMergedBranches` is schema-required,\n // so every validated config states an answer — which means \"absent\" here is not a consumer\n // who wants the default, it is a config that never passed validation. Deleting branches on\n // that basis would be deleting on a preference nobody expressed.\n if (config?.mode === 'OFF' || config?.autoReapMergedBranches !== true) return 0;\n\n const result: ReapResult = new BranchReaper().reap(repoRoot, 'auto-reap', cache);\n for (const failure of result.failed) {\n logSyncEvent(repoRoot, new SyncLogEvent(\n 'ERROR', process.pid, failure.branch, `reap failed: ${failure.error}`));\n }\n return result.reaped.length;\n } catch (err: unknown) {\n const error = toError(err);\n logSyncEvent(repoRoot, new SyncLogEvent('ERROR', process.pid, '-', `autoReap: ${error.message}`));\n return 0;\n }\n}\n\nif (require.main === module) {\n main();\n}\n"]}
1
+ {"version":3,"file":"sync-main.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/sync-main.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BranchReaper,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n MainSyncStatusFile,\n MergedBranchesCache,\n MergedBranchesService,\n ReapResult,\n loadAndValidate,\n computeAllMainSyncStatuses,\n writeMainSyncStatusFile,\n writeMainSyncLock,\n tryAcquireMainSyncLock,\n finishedLock,\n} from '@webpieces/rules-config';\n\nimport { toError } from './to-error';\nimport { logSyncEvent, SyncLogEvent } from './main-sync-log';\n\n/**\n * The detached, fire-and-forget refresher spawned (by file path, not a bin) from\n * main-sync-refresh.ts. It does the SLOW work (merged-PR lookup + git fetch + merge-base +\n * same-file-overlap) and writes `.webpieces/main-sync-status.json` so the next hook call reads it\n * instantly. Nobody reads our exit code or output — we run after the spawning hook has returned.\n *\n * Concurrency: a lock file (`.webpieces/main-sync.lock.json`) holds `inprocess`/`finished` + a start\n * epoch + the holder's pid, taken with an ATOMIC exclusive create. If another refresher already holds\n * it and is alive, we exit immediately (don't pile up `git fetch`es). If the holder is finished, dead,\n * or older than hangTimeoutMinutes, the lock is reclaimed and we proceed.\n *\n * argv: [, , repoRoot, hangTimeoutMinutes]\n */\nexport function main(): void {\n refreshMainSync(\n process.argv[2] ?? process.cwd(),\n Number(process.argv[3]) || DEFAULT_HANG_TIMEOUT_MINUTES,\n process.argv.slice(2).join(' '),\n );\n}\n\n/**\n * The refresh itself, argv-free so it can be driven by a test. `main()` is only the argv parse — a\n * test importing another module's `main` is what no-process-exit-outside-main exists to prevent.\n */\n// webpieces-disable no-function-outside-class -- module-level entry point of this detached refresher, matching the file's existing shape\nexport function refreshMainSync(repoRoot: string, hangTimeoutMinutes: number, argvDetail: string = ''): void {\n const startedMs = Date.now();\n\n // First action: prove the detached child actually started. If guard-async-work.log has no START line\n // for a spawn, the child never launched (or died before this point).\n logSyncEvent(repoRoot, new SyncLogEvent('START', process.pid, '-', `argv=${argvDetail}`));\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // ONE atomic acquire — not a check followed by a write. The old check-then-write pair had a\n // gap in which two detached children could both pass the check and then both run `git fetch`.\n const lock = tryAcquireMainSyncLock(repoRoot, hangTimeoutMinutes);\n if (!lock) {\n logSyncEvent(repoRoot, new SyncLogEvent('SKIP_INPROGRESS', process.pid, '-', 'another refresh is in progress'));\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // EVERY worktree's branch, not just ours. The cache is shared by the whole repo but used\n // to describe only the branch of whichever tree won this lock, so every other worktree's\n // guards read a cross-branch cache and abstained. One fetch + one `gh` call now arms all\n // of them; the map is rebuilt from the live worktree set, so dead branches drop out.\n const file = computeAllMainSyncStatuses(repoRoot);\n writeMainSyncStatusFile(repoRoot, file);\n\n // Second slow signal, same lock, same detached run: which local branches are dead. One bulk\n // `gh pr list --state merged` call. The branch-creation-guard reads the result to enforce its\n // cap without ever touching the network itself. Deliberately allowed to go stale.\n const mergedBranches = new MergedBranchesService();\n const cache = mergedBranches.computeMergedBranches(repoRoot);\n mergedBranches.writeMergedBranches(repoRoot, cache);\n\n // Third step, same detached run: actually DELETE the dead branches. Reporting them was\n // never enough — the reap was only ever a `git branch -D` string in a fix hint, which an\n // agent reads as destructive and stalls on, so nothing was ever cleaned. Here nobody has\n // to be asked. Reuses the verdicts we JUST computed (no second `gh` call).\n const reaped = autoReap(repoRoot, cache);\n\n // FINISH after a successful write — START-without-FINISH means we were killed mid-run.\n const names = Object.keys(file.branches);\n logSyncEvent(repoRoot, new SyncLogEvent(\n 'FINISH', process.pid, names.join(','),\n `branches=${String(names.length)} ${names.map(summarize(file)).join(' ')} deletableBranches=${String(cache.deletable.length)} reaped=${String(reaped)} ms=${String(Date.now() - startedMs)}`,\n ));\n } finally {\n // Always flip the lock off so a compute failure can't wedge the guard until the\n // staleness reclaim kicks in.\n writeMainSyncLock(repoRoot, finishedLock(lock.started));\n }\n } catch (err: unknown) {\n const error = toError(err);\n // Detached: swallow so a transient git/fs error never leaves poison state (the next hook call\n // spawns a fresh refresher) — but record WHY it died so the failure isn't invisible.\n logSyncEvent(repoRoot, new SyncLogEvent('ERROR', process.pid, '-', `${error.message} | ${error.stack ?? ''}`));\n }\n}\n\n/**\n * One `branch(merged/forkPoint/conflict)` token per recorded branch, for the FINISH log line. The log\n * is the only evidence that a given worktree's branch was actually armed by a refresh, so it has to\n * name every branch written, not just the tree the refresher happened to run in.\n */\n// webpieces-disable no-function-outside-class -- module-level log formatter of this detached main(), matching the file's existing shape\nfunction summarize(file: MainSyncStatusFile): (branch: string) => string {\n return (branch: string): string => {\n const status = file.branches[branch];\n if (status === undefined) return `${branch}(?)`;\n return `${branch}(merged=${String(status.branchAlreadyMerged)}:${status.mergedPr} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)})`;\n };\n}\n\n/**\n * Delete the branches the verdicts just declared dead. Returns how many actually went.\n *\n * WHY it is safe to do this unattended: every candidate is provably dead (merged PR / squash backup\n * of a merged branch / zero commits of its own), `main` and any worktree-held branch are excluded\n * upstream, and each delete is logged with the branch's pre-delete SHA plus the exact command that\n * restores it. WHY it is safe to do it HERE: this refresher already recomputed those verdicts on\n * this very run, so it is acting on evidence seconds old, not on the deliberately-stale cache file.\n *\n * Swallows everything. We are detached and fire-and-forget: cleanup failing must never damage the\n * main-sync status this process exists to produce — but every failure is logged, because a silent\n * background deletion is exactly what nobody should have to trust.\n */\n// webpieces-disable no-function-outside-class -- module-level helper of this detached main(), matching the file's existing shape\nfunction autoReap(repoRoot: string, cache: MergedBranchesCache): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const config = loadAndValidate(repoRoot).rulesConfig['branch-creation-guard'];\n // Strictly opt-IN: only an explicit `true` reaps. `autoReapMergedBranches` is schema-required,\n // so every validated config states an answer — which means \"absent\" here is not a consumer\n // who wants the default, it is a config that never passed validation. Deleting branches on\n // that basis would be deleting on a preference nobody expressed.\n if (config?.mode === 'OFF' || config?.autoReapMergedBranches !== true) return 0;\n\n const result: ReapResult = new BranchReaper().reap(repoRoot, 'auto-reap', cache);\n for (const failure of result.failed) {\n logSyncEvent(repoRoot, new SyncLogEvent(\n 'ERROR', process.pid, failure.branch, `reap failed: ${failure.error}`));\n }\n return result.reaped.length;\n } catch (err: unknown) {\n const error = toError(err);\n logSyncEvent(repoRoot, new SyncLogEvent('ERROR', process.pid, '-', `autoReap: ${error.message}`));\n return 0;\n }\n}\n\nif (require.main === module) {\n main();\n}\n"]}
@@ -148,7 +148,7 @@ if [ -n "$BROKEN_BIN" ]; then
148
148
  if [ "${STAGING_N:-0}" -gt 0 ] 2>/dev/null; then
149
149
  STAGING_NOTE=" Also found $STAGING_N orphaned pnpm staging dirs (name_pid_hash) under node_modules - the fingerprint of an install that was killed mid-write."
150
150
  fi
151
- REASON="❌ webpieces guards are DOWN and every OTHER tool call is BLOCKED: ${BIN_NAME} is installed but CRASHED ($CRASH_MSG). Your node_modules is corrupt or partially written, so the guards cannot run - and they must NOT be silently skipped. NOTE: a plain 'pnpm install' will NOT fix this; pnpm sees the correct version on disk and skips the broken package. THIS IS NOT A DEADLOCK: the option below is explicitly ALLOWED through while this guard is up, so run it YOURSELF rather than handing it to the human. OPTION 1 - run EXACTLY this command, then retry: 'rm -rf node_modules && pnpm install'. Type the option you pick EXACTLY as written, character for character, and run NOTHING else on that line. Seriously: do NOT append && anything (not even a harmless && git status), do NOT wrap it in a subshell. The allowlist is anchored to the ENTIRE command, so anything you bolt on makes it a DIFFERENT command and it WILL be rejected again - which is not the guard refusing its own cure. If an option already contains &&, that && is part of the command: keep it, and still add nothing beyond it. The only additions tolerated are a LEADING cd <dir> && (needed to run the cure in a linked worktree, since cd does not persist between tool calls - that one IS accepted), a trailing 2>&1, and a pipe into tail/head (e.g. cd /path/to/worktree && pnpm install 2>&1 | tail -20).${STAGING_NOTE}"
151
+ REASON="❌ webpieces guards are DOWN and every OTHER tool call is BLOCKED: ${BIN_NAME} is installed but CRASHED ($CRASH_MSG). Your node_modules is corrupt or partially written, so the guards cannot run - and they must NOT be silently skipped. NOTE: a plain 'pnpm install' will NOT fix this; pnpm sees the correct version on disk and skips the broken package. THIS IS NOT A DEADLOCK: the option below is explicitly ALLOWED through while this guard is up, so run it YOURSELF rather than handing it to the human. OPTION 1 - run EXACTLY this command, then retry: 'rm -rf node_modules && pnpm install'. Type the option you pick EXACTLY as written, character for character, and run NOTHING else on that line. Seriously: do NOT append && anything (not even a harmless && git status), do NOT wrap it in a subshell. The allowlist is anchored to the ENTIRE command, so anything you bolt on makes it a DIFFERENT command and it WILL be rejected again - which is not the guard refusing its own cure. If an option already contains &&, that && is part of the command: keep it, and still add nothing beyond it. The only additions tolerated are a LEADING cd <dir> && (needed to run the cure in a linked worktree, since the harness resets a cwd that left the workspace and puts you back in the primary clone - that one IS accepted), a trailing 2>&1, and a pipe into tail/head (e.g. cd /path/to/worktree && pnpm install 2>&1 | tail -20).${STAGING_NOTE}"
152
152
  elif [ -n "$DRIFT_PKG" ]; then
153
153
  # The 'how do I get current' half comes from SyncFlowGuidance so it cannot contradict the guards.
154
154
  # It used to name 'git merge --ff-only origin/main' and assert that merge is allowed while this guard
@@ -161,7 +161,7 @@ elif [ -n "$DRIFT_PKG" ]; then
161
161
  # check is a plain !=, so it fires BOTH ways, and the old text always claimed node_modules was the
162
162
  # older side. When it is actually the NEWER side (a checkout behind origin), that text sent people
163
163
  # to 'pnpm install', which DOWNGRADES them further from correct.
164
- REASON="❌ webpieces version drift: package.json pins $DRIFT_PKG@$DRIFT_DECLARED but node_modules has $DRIFT_INSTALLED. Every OTHER call is blocked until they agree. WHICH ONE IS STALE decides which option is yours - compare the two versions above. OPTION 1 (the pin is NEWER than node_modules - you just pulled or switched to a branch pinning a newer webpieces) - run EXACTLY this command to catch node_modules up: 'pnpm install'. OPTION 2 (the pin is OLDER than node_modules - your checkout is behind origin, so the PIN is the stale side, and 'pnpm install' on its own would DOWNGRADE you) - get the checkout current FIRST, THEN run 'pnpm install'. To get main itself current: ON main, run 'git pull origin main'. In a linked worktree (main is checked out in the primary clone, so checkout main fatals there), run 'git fetch origin main' and branch off origin/main. Do NOT reach for git merge --ff-only / git reset --hard / git checkout -B main: merge and rebase are blocked in EVERY form by redirect-how-to-merge-main, and the reset/-B forms silently throw away commits. To sync a FEATURE branch from main use pnpm wp-start-update (no PR open) or pnpm wp-start-upsert-pr (a PR is open). git pull and git fetch are allowed while this guard is up and are the cure here. git merge is NOT allowed - not by this guard and not by redirect-how-to-merge-main once the guards are back - because main is merged ONLY through the 3-point fork merge: 'pnpm wp-start-update', or 'pnpm wp-start-upsert-pr' when a PR is already open. Type the option you pick EXACTLY as written, character for character, and run NOTHING else on that line. Seriously: do NOT append && anything (not even a harmless && git status), do NOT wrap it in a subshell. The allowlist is anchored to the ENTIRE command, so anything you bolt on makes it a DIFFERENT command and it WILL be rejected again - which is not the guard refusing its own cure. If an option already contains &&, that && is part of the command: keep it, and still add nothing beyond it. The only additions tolerated are a LEADING cd <dir> && (needed to run the cure in a linked worktree, since cd does not persist between tool calls - that one IS accepted), a trailing 2>&1, and a pipe into tail/head (e.g. cd /path/to/worktree && pnpm install 2>&1 | tail -20)."
164
+ REASON="❌ webpieces version drift: package.json pins $DRIFT_PKG@$DRIFT_DECLARED but node_modules has $DRIFT_INSTALLED. Every OTHER call is blocked until they agree. WHICH ONE IS STALE decides which option is yours - compare the two versions above. OPTION 1 (the pin is NEWER than node_modules - you just pulled or switched to a branch pinning a newer webpieces) - run EXACTLY this command to catch node_modules up: 'pnpm install'. OPTION 2 (the pin is OLDER than node_modules - your checkout is behind origin, so the PIN is the stale side, and 'pnpm install' on its own would DOWNGRADE you) - get the checkout current FIRST, THEN run 'pnpm install'. To get main itself current: ON main, run 'git pull origin main'. In a linked worktree (main is checked out in the primary clone, so checkout main fatals there), run 'git fetch origin main' and branch off origin/main. Do NOT reach for git merge --ff-only / git reset --hard / git checkout -B main: merge and rebase are blocked in EVERY form by redirect-how-to-merge-main, and the reset/-B forms silently throw away commits. To sync a FEATURE branch from main use pnpm wp-start-update (no PR open) or pnpm wp-start-upsert-pr (a PR is open). git pull and git fetch are allowed while this guard is up and are the cure here. git merge is NOT allowed - not by this guard and not by redirect-how-to-merge-main once the guards are back - because main is merged ONLY through the 3-point fork merge: 'pnpm wp-start-update', or 'pnpm wp-start-upsert-pr' when a PR is already open. Type the option you pick EXACTLY as written, character for character, and run NOTHING else on that line. Seriously: do NOT append && anything (not even a harmless && git status), do NOT wrap it in a subshell. The allowlist is anchored to the ENTIRE command, so anything you bolt on makes it a DIFFERENT command and it WILL be rejected again - which is not the guard refusing its own cure. If an option already contains &&, that && is part of the command: keep it, and still add nothing beyond it. The only additions tolerated are a LEADING cd <dir> && (needed to run the cure in a linked worktree, since the harness resets a cwd that left the workspace and puts you back in the primary clone - that one IS accepted), a trailing 2>&1, and a pipe into tail/head (e.g. cd /path/to/worktree && pnpm install 2>&1 | tail -20)."
165
165
  else
166
166
  # A LINKED WORKTREE is the overwhelmingly common way to land here with a perfectly healthy repo:
167
167
  # git gives the new worktree a .git FILE (the primary clone has a .git directory) and copies no
@@ -172,7 +172,7 @@ else
172
172
  if [ -f "$ROOT/.git" ]; then
173
173
  WORKTREE_NOTE=" NOTE: $ROOT is a LINKED WORKTREE - git does not copy node_modules into a new worktree, so this is expected on a fresh one. Run 'pnpm install' HERE (in this worktree), not in the primary clone."
174
174
  fi
175
- REASON="❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (${BIN_NAME} not found). OPTION 1 - run EXACTLY this command to enable the webpieces AI guards, then retry: 'pnpm install'. Type the option you pick EXACTLY as written, character for character, and run NOTHING else on that line. Seriously: do NOT append && anything (not even a harmless && git status), do NOT wrap it in a subshell. The allowlist is anchored to the ENTIRE command, so anything you bolt on makes it a DIFFERENT command and it WILL be rejected again - which is not the guard refusing its own cure. If an option already contains &&, that && is part of the command: keep it, and still add nothing beyond it. The only additions tolerated are a LEADING cd <dir> && (needed to run the cure in a linked worktree, since cd does not persist between tool calls - that one IS accepted), a trailing 2>&1, and a pipe into tail/head (e.g. cd /path/to/worktree && pnpm install 2>&1 | tail -20).${WORKTREE_NOTE} (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)"
175
+ REASON="❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (${BIN_NAME} not found). OPTION 1 - run EXACTLY this command to enable the webpieces AI guards, then retry: 'pnpm install'. Type the option you pick EXACTLY as written, character for character, and run NOTHING else on that line. Seriously: do NOT append && anything (not even a harmless && git status), do NOT wrap it in a subshell. The allowlist is anchored to the ENTIRE command, so anything you bolt on makes it a DIFFERENT command and it WILL be rejected again - which is not the guard refusing its own cure. If an option already contains &&, that && is part of the command: keep it, and still add nothing beyond it. The only additions tolerated are a LEADING cd <dir> && (needed to run the cure in a linked worktree, since the harness resets a cwd that left the workspace and puts you back in the primary clone - that one IS accepted), a trailing 2>&1, and a pipe into tail/head (e.g. cd /path/to/worktree && pnpm install 2>&1 | tail -20).${WORKTREE_NOTE} (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)"
176
176
  fi
177
177
  if [ "$TOOL" = "Bash" ]; then
178
178
  BS='\' # one literal backslash, so the \u001b escape never sits in this source