@webpieces/ai-hook-rules 0.4.695 → 0.4.697
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.697",
|
|
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.697"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
|
@@ -63,8 +63,12 @@ class BuildOutputPipeGuardRule extends rule_base_1.BashRuleBase {
|
|
|
63
63
|
get fixHint() {
|
|
64
64
|
return new fix_hint_1.FixHint('Piping one of these commands hides the heartbeat that proves it is alive, and the harness ' +
|
|
65
65
|
'kills a command that has printed nothing for 600 seconds — after a full build has run.', 'Their output is ALREADY in a file. Run the command bare and read the file it names:', [
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
// Names NO command. `fixHint` is static per-rule and cannot see which command was
|
|
67
|
+
// actually piped, while `message(hit)` above already prints that exact command bare —
|
|
68
|
+
// so hardcoding one here spelled `wp-build` at somebody who piped `wp-review-upsert-pr`,
|
|
69
|
+
// which is a cure that does not match the block. One statement of the command, in the
|
|
70
|
+
// one place that knows it.
|
|
71
|
+
new rules_config_1.Option('Re-run the command named above with nothing after it — no pipe, no redirect.\n' +
|
|
68
72
|
'The console stays short — a size heartbeat, then the result and a "FullLog : <path>" line.', true),
|
|
69
73
|
new rules_config_1.Option('Then read as much or as little of that log as you like, as many times as you like:\n' +
|
|
70
74
|
' grep -n error "<the FullLog path it printed>"\n' +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-output-pipe-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/build-output-pipe-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { Option } 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 { ShellSegmentScan } from './shell-segment-scan';\nimport {\n BuildOutputPipeScan, BoundedOutputHit, BOUND_BY_PIPE, LOGGED_BUILD_COMMANDS,\n} from './build-output-pipe-scan';\n\n/**\n * Blocks a Bash command that PIPES or REDIRECTS the output of `pnpm wp-build`,\n * `pnpm wp-review-upsert-pr` or `pnpm wp-finish-upsert-pr`, and hands back the bare command plus a\n * `grep` of the log those commands already write.\n *\n * ─── The incident, measured ────────────────────────────────────────────────────────────────────────\n * The Claude Code harness backgrounds — and then kills — a foreground Bash command that has produced\n * no output for 600 seconds. Those three commands defend against that already: the build's output goes\n * to a FILE, and the console gets a heartbeat roughly every ten seconds plus a `FullLog :` pointer.\n *\n * A PIPE deletes that defence. `pnpm wp-review-upsert-pr 2>&1 | tail -50` withholds every byte until\n * the writer exits, because that is what `tail` is — so the terminal sees NOTHING for the length of a\n * full build, the watchdog fires, and the work is thrown away. The last fleet audit attributed ≥100\n * minutes of wall time to those kills across 8 agents, and this repo's primary tree alone holds 85\n * piped `wp-*` calls, 42 of them on a build-running command (`wp-review-upsert-pr` 22,\n * `wp-finish-upsert-pr` 17, `wp-build` 3).\n *\n * A `> file` redirect is the same silence, and additionally writes a second copy of a log the command\n * already wrote.\n *\n * ─── Why the ORDER of the fix matters, and why this half came second ───────────────────────────────\n * Agents pipe these commands for a REASON: stage ② and stage ③ used to print everything they did. So\n * the output was shrunk FIRST — both stages now capture their verbose body to a log and keep only the\n * lines an agent must act on (see `StageOutputLog` in @webpieces/pr-gate). Blocking the pipe before\n * that would have traded a watchdog kill for a flooded context, and an agent would have routed around\n * it with `> file` — which is why the redirect is on the blocklist too.\n *\n * ─── It acts UNCONDITIONALLY, and has NO config key ────────────────────────────────────────────────\n * Like `commit-message-substitution-guard`, and for the same two reasons.\n *\n * It has no key because a NEW key under `hookGuards` is a key every consumer must ADD or have every\n * Bash call blocked on upgrade (fault Y) — that shipped once already, with `whole-repo-build-guard`,\n * and took upgrading consumers' shells down for a feature nobody had asked for. Loading here, outside\n * the config-driven set, makes the config-sync check structurally unable to see it.\n *\n * It needs no switch because there is nothing for one to rescue: the cure is available for every\n * input, is strictly better than what was blocked, and can never itself match this guard. Running the\n * command bare gives you a heartbeat instead of silence and a log you can `grep` any number of times\n * instead of a fixed 50 lines you have to re-run a build to change.\n */\nexport class BuildOutputPipeGuardRule 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(), 'build-output-pipe-guard', 'build-output-pipe-guard');\n }\n\n private readonly scan = new BuildOutputPipeScan(new CommandScanner(), new ShellSegmentScan());\n\n readonly description =\n 'Block piping or redirecting the output of pnpm wp-build / wp-review-upsert-pr / ' +\n 'wp-finish-upsert-pr — a pipe withholds their heartbeat until exit, so the 600s watchdog kills ' +\n 'the build — and name the bare command plus a grep of the log they already write.';\n\n get fixHint(): FixHint {\n return new FixHint(\n 'Piping one of these commands hides the heartbeat that proves it is alive, and the harness ' +\n 'kills a command that has printed nothing for 600 seconds — after a full build has run.',\n 'Their output is ALREADY in a file. Run the command bare and read the file it names:',\n [\n new Option(\n 'Run it with nothing after it:\\n' +\n ` pnpm ${LOGGED_BUILD_COMMANDS[0]}\\n` +\n 'The console stays short — a size heartbeat, then the result and a \"FullLog : <path>\" line.',\n true),\n new Option(\n 'Then read as much or as little of that log as you like, as many times as you like:\\n' +\n ' grep -n error \"<the FullLog path it printed>\"\\n' +\n ' tail -50 \"<the FullLog path it printed>\"\\n' +\n 'Use the path from THIS run, never one typed from memory — a stale relative path ' +\n 'greps nothing, which reads exactly like a clean build.'),\n new Option(\n 'The previous run is still on disk as \"<that path>.bak\", so comparing two runs ' +\n 'needs no third one.'),\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 own commit messages and docs are full of these command names.\n const hit = this.scan.firstHit(ctx.commandCode);\n if (hit === null) return this.allow(ctx);\n return this.block(ctx, hit);\n }\n\n private allow(ctx: BashContext): readonly Violation[] {\n this.logDecision(ctx, 'ALLOW', 'output-not-bounded');\n return [];\n }\n\n private block(ctx: BashContext, hit: BoundedOutputHit): readonly Violation[] {\n // BLOCK_AI_CURE: the cure is the SAME command with less typing, which the agent runs itself.\n this.logDecision(ctx, 'BLOCK_AI_CURE', `${hit.command}-${hit.shape}`);\n return [new V(1, this.truncate(ctx.command), this.message(hit))];\n }\n\n // Short on purpose: it is read mid-task by an agent that needs the ONE next move.\n private message(hit: BoundedOutputHit): string {\n return `Blocked: ${this.what(hit)} \\`${hit.command}\\`. That command already writes its full output `\n + 'to a log file and prints a heartbeat while it runs — and bounding it withholds every byte '\n + 'until the command exits, so the harness sees a silent command and KILLS it at 600s, after '\n + 'the whole build has run.\\n\\n'\n + ` pnpm ${hit.command}\\n\\n`\n + 'Then grep the \"FullLog : <path>\" it prints. The previous run is kept beside it as `.bak`.';\n }\n\n private what(hit: BoundedOutputHit): string {\n return hit.shape === BOUND_BY_PIPE ? 'you piped' : 'you redirected the output of';\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 'build-output-pipe-guard', 'Bash', ctx.command, '-', verdict, reason,\n '-', L0_FAULT_NONE, MATRIX_L2_UNROWED,\n ),\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"build-output-pipe-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/build-output-pipe-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { Option } 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 { ShellSegmentScan } from './shell-segment-scan';\nimport {\n BuildOutputPipeScan, BoundedOutputHit, BOUND_BY_PIPE,\n} from './build-output-pipe-scan';\n\n/**\n * Blocks a Bash command that PIPES or REDIRECTS the output of `pnpm wp-build`,\n * `pnpm wp-review-upsert-pr` or `pnpm wp-finish-upsert-pr`, and hands back the bare command plus a\n * `grep` of the log those commands already write.\n *\n * ─── The incident, measured ────────────────────────────────────────────────────────────────────────\n * The Claude Code harness backgrounds — and then kills — a foreground Bash command that has produced\n * no output for 600 seconds. Those three commands defend against that already: the build's output goes\n * to a FILE, and the console gets a heartbeat roughly every ten seconds plus a `FullLog :` pointer.\n *\n * A PIPE deletes that defence. `pnpm wp-review-upsert-pr 2>&1 | tail -50` withholds every byte until\n * the writer exits, because that is what `tail` is — so the terminal sees NOTHING for the length of a\n * full build, the watchdog fires, and the work is thrown away. The last fleet audit attributed ≥100\n * minutes of wall time to those kills across 8 agents, and this repo's primary tree alone holds 85\n * piped `wp-*` calls, 42 of them on a build-running command (`wp-review-upsert-pr` 22,\n * `wp-finish-upsert-pr` 17, `wp-build` 3).\n *\n * A `> file` redirect is the same silence, and additionally writes a second copy of a log the command\n * already wrote.\n *\n * ─── Why the ORDER of the fix matters, and why this half came second ───────────────────────────────\n * Agents pipe these commands for a REASON: stage ② and stage ③ used to print everything they did. So\n * the output was shrunk FIRST — both stages now capture their verbose body to a log and keep only the\n * lines an agent must act on (see `StageOutputLog` in @webpieces/pr-gate). Blocking the pipe before\n * that would have traded a watchdog kill for a flooded context, and an agent would have routed around\n * it with `> file` — which is why the redirect is on the blocklist too.\n *\n * ─── It acts UNCONDITIONALLY, and has NO config key ────────────────────────────────────────────────\n * Like `commit-message-substitution-guard`, and for the same two reasons.\n *\n * It has no key because a NEW key under `hookGuards` is a key every consumer must ADD or have every\n * Bash call blocked on upgrade (fault Y) — that shipped once already, with `whole-repo-build-guard`,\n * and took upgrading consumers' shells down for a feature nobody had asked for. Loading here, outside\n * the config-driven set, makes the config-sync check structurally unable to see it.\n *\n * It needs no switch because there is nothing for one to rescue: the cure is available for every\n * input, is strictly better than what was blocked, and can never itself match this guard. Running the\n * command bare gives you a heartbeat instead of silence and a log you can `grep` any number of times\n * instead of a fixed 50 lines you have to re-run a build to change.\n */\nexport class BuildOutputPipeGuardRule 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(), 'build-output-pipe-guard', 'build-output-pipe-guard');\n }\n\n private readonly scan = new BuildOutputPipeScan(new CommandScanner(), new ShellSegmentScan());\n\n readonly description =\n 'Block piping or redirecting the output of pnpm wp-build / wp-review-upsert-pr / ' +\n 'wp-finish-upsert-pr — a pipe withholds their heartbeat until exit, so the 600s watchdog kills ' +\n 'the build — and name the bare command plus a grep of the log they already write.';\n\n get fixHint(): FixHint {\n return new FixHint(\n 'Piping one of these commands hides the heartbeat that proves it is alive, and the harness ' +\n 'kills a command that has printed nothing for 600 seconds — after a full build has run.',\n 'Their output is ALREADY in a file. Run the command bare and read the file it names:',\n [\n // Names NO command. `fixHint` is static per-rule and cannot see which command was\n // actually piped, while `message(hit)` above already prints that exact command bare —\n // so hardcoding one here spelled `wp-build` at somebody who piped `wp-review-upsert-pr`,\n // which is a cure that does not match the block. One statement of the command, in the\n // one place that knows it.\n new Option(\n 'Re-run the command named above with nothing after it — no pipe, no redirect.\\n' +\n 'The console stays short — a size heartbeat, then the result and a \"FullLog : <path>\" line.',\n true),\n new Option(\n 'Then read as much or as little of that log as you like, as many times as you like:\\n' +\n ' grep -n error \"<the FullLog path it printed>\"\\n' +\n ' tail -50 \"<the FullLog path it printed>\"\\n' +\n 'Use the path from THIS run, never one typed from memory — a stale relative path ' +\n 'greps nothing, which reads exactly like a clean build.'),\n new Option(\n 'The previous run is still on disk as \"<that path>.bak\", so comparing two runs ' +\n 'needs no third one.'),\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 own commit messages and docs are full of these command names.\n const hit = this.scan.firstHit(ctx.commandCode);\n if (hit === null) return this.allow(ctx);\n return this.block(ctx, hit);\n }\n\n private allow(ctx: BashContext): readonly Violation[] {\n this.logDecision(ctx, 'ALLOW', 'output-not-bounded');\n return [];\n }\n\n private block(ctx: BashContext, hit: BoundedOutputHit): readonly Violation[] {\n // BLOCK_AI_CURE: the cure is the SAME command with less typing, which the agent runs itself.\n this.logDecision(ctx, 'BLOCK_AI_CURE', `${hit.command}-${hit.shape}`);\n return [new V(1, this.truncate(ctx.command), this.message(hit))];\n }\n\n // Short on purpose: it is read mid-task by an agent that needs the ONE next move.\n private message(hit: BoundedOutputHit): string {\n return `Blocked: ${this.what(hit)} \\`${hit.command}\\`. That command already writes its full output `\n + 'to a log file and prints a heartbeat while it runs — and bounding it withholds every byte '\n + 'until the command exits, so the harness sees a silent command and KILLS it at 600s, after '\n + 'the whole build has run.\\n\\n'\n + ` pnpm ${hit.command}\\n\\n`\n + 'Then grep the \"FullLog : <path>\" it prints. The previous run is kept beside it as `.bak`.';\n }\n\n private what(hit: BoundedOutputHit): string {\n return hit.shape === BOUND_BY_PIPE ? 'you piped' : 'you redirected the output of';\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 'build-output-pipe-guard', 'Bash', ctx.command, '-', verdict, reason,\n '-', L0_FAULT_NONE, MATRIX_L2_UNROWED,\n ),\n );\n }\n}\n"]}
|