@webpieces/ai-hook-rules 0.4.757 → 0.4.758
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/ai-hook-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.758",
|
|
4
4
|
"description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/tooling/ai-hook-rules"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
28
|
+
"@webpieces/rules-config": "0.4.758",
|
|
29
29
|
"inversify": "7.10.4",
|
|
30
30
|
"reflect-metadata": "0.2.2"
|
|
31
31
|
},
|
package/src/core/load-rules.d.ts
CHANGED
|
@@ -30,8 +30,8 @@ export declare function loadRules(config: WebpiecesRulesConfig, workspaceRoot: s
|
|
|
30
30
|
* for every input, and it cannot itself match the guard.
|
|
31
31
|
* - `wait-spin-guard` acts unconditionally, on the same test again. An `echo .` keep-alive spends a
|
|
32
32
|
* whole turn (~557k tokens) to do nothing, and its cure — `pnpm wp-await-reviews` /
|
|
33
|
-
* `pnpm wp-await-checks` for a worktree subagent, a Monitor
|
|
34
|
-
* is available for every input and can never itself match the guard.
|
|
33
|
+
* `pnpm wp-await-checks` for a worktree subagent, a Monitor or the backgrounded command itself for a
|
|
34
|
+
* main agent — is available for every input and can never itself match the guard.
|
|
35
35
|
*
|
|
36
36
|
* `affectedBuildCommand` is the project's gate command, passed through so a refusal quotes what THIS
|
|
37
37
|
* repo's gate actually runs.
|
package/src/core/load-rules.js
CHANGED
|
@@ -117,8 +117,8 @@ function loadRules(config, workspaceRoot, guardHints) {
|
|
|
117
117
|
* for every input, and it cannot itself match the guard.
|
|
118
118
|
* - `wait-spin-guard` acts unconditionally, on the same test again. An `echo .` keep-alive spends a
|
|
119
119
|
* whole turn (~557k tokens) to do nothing, and its cure — `pnpm wp-await-reviews` /
|
|
120
|
-
* `pnpm wp-await-checks` for a worktree subagent, a Monitor
|
|
121
|
-
* is available for every input and can never itself match the guard.
|
|
120
|
+
* `pnpm wp-await-checks` for a worktree subagent, a Monitor or the backgrounded command itself for a
|
|
121
|
+
* main agent — is available for every input and can never itself match the guard.
|
|
122
122
|
*
|
|
123
123
|
* `affectedBuildCommand` is the project's gate command, passed through so a refusal quotes what THIS
|
|
124
124
|
* repo's gate actually runs.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-rules.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/load-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport {\n BaseRuleConfig, RuleOptions, WebpiecesRulesConfig,\n NoAnyUnknownConfig, NoImplicitAnyConfig, MaxFileLinesConfig, ValidateTsInSrcConfig,\n NoDestructureConfig, RequireReturnTypeConfig, NoUnmanagedExceptionsConfig,\n CatchErrorPatternConfig, ThrowCauseRequiredConfig,\n NoSymbolDiTokensConfig, NoCustomCssConfig, NoProcessExitOutsideMainConfig, BranchCreationGuardConfig,\n PrLifecycleGuardConfig, BranchStateGuardConfig,\n NoJsFilesConfig, MatchRuleConfig,\n} from '@webpieces/rules-config';\n\nimport type { Rule, PlainRule } from './types';\nimport { InformAiError } from './types';\nimport { toError } from './to-error';\nimport { EmptyRuleConfig } from './rule-base';\nimport { CustomRuleAdapter } from './custom-rule-adapter';\nimport { builtInConfigKeys } from './rules/index';\nimport { NoAnyUnknownRule } from './rules/no-any-unknown';\nimport { NoImplicitAnyRule } from './rules/no-implicit-any';\nimport { MaxFileLinesRule } from './rules/max-file-lines';\nimport { ValidateTsInSrcRule } from './rules/validate-ts-in-src';\nimport { NoDestructureRule } from './rules/no-destructure';\nimport { RequireReturnTypeRule } from './rules/require-return-type';\nimport { NoUnmanagedExceptionsRule } from './rules/no-unmanaged-exceptions';\nimport { CatchErrorPatternRule } from './rules/catch-error-pattern';\nimport { ThrowCauseRequiredRule } from './rules/throw-cause-required';\nimport { NoSymbolDiTokensRule } from './rules/no-symbol-di-tokens';\nimport { NoCustomCssRule } from './rules/no-custom-css';\nimport { NoProcessExitOutsideMainRule } from './rules/no-process-exit-outside-main';\nimport { BranchCreationGuardRule } from './rules/branch-creation-guard';\nimport { PrCreationOrPushGuardRule } from './rules/pr-creation-or-push-guard';\nimport { MergeInProgressGuardRule } from './rules/merge-in-progress-guard';\nimport { BuildOutputPipeGuardRule } from './rules/build-output-pipe-guard';\nimport { WaitSpinGuardRule } from './rules/wait-spin-guard';\nimport { PrMergeGuardRule } from './rules/pr-merge-guard';\nimport { RedirectHowToMergeMainRule } from './rules/redirect-how-to-merge-main';\nimport { NoJsFilesRule } from './rules/no-js-files';\nimport { FeatureBranchGuardRule } from './rules/feature-branch-guard';\nimport { ReadStaleGuardRule } from './rules/read-stale-guard';\nimport { MergedBranchBashGuardRule } from './rules/merged-branch-bash-guard';\nimport { StaleMainBashGuardRule } from './rules/stale-main-bash-guard';\nimport { WholeRepoBuildGuardRule } from './rules/whole-repo-build-guard';\nimport { CommitMessageSubstitutionGuardRule } from './rules/commit-message-substitution-guard';\nimport { MatchRule } from './rules/match-rule';\n\nconst REQUIRED_FIELDS: readonly string[] = ['name', 'description', 'scope', 'files', 'check'];\nconst VALID_SCOPES = new Set(['edit', 'file', 'bash']);\n\n/**\n * ONE CONFIG KEY → N RULES.\n *\n * Each built-in rule is constructed from its typed *Config (the entry in webpieces.config.json). The\n * config arrives as a plain object structurally typed as the *Config class, so the `as` narrows the\n * shared BaseRuleConfig param back to the concrete config the rule consumes.\n *\n * The map is keyed by CONFIG KEY and each factory returns an ARRAY, because a hookGuards key names a\n * POLICY and a policy may be implemented by several classes: `branch-state-guard` builds all four\n * branch-state guards from one entry, `pr-lifecycle-guard` all four PR-lifecycle guards. It used to be\n * `Record<string, (c) => Rule>` — one factory per key — which is precisely why four classes could not\n * share a key and why the config had to carry nine switches for three decisions.\n *\n * `guardHints` are the resolved `commands.guardHints` strings, handed to the two rules that print a\n * gated command. They arrive as a constructor argument rather than a config field, so there is exactly\n * one spelling of each command in the config (see PrLifecycleGuardConfig).\n */\ntype RuleFactory = (config: BaseRuleConfig, guardHints: GuardHintCommands) => readonly Rule[];\n\n/** The two gated-command strings guards print, resolved from `commands.guardHints`. Data-only. */\nexport class GuardHintCommands {\n constructor(readonly upsertPr: string, readonly mergeComplete: string) {}\n}\n\nconst BUILT_IN_RULE_MAP: Record<string, RuleFactory> = {\n 'no-any-unknown': (c: BaseRuleConfig) => [new NoAnyUnknownRule(c as NoAnyUnknownConfig)],\n 'no-implicit-any': (c: BaseRuleConfig) => [new NoImplicitAnyRule(c as NoImplicitAnyConfig)],\n 'max-file-lines': (c: BaseRuleConfig) => [new MaxFileLinesRule(c as MaxFileLinesConfig)],\n 'validate-ts-in-src': (c: BaseRuleConfig) => [new ValidateTsInSrcRule(c as ValidateTsInSrcConfig)],\n 'no-destructure': (c: BaseRuleConfig) => [new NoDestructureRule(c as NoDestructureConfig)],\n 'require-return-type': (c: BaseRuleConfig) => [new RequireReturnTypeRule(c as RequireReturnTypeConfig)],\n 'no-unmanaged-exceptions': (c: BaseRuleConfig) => [new NoUnmanagedExceptionsRule(c as NoUnmanagedExceptionsConfig)],\n 'catch-error-pattern': (c: BaseRuleConfig) => [new CatchErrorPatternRule(c as CatchErrorPatternConfig)],\n 'throw-cause-required': (c: BaseRuleConfig) => [new ThrowCauseRequiredRule(c as ThrowCauseRequiredConfig)],\n 'no-symbol-di-tokens': (c: BaseRuleConfig) => [new NoSymbolDiTokensRule(c as NoSymbolDiTokensConfig)],\n 'no-custom-css': (c: BaseRuleConfig) => [new NoCustomCssRule(c as NoCustomCssConfig)],\n 'no-process-exit-outside-main': (c: BaseRuleConfig) => [new NoProcessExitOutsideMainRule(c as NoProcessExitOutsideMainConfig)],\n 'no-js-files': (c: BaseRuleConfig) => [new NoJsFilesRule(c as NoJsFilesConfig)],\n 'branch-creation-guard': (c: BaseRuleConfig) => [new BranchCreationGuardRule(c as BranchCreationGuardConfig)],\n // THE TWO COLLAPSED POLICIES. Order inside each array is the order the rules run in, and it is the\n // same order the previous per-key registry produced.\n 'pr-lifecycle-guard': (c: BaseRuleConfig, hints: GuardHintCommands) => [\n new PrCreationOrPushGuardRule(c as PrLifecycleGuardConfig, hints.upsertPr),\n new MergeInProgressGuardRule(c as PrLifecycleGuardConfig, hints.mergeComplete),\n new PrMergeGuardRule(c as PrLifecycleGuardConfig),\n new RedirectHowToMergeMainRule(c as PrLifecycleGuardConfig),\n ],\n 'branch-state-guard': (c: BaseRuleConfig) => [\n new FeatureBranchGuardRule(c as BranchStateGuardConfig),\n new ReadStaleGuardRule(c as BranchStateGuardConfig),\n new MergedBranchBashGuardRule(c as BranchStateGuardConfig),\n new StaleMainBashGuardRule(c as BranchStateGuardConfig),\n ],\n};\n\n// Index the typed config by rule name. Each value is the rule's *Config (a plain object from\n// JSON), or undefined when the rule has no entry yet (the sync check reports those).\nfunction asConfigMap(config: WebpiecesRulesConfig): Record<string, BaseRuleConfig | undefined> {\n // webpieces-disable no-any-unknown -- index the typed config by dynamic rule name\n return config as unknown as Record<string, BaseRuleConfig | undefined>;\n}\n\n// webpieces-disable no-function-outside-class -- the module's entry point, beside loadMatchRules/loadKeylessBashRules; this whole loader is module-scope functions and a lone class for one of them would break the file's shape\nexport function loadRules(\n config: WebpiecesRulesConfig,\n workspaceRoot: string,\n guardHints: GuardHintCommands,\n): readonly Rule[] {\n const builtIns = loadBuiltInRules(config, guardHints);\n const custom = loadCustomRules(config, workspaceRoot);\n return [...builtIns, ...custom];\n}\n\n/**\n * The KEYLESS bash guards: rules that have NO webpieces.config.json entry, and are therefore\n * deliberately kept out of `builtInConfigKeys`/`BUILT_IN_RULE_MAP` — so the config-sync check (fault Y,\n * \"every built-in rule needs an entry, or every Bash call is blocked\") can never see them. That\n * containment is the whole point: whole-repo-build-guard shipped inside the config-driven set once and\n * took every upgrading consumer's shell down with it.\n *\n * Each rule here decides for ITSELF whether it acts, and the two do it differently on purpose:\n *\n * - `whole-repo-build-guard` is EXPERIMENTAL and inert unless the optional machine-local\n * `~/.webpieces/config.json` opts IN with `experimental.whole-repo-build-guard: true`. Every\n * experimental flag defaults OFF, and it takes no file and no key to be in that default state —\n * which is the difference between this and the required-key release that blocked every upgrading\n * consumer's shell.\n * - `commit-message-substitution-guard` acts unconditionally. Nobody legitimately wants a backtick\n * expanded inside a commit message, and its cure (`git commit -F <file>`) is available for every\n * input and can never itself match the guard — so there is nothing for a switch to rescue.\n * - `build-output-pipe-guard` acts unconditionally, on the same test. Piping `wp-build` /\n * `wp-review-upsert-pr` / `wp-finish-upsert-pr` withholds their heartbeat until they exit and gets\n * the build killed by the 600s watchdog; the cure is the SAME command with less typing, available\n * for every input, and it cannot itself match the guard.\n * - `wait-spin-guard` acts unconditionally, on the same test again. An `echo .` keep-alive spends a\n * whole turn (~557k tokens) to do nothing, and its cure — `pnpm wp-await-reviews` /\n * `pnpm wp-await-checks` for a worktree subagent, a Monitor plus ending the turn for a main agent —\n * is available for every input and can never itself match the guard.\n *\n * `affectedBuildCommand` is the project's gate command, passed through so a refusal quotes what THIS\n * repo's gate actually runs.\n */\n// webpieces-disable no-function-outside-class -- sibling of loadRules/loadMatchRules in this module; the whole loader is module-scope functions and a lone class for this one would break the file's shape\nexport function loadKeylessBashRules(affectedBuildCommand: string): Rule[] {\n return [\n new WholeRepoBuildGuardRule(affectedBuildCommand),\n new CommitMessageSubstitutionGuardRule(),\n new BuildOutputPipeGuardRule(),\n new WaitSpinGuardRule(),\n ];\n}\n\n// One MatchRule per entry of the `match-rules` array. Kept separate from loadRules (built-ins/custom)\n// because match-rules live in their own validated section — they must NOT flow through the\n// config-sync check, which compares rule names against the `rules`/`hookGuards` map.\nexport function loadMatchRules(matchRules: readonly MatchRuleConfig[]): Rule[] {\n return matchRules.map((c: MatchRuleConfig) => new MatchRule(c));\n}\n\n// Iterates CONFIG KEYS, not rule names — one entry can yield several rules (see BUILT_IN_RULE_MAP).\n// webpieces-disable no-function-outside-class -- the body of loadRules above, in the same module of loader functions\nfunction loadBuiltInRules(config: WebpiecesRulesConfig, guardHints: GuardHintCommands): Rule[] {\n const map = asConfigMap(config);\n const rules: Rule[] = [];\n for (const configKey of builtInConfigKeys) {\n const factory = BUILT_IN_RULE_MAP[configKey];\n if (!factory) {\n process.stderr.write(`[ai-hooks] unknown built-in config key: ${configKey}\\n`);\n continue;\n }\n const ruleConfig = map[configKey] ?? new EmptyRuleConfig();\n rules.push(...factory(ruleConfig, guardHints));\n }\n return rules;\n}\n\nfunction loadCustomRules(config: WebpiecesRulesConfig, workspaceRoot: string): Rule[] {\n const dirs = config.rulesDir ?? [];\n // webpieces-disable no-any-unknown -- index the typed config by dynamic custom-rule name\n const map = config as unknown as Record<string, RuleOptions | undefined>;\n const rules: Rule[] = [];\n for (const plain of loadCustomPlainRules(dirs, workspaceRoot)) {\n const rawConfig = map[plain.name] ?? {};\n rules.push(new CustomRuleAdapter(plain, rawConfig));\n }\n return rules;\n}\n\nfunction loadCustomPlainRules(rulesDirs: readonly string[], workspaceRoot: string): PlainRule[] {\n const modules: PlainRule[] = [];\n for (const dir of rulesDirs) {\n const absDir = path.isAbsolute(dir) ? dir : path.join(workspaceRoot, dir);\n if (!fs.existsSync(absDir)) {\n process.stderr.write(`[ai-hooks] rulesDir not found: ${absDir}\\n`);\n continue;\n }\n let entries: string[];\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n entries = fs.readdirSync(absDir).filter((e: string) => e.endsWith('.js'));\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Cannot read custom rules directory '${absDir}'`, { cause: error });\n }\n for (const entry of entries) {\n const full = path.join(absDir, entry);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const mod = require(full);\n const candidate = mod.default || mod;\n if (validateRule(candidate)) modules.push(candidate);\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Cannot load custom rule '${full}'`, { cause: error });\n }\n }\n }\n return modules;\n}\n\n// webpieces-disable no-any-unknown -- validates untrusted require() output at system boundary\nfunction validateRule(rule: unknown): rule is PlainRule {\n if (!rule || typeof rule !== 'object') {\n process.stderr.write('[ai-hooks] rule is not an object, skipping\\n');\n return false;\n }\n // webpieces-disable no-any-unknown -- narrowing from unknown at system boundary\n const obj = rule as Record<string, unknown>;\n for (const field of REQUIRED_FIELDS) {\n if (obj[field] === undefined) {\n const name = typeof obj['name'] === 'string' ? obj['name'] : '<unnamed>';\n process.stderr.write(`[ai-hooks] rule \"${name}\" missing required field: ${field}\\n`);\n return false;\n }\n }\n if (!VALID_SCOPES.has(obj['scope'] as string)) {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" has invalid scope: ${String(obj['scope'])}\\n`);\n return false;\n }\n if (!Array.isArray(obj['files'])) {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" files must be an array\\n`);\n return false;\n }\n if (typeof obj['check'] !== 'function') {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" check must be a function\\n`);\n return false;\n }\n return true;\n}\n\nexport function globMatches(pattern: string, filePath: string): boolean {\n const regex = globToRegex(pattern);\n return regex.test(filePath);\n}\n\nfunction globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*') {\n if (pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"load-rules.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/load-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport {\n BaseRuleConfig, RuleOptions, WebpiecesRulesConfig,\n NoAnyUnknownConfig, NoImplicitAnyConfig, MaxFileLinesConfig, ValidateTsInSrcConfig,\n NoDestructureConfig, RequireReturnTypeConfig, NoUnmanagedExceptionsConfig,\n CatchErrorPatternConfig, ThrowCauseRequiredConfig,\n NoSymbolDiTokensConfig, NoCustomCssConfig, NoProcessExitOutsideMainConfig, BranchCreationGuardConfig,\n PrLifecycleGuardConfig, BranchStateGuardConfig,\n NoJsFilesConfig, MatchRuleConfig,\n} from '@webpieces/rules-config';\n\nimport type { Rule, PlainRule } from './types';\nimport { InformAiError } from './types';\nimport { toError } from './to-error';\nimport { EmptyRuleConfig } from './rule-base';\nimport { CustomRuleAdapter } from './custom-rule-adapter';\nimport { builtInConfigKeys } from './rules/index';\nimport { NoAnyUnknownRule } from './rules/no-any-unknown';\nimport { NoImplicitAnyRule } from './rules/no-implicit-any';\nimport { MaxFileLinesRule } from './rules/max-file-lines';\nimport { ValidateTsInSrcRule } from './rules/validate-ts-in-src';\nimport { NoDestructureRule } from './rules/no-destructure';\nimport { RequireReturnTypeRule } from './rules/require-return-type';\nimport { NoUnmanagedExceptionsRule } from './rules/no-unmanaged-exceptions';\nimport { CatchErrorPatternRule } from './rules/catch-error-pattern';\nimport { ThrowCauseRequiredRule } from './rules/throw-cause-required';\nimport { NoSymbolDiTokensRule } from './rules/no-symbol-di-tokens';\nimport { NoCustomCssRule } from './rules/no-custom-css';\nimport { NoProcessExitOutsideMainRule } from './rules/no-process-exit-outside-main';\nimport { BranchCreationGuardRule } from './rules/branch-creation-guard';\nimport { PrCreationOrPushGuardRule } from './rules/pr-creation-or-push-guard';\nimport { MergeInProgressGuardRule } from './rules/merge-in-progress-guard';\nimport { BuildOutputPipeGuardRule } from './rules/build-output-pipe-guard';\nimport { WaitSpinGuardRule } from './rules/wait-spin-guard';\nimport { PrMergeGuardRule } from './rules/pr-merge-guard';\nimport { RedirectHowToMergeMainRule } from './rules/redirect-how-to-merge-main';\nimport { NoJsFilesRule } from './rules/no-js-files';\nimport { FeatureBranchGuardRule } from './rules/feature-branch-guard';\nimport { ReadStaleGuardRule } from './rules/read-stale-guard';\nimport { MergedBranchBashGuardRule } from './rules/merged-branch-bash-guard';\nimport { StaleMainBashGuardRule } from './rules/stale-main-bash-guard';\nimport { WholeRepoBuildGuardRule } from './rules/whole-repo-build-guard';\nimport { CommitMessageSubstitutionGuardRule } from './rules/commit-message-substitution-guard';\nimport { MatchRule } from './rules/match-rule';\n\nconst REQUIRED_FIELDS: readonly string[] = ['name', 'description', 'scope', 'files', 'check'];\nconst VALID_SCOPES = new Set(['edit', 'file', 'bash']);\n\n/**\n * ONE CONFIG KEY → N RULES.\n *\n * Each built-in rule is constructed from its typed *Config (the entry in webpieces.config.json). The\n * config arrives as a plain object structurally typed as the *Config class, so the `as` narrows the\n * shared BaseRuleConfig param back to the concrete config the rule consumes.\n *\n * The map is keyed by CONFIG KEY and each factory returns an ARRAY, because a hookGuards key names a\n * POLICY and a policy may be implemented by several classes: `branch-state-guard` builds all four\n * branch-state guards from one entry, `pr-lifecycle-guard` all four PR-lifecycle guards. It used to be\n * `Record<string, (c) => Rule>` — one factory per key — which is precisely why four classes could not\n * share a key and why the config had to carry nine switches for three decisions.\n *\n * `guardHints` are the resolved `commands.guardHints` strings, handed to the two rules that print a\n * gated command. They arrive as a constructor argument rather than a config field, so there is exactly\n * one spelling of each command in the config (see PrLifecycleGuardConfig).\n */\ntype RuleFactory = (config: BaseRuleConfig, guardHints: GuardHintCommands) => readonly Rule[];\n\n/** The two gated-command strings guards print, resolved from `commands.guardHints`. Data-only. */\nexport class GuardHintCommands {\n constructor(readonly upsertPr: string, readonly mergeComplete: string) {}\n}\n\nconst BUILT_IN_RULE_MAP: Record<string, RuleFactory> = {\n 'no-any-unknown': (c: BaseRuleConfig) => [new NoAnyUnknownRule(c as NoAnyUnknownConfig)],\n 'no-implicit-any': (c: BaseRuleConfig) => [new NoImplicitAnyRule(c as NoImplicitAnyConfig)],\n 'max-file-lines': (c: BaseRuleConfig) => [new MaxFileLinesRule(c as MaxFileLinesConfig)],\n 'validate-ts-in-src': (c: BaseRuleConfig) => [new ValidateTsInSrcRule(c as ValidateTsInSrcConfig)],\n 'no-destructure': (c: BaseRuleConfig) => [new NoDestructureRule(c as NoDestructureConfig)],\n 'require-return-type': (c: BaseRuleConfig) => [new RequireReturnTypeRule(c as RequireReturnTypeConfig)],\n 'no-unmanaged-exceptions': (c: BaseRuleConfig) => [new NoUnmanagedExceptionsRule(c as NoUnmanagedExceptionsConfig)],\n 'catch-error-pattern': (c: BaseRuleConfig) => [new CatchErrorPatternRule(c as CatchErrorPatternConfig)],\n 'throw-cause-required': (c: BaseRuleConfig) => [new ThrowCauseRequiredRule(c as ThrowCauseRequiredConfig)],\n 'no-symbol-di-tokens': (c: BaseRuleConfig) => [new NoSymbolDiTokensRule(c as NoSymbolDiTokensConfig)],\n 'no-custom-css': (c: BaseRuleConfig) => [new NoCustomCssRule(c as NoCustomCssConfig)],\n 'no-process-exit-outside-main': (c: BaseRuleConfig) => [new NoProcessExitOutsideMainRule(c as NoProcessExitOutsideMainConfig)],\n 'no-js-files': (c: BaseRuleConfig) => [new NoJsFilesRule(c as NoJsFilesConfig)],\n 'branch-creation-guard': (c: BaseRuleConfig) => [new BranchCreationGuardRule(c as BranchCreationGuardConfig)],\n // THE TWO COLLAPSED POLICIES. Order inside each array is the order the rules run in, and it is the\n // same order the previous per-key registry produced.\n 'pr-lifecycle-guard': (c: BaseRuleConfig, hints: GuardHintCommands) => [\n new PrCreationOrPushGuardRule(c as PrLifecycleGuardConfig, hints.upsertPr),\n new MergeInProgressGuardRule(c as PrLifecycleGuardConfig, hints.mergeComplete),\n new PrMergeGuardRule(c as PrLifecycleGuardConfig),\n new RedirectHowToMergeMainRule(c as PrLifecycleGuardConfig),\n ],\n 'branch-state-guard': (c: BaseRuleConfig) => [\n new FeatureBranchGuardRule(c as BranchStateGuardConfig),\n new ReadStaleGuardRule(c as BranchStateGuardConfig),\n new MergedBranchBashGuardRule(c as BranchStateGuardConfig),\n new StaleMainBashGuardRule(c as BranchStateGuardConfig),\n ],\n};\n\n// Index the typed config by rule name. Each value is the rule's *Config (a plain object from\n// JSON), or undefined when the rule has no entry yet (the sync check reports those).\nfunction asConfigMap(config: WebpiecesRulesConfig): Record<string, BaseRuleConfig | undefined> {\n // webpieces-disable no-any-unknown -- index the typed config by dynamic rule name\n return config as unknown as Record<string, BaseRuleConfig | undefined>;\n}\n\n// webpieces-disable no-function-outside-class -- the module's entry point, beside loadMatchRules/loadKeylessBashRules; this whole loader is module-scope functions and a lone class for one of them would break the file's shape\nexport function loadRules(\n config: WebpiecesRulesConfig,\n workspaceRoot: string,\n guardHints: GuardHintCommands,\n): readonly Rule[] {\n const builtIns = loadBuiltInRules(config, guardHints);\n const custom = loadCustomRules(config, workspaceRoot);\n return [...builtIns, ...custom];\n}\n\n/**\n * The KEYLESS bash guards: rules that have NO webpieces.config.json entry, and are therefore\n * deliberately kept out of `builtInConfigKeys`/`BUILT_IN_RULE_MAP` — so the config-sync check (fault Y,\n * \"every built-in rule needs an entry, or every Bash call is blocked\") can never see them. That\n * containment is the whole point: whole-repo-build-guard shipped inside the config-driven set once and\n * took every upgrading consumer's shell down with it.\n *\n * Each rule here decides for ITSELF whether it acts, and the two do it differently on purpose:\n *\n * - `whole-repo-build-guard` is EXPERIMENTAL and inert unless the optional machine-local\n * `~/.webpieces/config.json` opts IN with `experimental.whole-repo-build-guard: true`. Every\n * experimental flag defaults OFF, and it takes no file and no key to be in that default state —\n * which is the difference between this and the required-key release that blocked every upgrading\n * consumer's shell.\n * - `commit-message-substitution-guard` acts unconditionally. Nobody legitimately wants a backtick\n * expanded inside a commit message, and its cure (`git commit -F <file>`) is available for every\n * input and can never itself match the guard — so there is nothing for a switch to rescue.\n * - `build-output-pipe-guard` acts unconditionally, on the same test. Piping `wp-build` /\n * `wp-review-upsert-pr` / `wp-finish-upsert-pr` withholds their heartbeat until they exit and gets\n * the build killed by the 600s watchdog; the cure is the SAME command with less typing, available\n * for every input, and it cannot itself match the guard.\n * - `wait-spin-guard` acts unconditionally, on the same test again. An `echo .` keep-alive spends a\n * whole turn (~557k tokens) to do nothing, and its cure — `pnpm wp-await-reviews` /\n * `pnpm wp-await-checks` for a worktree subagent, a Monitor or the backgrounded command itself for a\n * main agent — is available for every input and can never itself match the guard.\n *\n * `affectedBuildCommand` is the project's gate command, passed through so a refusal quotes what THIS\n * repo's gate actually runs.\n */\n// webpieces-disable no-function-outside-class -- sibling of loadRules/loadMatchRules in this module; the whole loader is module-scope functions and a lone class for this one would break the file's shape\nexport function loadKeylessBashRules(affectedBuildCommand: string): Rule[] {\n return [\n new WholeRepoBuildGuardRule(affectedBuildCommand),\n new CommitMessageSubstitutionGuardRule(),\n new BuildOutputPipeGuardRule(),\n new WaitSpinGuardRule(),\n ];\n}\n\n// One MatchRule per entry of the `match-rules` array. Kept separate from loadRules (built-ins/custom)\n// because match-rules live in their own validated section — they must NOT flow through the\n// config-sync check, which compares rule names against the `rules`/`hookGuards` map.\nexport function loadMatchRules(matchRules: readonly MatchRuleConfig[]): Rule[] {\n return matchRules.map((c: MatchRuleConfig) => new MatchRule(c));\n}\n\n// Iterates CONFIG KEYS, not rule names — one entry can yield several rules (see BUILT_IN_RULE_MAP).\n// webpieces-disable no-function-outside-class -- the body of loadRules above, in the same module of loader functions\nfunction loadBuiltInRules(config: WebpiecesRulesConfig, guardHints: GuardHintCommands): Rule[] {\n const map = asConfigMap(config);\n const rules: Rule[] = [];\n for (const configKey of builtInConfigKeys) {\n const factory = BUILT_IN_RULE_MAP[configKey];\n if (!factory) {\n process.stderr.write(`[ai-hooks] unknown built-in config key: ${configKey}\\n`);\n continue;\n }\n const ruleConfig = map[configKey] ?? new EmptyRuleConfig();\n rules.push(...factory(ruleConfig, guardHints));\n }\n return rules;\n}\n\nfunction loadCustomRules(config: WebpiecesRulesConfig, workspaceRoot: string): Rule[] {\n const dirs = config.rulesDir ?? [];\n // webpieces-disable no-any-unknown -- index the typed config by dynamic custom-rule name\n const map = config as unknown as Record<string, RuleOptions | undefined>;\n const rules: Rule[] = [];\n for (const plain of loadCustomPlainRules(dirs, workspaceRoot)) {\n const rawConfig = map[plain.name] ?? {};\n rules.push(new CustomRuleAdapter(plain, rawConfig));\n }\n return rules;\n}\n\nfunction loadCustomPlainRules(rulesDirs: readonly string[], workspaceRoot: string): PlainRule[] {\n const modules: PlainRule[] = [];\n for (const dir of rulesDirs) {\n const absDir = path.isAbsolute(dir) ? dir : path.join(workspaceRoot, dir);\n if (!fs.existsSync(absDir)) {\n process.stderr.write(`[ai-hooks] rulesDir not found: ${absDir}\\n`);\n continue;\n }\n let entries: string[];\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n entries = fs.readdirSync(absDir).filter((e: string) => e.endsWith('.js'));\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Cannot read custom rules directory '${absDir}'`, { cause: error });\n }\n for (const entry of entries) {\n const full = path.join(absDir, entry);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const mod = require(full);\n const candidate = mod.default || mod;\n if (validateRule(candidate)) modules.push(candidate);\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Cannot load custom rule '${full}'`, { cause: error });\n }\n }\n }\n return modules;\n}\n\n// webpieces-disable no-any-unknown -- validates untrusted require() output at system boundary\nfunction validateRule(rule: unknown): rule is PlainRule {\n if (!rule || typeof rule !== 'object') {\n process.stderr.write('[ai-hooks] rule is not an object, skipping\\n');\n return false;\n }\n // webpieces-disable no-any-unknown -- narrowing from unknown at system boundary\n const obj = rule as Record<string, unknown>;\n for (const field of REQUIRED_FIELDS) {\n if (obj[field] === undefined) {\n const name = typeof obj['name'] === 'string' ? obj['name'] : '<unnamed>';\n process.stderr.write(`[ai-hooks] rule \"${name}\" missing required field: ${field}\\n`);\n return false;\n }\n }\n if (!VALID_SCOPES.has(obj['scope'] as string)) {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" has invalid scope: ${String(obj['scope'])}\\n`);\n return false;\n }\n if (!Array.isArray(obj['files'])) {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" files must be an array\\n`);\n return false;\n }\n if (typeof obj['check'] !== 'function') {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" check must be a function\\n`);\n return false;\n }\n return true;\n}\n\nexport function globMatches(pattern: string, filePath: string): boolean {\n const regex = globToRegex(pattern);\n return regex.test(filePath);\n}\n\nfunction globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*') {\n if (pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n}\n"]}
|
|
@@ -120,7 +120,9 @@ export declare const REPEATS_BEFORE_REFUSAL = 2;
|
|
|
120
120
|
*/
|
|
121
121
|
export declare const SUBAGENT_CURE: string;
|
|
122
122
|
/**
|
|
123
|
-
* The main-agent cure. A main agent has
|
|
124
|
-
*
|
|
123
|
+
* The main-agent cure. A main agent has `Monitor` and `run_in_background` genuinely available, so the
|
|
124
|
+
* efficient wait is named and the wasteful poll is refused — and nothing further is said. Telling it to
|
|
125
|
+
* sit inside a blocking command instead would be worse advice than the spin it just wrote; telling it
|
|
126
|
+
* when to end its turn would be webpieces ruling on a judgement it cannot see (issue #902).
|
|
125
127
|
*/
|
|
126
128
|
export declare const MAIN_AGENT_CURE: string;
|
|
@@ -95,8 +95,8 @@ class WaitSpinGuardRule extends rule_base_1.BashRuleBase {
|
|
|
95
95
|
'the same gh pr checks/view asked a third time (a trailing | head/tail/cat/wc -l included) — ' +
|
|
96
96
|
'and name the wait its kind of agent can actually use: a worktree subagent blocks in the ' +
|
|
97
97
|
'FOREGROUND with pnpm wp-await-reviews / pnpm wp-await-checks and re-runs it while it is still ' +
|
|
98
|
-
'waiting, a main agent starts a Monitor
|
|
99
|
-
'and is never refused.';
|
|
98
|
+
'waiting, a main agent starts a Monitor or backgrounds the command. gh pr checks --watch already ' +
|
|
99
|
+
'blocks and is never refused.';
|
|
100
100
|
get fixHint() {
|
|
101
101
|
return new fix_hint_1.FixHint('This command does nothing except keep your turn alive, and a turn costs your whole context — '
|
|
102
102
|
+ '~557,000 tokens measured, whatever the command was.', 'Stop waiting by taking turns. The lines above name what the agent you are should do '
|
|
@@ -199,10 +199,13 @@ exports.SUBAGENT_CURE = 'BLOCK IN ONE FOREGROUND CALL. Run whichever of these yo
|
|
|
199
199
|
+ 'session (issue #900): the notification read "no live background children of its own" and the run\n'
|
|
200
200
|
+ 'made zero further progress until a human poked it.';
|
|
201
201
|
/**
|
|
202
|
-
* The main-agent cure. A main agent has
|
|
203
|
-
*
|
|
202
|
+
* The main-agent cure. A main agent has `Monitor` and `run_in_background` genuinely available, so the
|
|
203
|
+
* efficient wait is named and the wasteful poll is refused — and nothing further is said. Telling it to
|
|
204
|
+
* sit inside a blocking command instead would be worse advice than the spin it just wrote; telling it
|
|
205
|
+
* when to end its turn would be webpieces ruling on a judgement it cannot see (issue #902).
|
|
204
206
|
*/
|
|
205
|
-
exports.MAIN_AGENT_CURE = 'You are the main agent, so
|
|
206
|
-
+ 'command you are waiting on with run_in_background
|
|
207
|
-
+ '
|
|
207
|
+
exports.MAIN_AGENT_CURE = 'You are the main agent, so the efficient wait is available to you: start a `Monitor`, or run the\n'
|
|
208
|
+
+ 'command you are waiting on with run_in_background. Be efficient with tokens — a backgrounded\n'
|
|
209
|
+
+ 'command costs nothing while it runs, where a status check every few seconds costs a whole turn\n'
|
|
210
|
+
+ '(~557k tokens) each time.';
|
|
208
211
|
//# sourceMappingURL=wait-spin-guard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wait-spin-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/wait-spin-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { dotWebpieces } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase, EmptyRuleConfig } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { L0_FAULT_NONE } from '../l0-fault-codes';\nimport { logGuardDecision, GuardDecision, Verdict, MATRIX_L2_UNROWED } from '../decision-log';\nimport { CommandScanner } from '../command-scan';\nimport { SessionCallHistory } from '../session-call-history';\nimport { ShellSegmentScan } from './shell-segment-scan';\nimport { WaitSpinScan, WaitSpinHit, SPIN_POLL } from './wait-spin-scan';\n\n/**\n * Blocks the two shapes an agent uses to WAIT when it has nothing left to do — a bare `echo .` /\n * `true` / `date` keep-alive, and the same `gh pr checks <n>` asked for the third time — and hands back\n * the ONE blocking command that its kind of agent can actually use instead.\n *\n * ─── The incident, measured (issue #874) ───────────────────────────────────────────────────────────\n * In the 24 hours to 2026-09-07, ten agent runs across four repos spun this way: 920M tokens, 18.3% of\n * every token the fleet spent, ~$1,389. Excluding the single worst run entirely it is still 9.0%. The\n * arithmetic is not subtle — every turn resends the whole conversation, measured at ~557,000 tokens per\n * turn, so a three-second `echo .` costs more than most of the work around it.\n *\n * ─── WHY agents do it, and TWO corrections to the cure (issues #878, then #900) ────────────────────\n * The first cut of this guard told a subagent it could not end its turn at all. #878 measured that and\n * found it false — 2,605 subagent turns ended with `stop_reason: end_turn`, 588 were followed by MORE\n * turns, and 449 of those resumptions arrived as a `[SYSTEM NOTIFICATION] … background-task event`\n * (288 of them while waiting on spawned reviewers). So the cure was rewritten to lead with ENDING THE\n * TURN and to offer a blocking command second.\n *\n * **#900 measured the other half of that claim, and it does not hold.** Those 449 resumptions say a\n * wake-up CAN happen; they never established that it RELIABLY happens for the case this cure creates —\n * a worktree-isolated subagent whose only pending work is one `run_in_background` wait. Measured\n * directly: three stalls across two subagents in a single session, each of which HAD launched its wait\n * with `run_in_background: true` exactly as instructed. Every task-notification read\n * \"stops with no live background children of its own\", and each run made ZERO further progress until a\n * human poked it. On a 19-minute CI job that is 19 minutes of dead time per wait plus a parent\n * round-trip to notice it.\n *\n * So the SUBAGENT cure no longer asks the agent to end its turn and be re-invoked. It names the\n * FOREGROUND blocking wait — the same shape Codex already gets, which returns under its harness ceiling\n * and is re-run — because a wait that re-invokes on its own is only cheaper than a foreground call when\n * the re-invocation actually fires, and here it does not. The honest cost, stated so nobody has to\n * rediscover it: a stop costs ~650 tokens, while foregrounding re-enters the agent roughly every nine\n * minutes and resends its context each time. That is more expensive per wait and still right, because\n * the alternative is a run that does not finish.\n *\n * **REVERT TO THE END-YOUR-TURN CURE IF THE HARNESS BEHAVIOUR IS FIXED.** The root cause is Claude\n * Code's, was reported to Anthropic separately, and is not webpieces' to repair; this guard only\n * mitigates it. When background re-invocation is observed to fire reliably for a subagent with a\n * backgrounded Bash and nothing else pending, the free wait is the better cure again.\n *\n * What both corrections leave untouched: `Monitor` still does not block (its own result text says\n * \"Keep working — do not poll or sleep\"), and a `Monitor` carrying a real polling loop is still refused\n * by the HARNESS — 178 of 553 subagent Monitor calls — because a `while`/`until` with a redirect cannot\n * be statically proven to stay inside the worktree. That refusal is Claude Code's, not webpieces'; it\n * is not ours to relax and this guard does not try. A blocking Bash call remains the only wait a\n * subagent can actually express, and that is what `pnpm wp-await-reviews` and `pnpm wp-await-checks`\n * are for. They are the FIRST answer now.\n *\n * ─── THE CURE STILL DIFFERS BY AGENT KIND ──────────────────────────────────────────────────────────\n * A main agent has `Monitor` and `run_in_background` genuinely available and its own cure names them;\n * #900's evidence is entirely about worktree-isolated subagents, so MAIN_AGENT_CURE is unchanged. The\n * kind is read the same way the decision log stamps it: `dotWebpieces.worktreeName(root)`, git's own\n * worktree name, empty for the primary clone. Same call, so a `tree=` column and this verdict cannot\n * disagree.\n *\n * ─── WHY THE SUBAGENT CURE NAMES A COMMAND AND NOT THE AGENT'S PENDING WORK ────────────────────────\n * The cure would ideally say \"you have 4 reviewers running, they will wake you\". It cannot, and the\n * reason is structural rather than an omission: this hook is registered on `Write|Edit|MultiEdit|Bash|\n * Read`, so an Agent spawn NEVER reaches it and no count of live children exists to read. A\n * backgrounded Bash is no better — `run_in_background` is not carried on the normalized payload, and\n * even if it were, a background command's EXIT is unobservable from a PreToolUse hook, so a remembered\n * spawn could not be distinguished from one that finished an hour ago. That header note is now the\n * measured story of #900 rather than a caveat: the guard cannot see the wake-up, and the wake-up cannot\n * be relied on, so the cure names the one move that works from where the agent is standing.\n *\n * ─── It acts UNCONDITIONALLY, and has NO config key ────────────────────────────────────────────────\n * Like `commit-message-substitution-guard` and `build-output-pipe-guard`, and on the same two tests. A\n * NEW key under `hookGuards` is a key every consumer must ADD or have every Bash call blocked on\n * upgrade (fault Y) — that shipped once, with `whole-repo-build-guard`. And there is nothing for a\n * switch to rescue: the cure is available for every input, is strictly better than what was blocked,\n * and can never itself match this guard.\n */\nexport class WaitSpinGuardRule extends BashRuleBase<EmptyRuleConfig> {\n constructor() {\n // configKey === name and is DELIBERATELY not a real webpieces.config.json key — see the class\n // docstring on fault Y.\n super(new EmptyRuleConfig(), 'wait-spin-guard', 'wait-spin-guard');\n }\n\n private readonly scan = new WaitSpinScan(new CommandScanner(), new ShellSegmentScan());\n private readonly history = new SessionCallHistory();\n\n readonly description =\n 'Block a Bash call whose whole purpose is to stay alive — a bare echo/true/date keep-alive, or ' +\n 'the same gh pr checks/view asked a third time (a trailing | head/tail/cat/wc -l included) — ' +\n 'and name the wait its kind of agent can actually use: a worktree subagent blocks in the ' +\n 'FOREGROUND with pnpm wp-await-reviews / pnpm wp-await-checks and re-runs it while it is still ' +\n 'waiting, a main agent starts a Monitor and ends its turn. gh pr checks --watch already blocks ' +\n 'and is never refused.';\n\n get fixHint(): FixHint {\n return new FixHint(\n 'This command does nothing except keep your turn alive, and a turn costs your whole context — '\n + '~557,000 tokens measured, whatever the command was.',\n 'Stop waiting by taking turns. The lines above name what the agent you are should do '\n + 'instead, cheapest first; they are the only moves to make.',\n [],\n );\n }\n\n check(ctx: BashContext): readonly Violation[] {\n // Blocklist-shaped, so match on commandCode — stripping heredocs and quoted prose can only ever\n // block LESS, and this repo's docs and commit messages are full of these command names.\n const hit = this.scan.classify(ctx.commandCode);\n if (hit === null) return this.allow(ctx, 'not-a-wait-spin');\n if (hit.shape === SPIN_POLL) return this.judgePoll(ctx, hit);\n return this.block(ctx, hit);\n }\n\n /**\n * ONE `gh pr checks 874` is a snapshot somebody acts on and must stay allowed — the difference\n * between asking a question and refusing to stop asking it is the number of times, and nothing else.\n * So the third identical call is the one refused, and the count comes from this session's own call\n * log (which holds calls 1..N-1 by the time this runs) and fails OPEN when it cannot be read.\n */\n private judgePoll(ctx: BashContext, hit: WaitSpinHit): readonly Violation[] {\n const prior = this.history.priorBashCalls(ctx.workspaceRoot, ctx.command);\n if (prior < REPEATS_BEFORE_REFUSAL) return this.allow(ctx, `poll-${String(prior)}-prior`);\n return this.block(ctx, hit);\n }\n\n private allow(ctx: BashContext, reason: string): readonly Violation[] {\n this.logDecision(ctx, 'ALLOW', reason);\n return [];\n }\n\n private block(ctx: BashContext, hit: WaitSpinHit): readonly Violation[] {\n // BLOCK_AI_CURE: the cure is a command the agent runs itself, right now, in place of this one.\n this.logDecision(ctx, 'BLOCK_AI_CURE', `${hit.shape}-${hit.program}`);\n return [new V(1, this.truncate(ctx.command), this.message(ctx, hit))];\n }\n\n // Short on purpose: it is read mid-wait by an agent that needs the ONE next move.\n private message(ctx: BashContext, hit: WaitSpinHit): string {\n return `Blocked: \\`${hit.program}\\` ${this.what(hit)}. `\n + 'Every turn resends your whole conversation — ~557,000 tokens measured — so waiting by '\n + 'taking turns is the most expensive thing you can do, and it was 18.3% of all fleet '\n + 'tokens in one measured day.\\n\\n'\n + this.cure(ctx);\n }\n\n private what(hit: WaitSpinHit): string {\n return hit.shape === SPIN_POLL\n ? 'has already been run twice in this session with the identical arguments, and the answer '\n + 'has not changed because you have not done anything between the calls'\n : 'does nothing at all — it exists only to keep your turn alive';\n }\n\n /**\n * The ONE cure for the agent that is actually calling, and never both. See the class docstring: a\n * main agent told to block wastes ten minutes it did not have to spend, and a subagent told to end\n * its turn stalls until a human notices (issue #900).\n */\n private cure(ctx: BashContext): string {\n return this.isWorktreeIsolated(ctx) ? SUBAGENT_CURE : MAIN_AGENT_CURE;\n }\n\n /**\n * A linked worktree means a worktree-isolated subagent — git's own answer, via the same call the\n * decision log's `tree=` column makes. Fails to `false` (the main-agent cure) only when git says\n * this is the primary clone, which is what the primary clone is.\n */\n private isWorktreeIsolated(ctx: BashContext): boolean {\n return dotWebpieces.worktreeName(ctx.workspaceRoot) !== '';\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, verdict: Verdict, reason: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision(\n 'wait-spin-guard', 'Bash', ctx.command, '-', verdict, reason,\n '-', L0_FAULT_NONE, MATRIX_L2_UNROWED,\n ),\n );\n }\n}\n\n/**\n * How many identical prior calls make the next one a spin. TWO, so the THIRD is refused: one call is a\n * snapshot, two is a re-check after doing something, three in a row with nothing in between is a loop.\n */\nexport const REPEATS_BEFORE_REFUSAL = 2;\n\n/**\n * The subagent cure: BLOCK IN THE FOREGROUND. An earlier cut led with ending the turn, on the strength\n * of 449 measured re-invocations — and #900 measured that the re-invocation does not fire for the case\n * that cure creates (three stalls, two subagents, one session, every one of them backgrounded exactly\n * as told). A wait that never wakes is not a cheap wait; it is a run that does not finish. See the\n * class docstring, including the condition under which this reverts to the free wait.\n */\nexport const SUBAGENT_CURE =\n 'BLOCK IN ONE FOREGROUND CALL. Run whichever of these you are waiting on, and do NOT pass\\n'\n + 'run_in_background:\\n\\n'\n + ' pnpm wp-await-reviews # waiting on reviewer verdicts\\n'\n + ' pnpm wp-await-checks --pr <n> # waiting on CI for a PR\\n\\n'\n + 'Either one heartbeats throughout and RETURNS on its own at 540s — under the 600s ceiling at which\\n'\n + 'the harness demotes a foreground call to the background and the result is lost. When it comes\\n'\n + 'back saying it is still waiting, run the IDENTICAL command again; that is the wait continuing,\\n'\n + 'not a failure. A 100-minute wait costs about eleven calls this way.\\n\\n'\n + 'Same contract if you are waiting on something these two do not cover and you have the full-cycle\\n'\n + \"skill's wp-await.sh: run it in the FOREGROUND with --timeout 545 and re-run the identical command\\n\"\n + 'on exit 2.\\n\\n'\n + 'Do NOT end your turn expecting a backgrounded wait to re-invoke you. Measured three times in one\\n'\n + 'session (issue #900): the notification read \"no live background children of its own\" and the run\\n'\n + 'made zero further progress until a human poked it.';\n\n/**\n * The main-agent cure. A main agent has the cheaper option — cost nothing while waiting — and telling\n * it to sit inside a blocking command instead would be worse advice than the spin it just wrote.\n */\nexport const MAIN_AGENT_CURE =\n 'You are the main agent, so you have the cheapest wait there is: start a `Monitor` (or run the\\n'\n + 'command you are waiting on with run_in_background) and then END YOUR TURN. A backgrounded command\\n'\n + 're-invokes you when it exits, and you burn nothing at all in the meantime.';\n"]}
|
|
1
|
+
{"version":3,"file":"wait-spin-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/wait-spin-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { dotWebpieces } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase, EmptyRuleConfig } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { L0_FAULT_NONE } from '../l0-fault-codes';\nimport { logGuardDecision, GuardDecision, Verdict, MATRIX_L2_UNROWED } from '../decision-log';\nimport { CommandScanner } from '../command-scan';\nimport { SessionCallHistory } from '../session-call-history';\nimport { ShellSegmentScan } from './shell-segment-scan';\nimport { WaitSpinScan, WaitSpinHit, SPIN_POLL } from './wait-spin-scan';\n\n/**\n * Blocks the two shapes an agent uses to WAIT when it has nothing left to do — a bare `echo .` /\n * `true` / `date` keep-alive, and the same `gh pr checks <n>` asked for the third time — and hands back\n * the ONE blocking command that its kind of agent can actually use instead.\n *\n * ─── The incident, measured (issue #874) ───────────────────────────────────────────────────────────\n * In the 24 hours to 2026-09-07, ten agent runs across four repos spun this way: 920M tokens, 18.3% of\n * every token the fleet spent, ~$1,389. Excluding the single worst run entirely it is still 9.0%. The\n * arithmetic is not subtle — every turn resends the whole conversation, measured at ~557,000 tokens per\n * turn, so a three-second `echo .` costs more than most of the work around it.\n *\n * ─── WHY agents do it, and TWO corrections to the cure (issues #878, then #900) ────────────────────\n * The first cut of this guard told a subagent it could not end its turn at all. #878 measured that and\n * found it false — 2,605 subagent turns ended with `stop_reason: end_turn`, 588 were followed by MORE\n * turns, and 449 of those resumptions arrived as a `[SYSTEM NOTIFICATION] … background-task event`\n * (288 of them while waiting on spawned reviewers). So the cure was rewritten to lead with ENDING THE\n * TURN and to offer a blocking command second.\n *\n * **#900 measured the other half of that claim, and it does not hold.** Those 449 resumptions say a\n * wake-up CAN happen; they never established that it RELIABLY happens for the case this cure creates —\n * a worktree-isolated subagent whose only pending work is one `run_in_background` wait. Measured\n * directly: three stalls across two subagents in a single session, each of which HAD launched its wait\n * with `run_in_background: true` exactly as instructed. Every task-notification read\n * \"stops with no live background children of its own\", and each run made ZERO further progress until a\n * human poked it. On a 19-minute CI job that is 19 minutes of dead time per wait plus a parent\n * round-trip to notice it.\n *\n * So the SUBAGENT cure no longer asks the agent to end its turn and be re-invoked. It names the\n * FOREGROUND blocking wait — the same shape Codex already gets, which returns under its harness ceiling\n * and is re-run — because a wait that re-invokes on its own is only cheaper than a foreground call when\n * the re-invocation actually fires, and here it does not. The honest cost, stated so nobody has to\n * rediscover it: a stop costs ~650 tokens, while foregrounding re-enters the agent roughly every nine\n * minutes and resends its context each time. That is more expensive per wait and still right, because\n * the alternative is a run that does not finish.\n *\n * **REVERT TO THE END-YOUR-TURN CURE IF THE HARNESS BEHAVIOUR IS FIXED.** The root cause is Claude\n * Code's, was reported to Anthropic separately, and is not webpieces' to repair; this guard only\n * mitigates it. When background re-invocation is observed to fire reliably for a subagent with a\n * backgrounded Bash and nothing else pending, the free wait is the better cure again.\n *\n * What both corrections leave untouched: `Monitor` still does not block (its own result text says\n * \"Keep working — do not poll or sleep\"), and a `Monitor` carrying a real polling loop is still refused\n * by the HARNESS — 178 of 553 subagent Monitor calls — because a `while`/`until` with a redirect cannot\n * be statically proven to stay inside the worktree. That refusal is Claude Code's, not webpieces'; it\n * is not ours to relax and this guard does not try. A blocking Bash call remains the only wait a\n * subagent can actually express, and that is what `pnpm wp-await-reviews` and `pnpm wp-await-checks`\n * are for. They are the FIRST answer now.\n *\n * ─── THE CURE STILL DIFFERS BY AGENT KIND ──────────────────────────────────────────────────────────\n * A main agent has `Monitor` and `run_in_background` genuinely available and its own cure names them;\n * #900's evidence is entirely about worktree-isolated subagents, so MAIN_AGENT_CURE is unchanged. The\n * kind is read the same way the decision log stamps it: `dotWebpieces.worktreeName(root)`, git's own\n * worktree name, empty for the primary clone. Same call, so a `tree=` column and this verdict cannot\n * disagree.\n *\n * ─── WHY THE SUBAGENT CURE NAMES A COMMAND AND NOT THE AGENT'S PENDING WORK ────────────────────────\n * The cure would ideally say \"you have 4 reviewers running, they will wake you\". It cannot, and the\n * reason is structural rather than an omission: this hook is registered on `Write|Edit|MultiEdit|Bash|\n * Read`, so an Agent spawn NEVER reaches it and no count of live children exists to read. A\n * backgrounded Bash is no better — `run_in_background` is not carried on the normalized payload, and\n * even if it were, a background command's EXIT is unobservable from a PreToolUse hook, so a remembered\n * spawn could not be distinguished from one that finished an hour ago. That header note is now the\n * measured story of #900 rather than a caveat: the guard cannot see the wake-up, and the wake-up cannot\n * be relied on, so the cure names the one move that works from where the agent is standing.\n *\n * ─── It acts UNCONDITIONALLY, and has NO config key ────────────────────────────────────────────────\n * Like `commit-message-substitution-guard` and `build-output-pipe-guard`, and on the same two tests. A\n * NEW key under `hookGuards` is a key every consumer must ADD or have every Bash call blocked on\n * upgrade (fault Y) — that shipped once, with `whole-repo-build-guard`. And there is nothing for a\n * switch to rescue: the cure is available for every input, is strictly better than what was blocked,\n * and can never itself match this guard.\n */\nexport class WaitSpinGuardRule extends BashRuleBase<EmptyRuleConfig> {\n constructor() {\n // configKey === name and is DELIBERATELY not a real webpieces.config.json key — see the class\n // docstring on fault Y.\n super(new EmptyRuleConfig(), 'wait-spin-guard', 'wait-spin-guard');\n }\n\n private readonly scan = new WaitSpinScan(new CommandScanner(), new ShellSegmentScan());\n private readonly history = new SessionCallHistory();\n\n readonly description =\n 'Block a Bash call whose whole purpose is to stay alive — a bare echo/true/date keep-alive, or ' +\n 'the same gh pr checks/view asked a third time (a trailing | head/tail/cat/wc -l included) — ' +\n 'and name the wait its kind of agent can actually use: a worktree subagent blocks in the ' +\n 'FOREGROUND with pnpm wp-await-reviews / pnpm wp-await-checks and re-runs it while it is still ' +\n 'waiting, a main agent starts a Monitor or backgrounds the command. gh pr checks --watch already ' +\n 'blocks and is never refused.';\n\n get fixHint(): FixHint {\n return new FixHint(\n 'This command does nothing except keep your turn alive, and a turn costs your whole context — '\n + '~557,000 tokens measured, whatever the command was.',\n 'Stop waiting by taking turns. The lines above name what the agent you are should do '\n + 'instead, cheapest first; they are the only moves to make.',\n [],\n );\n }\n\n check(ctx: BashContext): readonly Violation[] {\n // Blocklist-shaped, so match on commandCode — stripping heredocs and quoted prose can only ever\n // block LESS, and this repo's docs and commit messages are full of these command names.\n const hit = this.scan.classify(ctx.commandCode);\n if (hit === null) return this.allow(ctx, 'not-a-wait-spin');\n if (hit.shape === SPIN_POLL) return this.judgePoll(ctx, hit);\n return this.block(ctx, hit);\n }\n\n /**\n * ONE `gh pr checks 874` is a snapshot somebody acts on and must stay allowed — the difference\n * between asking a question and refusing to stop asking it is the number of times, and nothing else.\n * So the third identical call is the one refused, and the count comes from this session's own call\n * log (which holds calls 1..N-1 by the time this runs) and fails OPEN when it cannot be read.\n */\n private judgePoll(ctx: BashContext, hit: WaitSpinHit): readonly Violation[] {\n const prior = this.history.priorBashCalls(ctx.workspaceRoot, ctx.command);\n if (prior < REPEATS_BEFORE_REFUSAL) return this.allow(ctx, `poll-${String(prior)}-prior`);\n return this.block(ctx, hit);\n }\n\n private allow(ctx: BashContext, reason: string): readonly Violation[] {\n this.logDecision(ctx, 'ALLOW', reason);\n return [];\n }\n\n private block(ctx: BashContext, hit: WaitSpinHit): readonly Violation[] {\n // BLOCK_AI_CURE: the cure is a command the agent runs itself, right now, in place of this one.\n this.logDecision(ctx, 'BLOCK_AI_CURE', `${hit.shape}-${hit.program}`);\n return [new V(1, this.truncate(ctx.command), this.message(ctx, hit))];\n }\n\n // Short on purpose: it is read mid-wait by an agent that needs the ONE next move.\n private message(ctx: BashContext, hit: WaitSpinHit): string {\n return `Blocked: \\`${hit.program}\\` ${this.what(hit)}. `\n + 'Every turn resends your whole conversation — ~557,000 tokens measured — so waiting by '\n + 'taking turns is the most expensive thing you can do, and it was 18.3% of all fleet '\n + 'tokens in one measured day.\\n\\n'\n + this.cure(ctx);\n }\n\n private what(hit: WaitSpinHit): string {\n return hit.shape === SPIN_POLL\n ? 'has already been run twice in this session with the identical arguments, and the answer '\n + 'has not changed because you have not done anything between the calls'\n : 'does nothing at all — it exists only to keep your turn alive';\n }\n\n /**\n * The ONE cure for the agent that is actually calling, and never both. See the class docstring: a\n * main agent told to block wastes ten minutes it did not have to spend, and a subagent told to end\n * its turn stalls until a human notices (issue #900).\n */\n private cure(ctx: BashContext): string {\n return this.isWorktreeIsolated(ctx) ? SUBAGENT_CURE : MAIN_AGENT_CURE;\n }\n\n /**\n * A linked worktree means a worktree-isolated subagent — git's own answer, via the same call the\n * decision log's `tree=` column makes. Fails to `false` (the main-agent cure) only when git says\n * this is the primary clone, which is what the primary clone is.\n */\n private isWorktreeIsolated(ctx: BashContext): boolean {\n return dotWebpieces.worktreeName(ctx.workspaceRoot) !== '';\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, verdict: Verdict, reason: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision(\n 'wait-spin-guard', 'Bash', ctx.command, '-', verdict, reason,\n '-', L0_FAULT_NONE, MATRIX_L2_UNROWED,\n ),\n );\n }\n}\n\n/**\n * How many identical prior calls make the next one a spin. TWO, so the THIRD is refused: one call is a\n * snapshot, two is a re-check after doing something, three in a row with nothing in between is a loop.\n */\nexport const REPEATS_BEFORE_REFUSAL = 2;\n\n/**\n * The subagent cure: BLOCK IN THE FOREGROUND. An earlier cut led with ending the turn, on the strength\n * of 449 measured re-invocations — and #900 measured that the re-invocation does not fire for the case\n * that cure creates (three stalls, two subagents, one session, every one of them backgrounded exactly\n * as told). A wait that never wakes is not a cheap wait; it is a run that does not finish. See the\n * class docstring, including the condition under which this reverts to the free wait.\n */\nexport const SUBAGENT_CURE =\n 'BLOCK IN ONE FOREGROUND CALL. Run whichever of these you are waiting on, and do NOT pass\\n'\n + 'run_in_background:\\n\\n'\n + ' pnpm wp-await-reviews # waiting on reviewer verdicts\\n'\n + ' pnpm wp-await-checks --pr <n> # waiting on CI for a PR\\n\\n'\n + 'Either one heartbeats throughout and RETURNS on its own at 540s — under the 600s ceiling at which\\n'\n + 'the harness demotes a foreground call to the background and the result is lost. When it comes\\n'\n + 'back saying it is still waiting, run the IDENTICAL command again; that is the wait continuing,\\n'\n + 'not a failure. A 100-minute wait costs about eleven calls this way.\\n\\n'\n + 'Same contract if you are waiting on something these two do not cover and you have the full-cycle\\n'\n + \"skill's wp-await.sh: run it in the FOREGROUND with --timeout 545 and re-run the identical command\\n\"\n + 'on exit 2.\\n\\n'\n + 'Do NOT end your turn expecting a backgrounded wait to re-invoke you. Measured three times in one\\n'\n + 'session (issue #900): the notification read \"no live background children of its own\" and the run\\n'\n + 'made zero further progress until a human poked it.';\n\n/**\n * The main-agent cure. A main agent has `Monitor` and `run_in_background` genuinely available, so the\n * efficient wait is named and the wasteful poll is refused — and nothing further is said. Telling it to\n * sit inside a blocking command instead would be worse advice than the spin it just wrote; telling it\n * when to end its turn would be webpieces ruling on a judgement it cannot see (issue #902).\n */\nexport const MAIN_AGENT_CURE =\n 'You are the main agent, so the efficient wait is available to you: start a `Monitor`, or run the\\n'\n + 'command you are waiting on with run_in_background. Be efficient with tokens — a backgrounded\\n'\n + 'command costs nothing while it runs, where a status check every few seconds costs a whole turn\\n'\n + '(~557k tokens) each time.';\n"]}
|