@webpieces/ai-hook-rules 0.4.565 → 0.4.566
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.566",
|
|
4
4
|
"description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"directory": "packages/tooling/ai-hook-rules"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@webpieces/rules-config": "0.4.
|
|
35
|
+
"@webpieces/rules-config": "0.4.566"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -8,4 +8,12 @@ export declare class PrCreationOrPushGuardRule extends BashRuleBase<PrCreationOr
|
|
|
8
8
|
readonly description = "Block manual `git push` and direct PR creation/edit (gh pr / gh api / curl) so pushes and PRs go only through the gated upsert-pr command.";
|
|
9
9
|
get fixHint(): FixHint;
|
|
10
10
|
check(ctx: BashContext): readonly Violation[];
|
|
11
|
+
/**
|
|
12
|
+
* Read the configured dev refs, falling back to the defaults when this repo has no `pr-gate` section
|
|
13
|
+
* at all (a consumer that has not adopted the gate still gets a correct hint for the default names).
|
|
14
|
+
*
|
|
15
|
+
* Calling the loader here is safe by construction: an INVALID config blocks every Bash call long
|
|
16
|
+
* before a rule runs, so reaching this line already means the config loaded.
|
|
17
|
+
*/
|
|
18
|
+
private aimedAtDevEnvironment;
|
|
11
19
|
}
|
|
@@ -7,17 +7,34 @@ const rule_base_1 = require("../rule-base");
|
|
|
7
7
|
const fix_hint_1 = require("../fix-hint");
|
|
8
8
|
const DEFAULT_UPSERT_PR_COMMAND = 'pnpm wp-start-upsert-pr';
|
|
9
9
|
const INSTRUCT_FILE = 'webpieces.git-workflow.md';
|
|
10
|
+
/**
|
|
11
|
+
* The hint BRANCHES ON INTENT, and that is not polish.
|
|
12
|
+
*
|
|
13
|
+
* There are now two legitimate destinations for a blocked push — land it on `main`, or get it onto the
|
|
14
|
+
* shared dev server — and the block alone cannot tell them apart. The old flat hint said "everything goes
|
|
15
|
+
* through `wp-start-upsert-pr`", so an AI told "put my branch on dev" would try `git push`, get blocked,
|
|
16
|
+
* read this, and open a PR to main: the exact opposite of what it was asked to do, and the destructive
|
|
17
|
+
* direction of the two. Naming both destinations is what makes the dev flow discoverable at all — nothing
|
|
18
|
+
* else the AI can see mentions it.
|
|
19
|
+
*/
|
|
10
20
|
function fixHintFor(upsertPrCommand) {
|
|
11
|
-
return new fix_hint_1.FixHint('Direct PR creation/update AND manual `git push` are blocked.', 'Never push or open/update a PR by hand —
|
|
21
|
+
return new fix_hint_1.FixHint('Direct PR creation/update AND manual `git push` are blocked.', 'Never push or open/update a PR by hand. There are TWO gated destinations — pick by what you were\n'
|
|
22
|
+
+ 'actually trying to do:\n\n'
|
|
23
|
+
+ 'LANDING ON MAIN (a PR, prod-quality, reviewed):\n'
|
|
12
24
|
+ ` ${upsertPrCommand}\n`
|
|
13
|
-
+ 'It updates the branch from main (3-point merge) and runs the real build (nx affected), then\n'
|
|
14
|
-
+ 'instructs you to write review.json and run `pnpm wp-finish-upsert-pr`, which assembles the\n'
|
|
15
|
-
+ 'dashboard and creates/updates the PR — and pushes for you
|
|
16
|
-
+ '
|
|
17
|
-
+
|
|
18
|
-
+ '
|
|
19
|
-
+ '
|
|
20
|
-
+ 'the
|
|
25
|
+
+ ' It updates the branch from main (3-point merge) and runs the real build (nx affected), then\n'
|
|
26
|
+
+ ' instructs you to write review.json and run `pnpm wp-finish-upsert-pr`, which assembles the\n'
|
|
27
|
+
+ ' dashboard and creates/updates the PR — and pushes for you. A failing build = no push, no PR.\n\n'
|
|
28
|
+
+ 'JUST WANT IT ON THE SHARED DEV SERVER (no PR, not landing on main):\n'
|
|
29
|
+
+ ` ${rules_config_1.WP_PUSH_DEV}\n`
|
|
30
|
+
+ ' It publishes a DISPOSABLE copy of your branch that the dev environment composes and rebuilds.\n'
|
|
31
|
+
+ ' Your feature branch is never moved and never acquires another developer\'s commits, which is\n'
|
|
32
|
+
+ ' the whole reason the copy exists. Do NOT open a PR to main just to test something in dev.\n\n'
|
|
33
|
+
+ 'Both push internally as child processes this hook never sees, so the gated commands are\n'
|
|
34
|
+
+ 'unaffected by this guard. There is nothing to paste or attest to; the commands do the work.\n'
|
|
35
|
+
+ 'If a HUMAN genuinely needs an out-of-band push (neither destination above), do NOT do it\n'
|
|
36
|
+
+ 'yourself — ask them to run the push, since a manual push bypasses the build gate, review.json,\n'
|
|
37
|
+
+ 'and dashboard.\n'
|
|
21
38
|
+ 'Full branch → update → PR flow: READ the instruct-ai git-workflow doc at the absolute path on the violation line above.\n'
|
|
22
39
|
+ 'Add this to your memory so you don\'t forget next time and waste tokens.');
|
|
23
40
|
}
|
|
@@ -45,6 +62,25 @@ function truncate(s) {
|
|
|
45
62
|
const MAX = 120;
|
|
46
63
|
return s.length <= MAX ? s : s.slice(0, MAX) + '…';
|
|
47
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Does this blocked push AIM at the shared dev environment?
|
|
67
|
+
*
|
|
68
|
+
* The refspec is the one place the AI already stated its intent unambiguously, so reading it is strictly
|
|
69
|
+
* better than making the AI choose between two hints it has no way to disambiguate. A push whose
|
|
70
|
+
* destination is the dev namespace or the dev branch itself gets the `wp-push-dev` remedy on the `→`
|
|
71
|
+
* line; everything else keeps the PR-flow remedy it has always had.
|
|
72
|
+
*/
|
|
73
|
+
// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file
|
|
74
|
+
function targetsDevEnvironment(cmd, namespace, devBranch) {
|
|
75
|
+
if (cmd.includes(`${namespace}/`))
|
|
76
|
+
return true;
|
|
77
|
+
const push = /\bgit\s+push\b([^\n;&|]*)/.exec(cmd);
|
|
78
|
+
if (push === null)
|
|
79
|
+
return false;
|
|
80
|
+
// Split on whitespace AND `:` so both halves of a `HEAD:<dest>` refspec are examined. Matching whole
|
|
81
|
+
// tokens (never a substring) keeps a branch merely NAMED `develop` from reading as the dev branch.
|
|
82
|
+
return push[1].split(/[\s:]+/).some((token) => token === devBranch || token === `refs/heads/${devBranch}`);
|
|
83
|
+
}
|
|
48
84
|
class PrCreationOrPushGuardRule extends rule_base_1.BashRuleBase {
|
|
49
85
|
upsertPrCommand;
|
|
50
86
|
constructor(config) {
|
|
@@ -61,8 +97,24 @@ class PrCreationOrPushGuardRule extends rule_base_1.BashRuleBase {
|
|
|
61
97
|
// copy from an older @webpieces is just as bad, so overwrite rather than write-if-missing.
|
|
62
98
|
(0, rules_config_1.writeTemplate)(ctx.workspaceRoot, INSTRUCT_FILE);
|
|
63
99
|
const docPath = new rules_config_1.RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);
|
|
100
|
+
if (this.aimedAtDevEnvironment(ctx)) {
|
|
101
|
+
return [new types_1.Violation(1, truncate(ctx.command), 'Manual push is blocked — but this one is aimed at the shared DEV environment, which has its own\n'
|
|
102
|
+
+ `gated command. Use \`${rules_config_1.WP_PUSH_DEV}\` (publishes a disposable copy; never moves your branch, never\n`
|
|
103
|
+
+ `opens a PR). Do NOT use ${this.upsertPrCommand} for this — that lands on main. Full flow: READ ${docPath}.`)];
|
|
104
|
+
}
|
|
64
105
|
return [new types_1.Violation(1, truncate(ctx.command), `Manual push / direct PR is blocked — use the gated flow (${this.upsertPrCommand}). Full flow: READ ${docPath}.`)];
|
|
65
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Read the configured dev refs, falling back to the defaults when this repo has no `pr-gate` section
|
|
109
|
+
* at all (a consumer that has not adopted the gate still gets a correct hint for the default names).
|
|
110
|
+
*
|
|
111
|
+
* Calling the loader here is safe by construction: an INVALID config blocks every Bash call long
|
|
112
|
+
* before a rule runs, so reaching this line already means the config loaded.
|
|
113
|
+
*/
|
|
114
|
+
aimedAtDevEnvironment(ctx) {
|
|
115
|
+
const devDeploy = (0, rules_config_1.loadAndValidate)(ctx.governedRoot).prGate.devDeploy;
|
|
116
|
+
return targetsDevEnvironment(ctx.commandCode, devDeploy.branchNamespace === '' ? rules_config_1.DEFAULT_DEV_BRANCH_NAMESPACE : devDeploy.branchNamespace, devDeploy.devBranch === '' ? rules_config_1.DEFAULT_DEV_BRANCH : devDeploy.devBranch);
|
|
117
|
+
}
|
|
66
118
|
}
|
|
67
119
|
exports.PrCreationOrPushGuardRule = PrCreationOrPushGuardRule;
|
|
68
120
|
//# sourceMappingURL=pr-creation-or-push-guard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-creation-or-push-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-creation-or-push-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { PrCreationOrPushGuardConfig, RepoRootFinder, writeTemplate } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\n\nconst DEFAULT_UPSERT_PR_COMMAND = 'pnpm wp-start-upsert-pr';\nconst INSTRUCT_FILE = 'webpieces.git-workflow.md';\n\nfunction fixHintFor(upsertPrCommand: string): FixHint {\n return new FixHint(\n 'Direct PR creation/update AND manual `git push` are blocked.',\n 'Never push or open/update a PR by hand — everything goes through the gated flow:\\n'\n + ` ${upsertPrCommand}\\n`\n + 'It updates the branch from main (3-point merge) and runs the real build (nx affected), then\\n'\n + 'instructs you to write review.json and run `pnpm wp-finish-upsert-pr`, which assembles the\\n'\n + 'dashboard and creates/updates the PR — and pushes for you (its internal `git push` is a child\\n'\n + 'process this hook never sees, so the gated commands are unaffected by this guard). A failing\\n'\n + 'build = no push and no PR.\\n'\n + 'There is nothing to paste or attest to; the commands do the work.\\n'\n + 'If a HUMAN genuinely needs an out-of-band push (no PR), do NOT do it yourself — ask them to run\\n'\n + 'the push, since a manual push bypasses the build gate, review.json, and dashboard.\\n'\n + 'Full branch → update → PR flow: READ the instruct-ai git-workflow doc at the absolute path on the violation line above.\\n'\n + 'Add this to your memory so you don\\'t forget next time and waste tokens.',\n );\n}\n\n// Detect every way an agent could push or open/update a PR directly, so the ONLY path left is the\n// gated flow (wp-start-upsert-pr → wp-finish-upsert-pr, whose internal `git push` / `gh pr create`\n// run as child processes the hook never sees). Read-only `gh pr list` / `gh api .../pulls` GET are\n// intentionally allowed.\nfunction isBlockedPrOrPush(cmd: string): boolean {\n // A manual push is always blocked — the gated flow pushes for you behind the build gate.\n if (/\\bgit\\s+push\\b/.test(cmd)) return true;\n\n if (/\\bgh\\s+pr\\s+(create|edit)\\b/.test(cmd)) return true;\n\n const ghApiPulls = /\\bgh\\s+api\\b[^\\n]*\\/pulls\\b/.test(cmd);\n if (ghApiPulls && (/--method\\s+POST/i.test(cmd) || /-X\\s+POST/i.test(cmd) || /\\s-f\\b/.test(cmd) || /\\s-F\\b/.test(cmd) || /--field\\b/.test(cmd))) {\n return true;\n }\n\n const curlPulls = /\\bcurl\\b[^\\n]*api\\.github\\.com[^\\n]*\\/pulls\\b/.test(cmd);\n if (curlPulls && (/-X\\s*POST/i.test(cmd) || /--request\\s+POST/i.test(cmd) || /(\\s-d\\b|--data\\b)/.test(cmd))) {\n return true;\n }\n return false;\n}\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class PrCreationOrPushGuardRule extends BashRuleBase<PrCreationOrPushGuardConfig> {\n private readonly upsertPrCommand: string;\n\n constructor(config: PrCreationOrPushGuardConfig) {\n super(config, 'pr-creation-or-push-guard');\n this.upsertPrCommand = config.upsertPrCommand ?? DEFAULT_UPSERT_PR_COMMAND;\n }\n\n readonly description = 'Block manual `git push` and direct PR creation/edit (gh pr / gh api / curl) so pushes and PRs go only through the gated upsert-pr command.';\n get fixHint(): FixHint { return fixHintFor(this.upsertPrCommand); }\n\n check(ctx: BashContext): readonly Violation[] {\n if (!isBlockedPrOrPush(ctx.commandCode)) return [];\n // Materialize the doc we are about to send the AI to. Pointing at a path that does not exist\n // (nothing has run a `wp-*` command in this tree yet) costs the AI a turn to discover; a STALE\n // copy from an older @webpieces is just as bad, so overwrite rather than write-if-missing.\n writeTemplate(ctx.workspaceRoot, INSTRUCT_FILE);\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n return [new V(1, truncate(ctx.command),\n `Manual push / direct PR is blocked — use the gated flow (${this.upsertPrCommand}). Full flow: READ ${docPath}.`)];\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pr-creation-or-push-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-creation-or-push-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_DEV_BRANCH,\n DEFAULT_DEV_BRANCH_NAMESPACE,\n PrCreationOrPushGuardConfig,\n RepoRootFinder,\n WP_PUSH_DEV,\n loadAndValidate,\n writeTemplate,\n} from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\n\nconst DEFAULT_UPSERT_PR_COMMAND = 'pnpm wp-start-upsert-pr';\nconst INSTRUCT_FILE = 'webpieces.git-workflow.md';\n\n/**\n * The hint BRANCHES ON INTENT, and that is not polish.\n *\n * There are now two legitimate destinations for a blocked push — land it on `main`, or get it onto the\n * shared dev server — and the block alone cannot tell them apart. The old flat hint said \"everything goes\n * through `wp-start-upsert-pr`\", so an AI told \"put my branch on dev\" would try `git push`, get blocked,\n * read this, and open a PR to main: the exact opposite of what it was asked to do, and the destructive\n * direction of the two. Naming both destinations is what makes the dev flow discoverable at all — nothing\n * else the AI can see mentions it.\n */\nfunction fixHintFor(upsertPrCommand: string): FixHint {\n return new FixHint(\n 'Direct PR creation/update AND manual `git push` are blocked.',\n 'Never push or open/update a PR by hand. There are TWO gated destinations — pick by what you were\\n'\n + 'actually trying to do:\\n\\n'\n + 'LANDING ON MAIN (a PR, prod-quality, reviewed):\\n'\n + ` ${upsertPrCommand}\\n`\n + ' It updates the branch from main (3-point merge) and runs the real build (nx affected), then\\n'\n + ' instructs you to write review.json and run `pnpm wp-finish-upsert-pr`, which assembles the\\n'\n + ' dashboard and creates/updates the PR — and pushes for you. A failing build = no push, no PR.\\n\\n'\n + 'JUST WANT IT ON THE SHARED DEV SERVER (no PR, not landing on main):\\n'\n + ` ${WP_PUSH_DEV}\\n`\n + ' It publishes a DISPOSABLE copy of your branch that the dev environment composes and rebuilds.\\n'\n + ' Your feature branch is never moved and never acquires another developer\\'s commits, which is\\n'\n + ' the whole reason the copy exists. Do NOT open a PR to main just to test something in dev.\\n\\n'\n + 'Both push internally as child processes this hook never sees, so the gated commands are\\n'\n + 'unaffected by this guard. There is nothing to paste or attest to; the commands do the work.\\n'\n + 'If a HUMAN genuinely needs an out-of-band push (neither destination above), do NOT do it\\n'\n + 'yourself — ask them to run the push, since a manual push bypasses the build gate, review.json,\\n'\n + 'and dashboard.\\n'\n + 'Full branch → update → PR flow: READ the instruct-ai git-workflow doc at the absolute path on the violation line above.\\n'\n + 'Add this to your memory so you don\\'t forget next time and waste tokens.',\n );\n}\n\n// Detect every way an agent could push or open/update a PR directly, so the ONLY path left is the\n// gated flow (wp-start-upsert-pr → wp-finish-upsert-pr, whose internal `git push` / `gh pr create`\n// run as child processes the hook never sees). Read-only `gh pr list` / `gh api .../pulls` GET are\n// intentionally allowed.\nfunction isBlockedPrOrPush(cmd: string): boolean {\n // A manual push is always blocked — the gated flow pushes for you behind the build gate.\n if (/\\bgit\\s+push\\b/.test(cmd)) return true;\n\n if (/\\bgh\\s+pr\\s+(create|edit)\\b/.test(cmd)) return true;\n\n const ghApiPulls = /\\bgh\\s+api\\b[^\\n]*\\/pulls\\b/.test(cmd);\n if (ghApiPulls && (/--method\\s+POST/i.test(cmd) || /-X\\s+POST/i.test(cmd) || /\\s-f\\b/.test(cmd) || /\\s-F\\b/.test(cmd) || /--field\\b/.test(cmd))) {\n return true;\n }\n\n const curlPulls = /\\bcurl\\b[^\\n]*api\\.github\\.com[^\\n]*\\/pulls\\b/.test(cmd);\n if (curlPulls && (/-X\\s*POST/i.test(cmd) || /--request\\s+POST/i.test(cmd) || /(\\s-d\\b|--data\\b)/.test(cmd))) {\n return true;\n }\n return false;\n}\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\n/**\n * Does this blocked push AIM at the shared dev environment?\n *\n * The refspec is the one place the AI already stated its intent unambiguously, so reading it is strictly\n * better than making the AI choose between two hints it has no way to disambiguate. A push whose\n * destination is the dev namespace or the dev branch itself gets the `wp-push-dev` remedy on the `→`\n * line; everything else keeps the PR-flow remedy it has always had.\n */\n// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file\nfunction targetsDevEnvironment(cmd: string, namespace: string, devBranch: string): boolean {\n if (cmd.includes(`${namespace}/`)) return true;\n const push = /\\bgit\\s+push\\b([^\\n;&|]*)/.exec(cmd);\n if (push === null) return false;\n // Split on whitespace AND `:` so both halves of a `HEAD:<dest>` refspec are examined. Matching whole\n // tokens (never a substring) keeps a branch merely NAMED `develop` from reading as the dev branch.\n return push[1].split(/[\\s:]+/).some(\n (token: string): boolean => token === devBranch || token === `refs/heads/${devBranch}`);\n}\n\nexport class PrCreationOrPushGuardRule extends BashRuleBase<PrCreationOrPushGuardConfig> {\n private readonly upsertPrCommand: string;\n\n constructor(config: PrCreationOrPushGuardConfig) {\n super(config, 'pr-creation-or-push-guard');\n this.upsertPrCommand = config.upsertPrCommand ?? DEFAULT_UPSERT_PR_COMMAND;\n }\n\n readonly description = 'Block manual `git push` and direct PR creation/edit (gh pr / gh api / curl) so pushes and PRs go only through the gated upsert-pr command.';\n get fixHint(): FixHint { return fixHintFor(this.upsertPrCommand); }\n\n check(ctx: BashContext): readonly Violation[] {\n if (!isBlockedPrOrPush(ctx.commandCode)) return [];\n // Materialize the doc we are about to send the AI to. Pointing at a path that does not exist\n // (nothing has run a `wp-*` command in this tree yet) costs the AI a turn to discover; a STALE\n // copy from an older @webpieces is just as bad, so overwrite rather than write-if-missing.\n writeTemplate(ctx.workspaceRoot, INSTRUCT_FILE);\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n if (this.aimedAtDevEnvironment(ctx)) {\n return [new V(1, truncate(ctx.command),\n 'Manual push is blocked — but this one is aimed at the shared DEV environment, which has its own\\n'\n + `gated command. Use \\`${WP_PUSH_DEV}\\` (publishes a disposable copy; never moves your branch, never\\n`\n + `opens a PR). Do NOT use ${this.upsertPrCommand} for this — that lands on main. Full flow: READ ${docPath}.`)];\n }\n return [new V(1, truncate(ctx.command),\n `Manual push / direct PR is blocked — use the gated flow (${this.upsertPrCommand}). Full flow: READ ${docPath}.`)];\n }\n\n /**\n * Read the configured dev refs, falling back to the defaults when this repo has no `pr-gate` section\n * at all (a consumer that has not adopted the gate still gets a correct hint for the default names).\n *\n * Calling the loader here is safe by construction: an INVALID config blocks every Bash call long\n * before a rule runs, so reaching this line already means the config loaded.\n */\n private aimedAtDevEnvironment(ctx: BashContext): boolean {\n const devDeploy = loadAndValidate(ctx.governedRoot).prGate.devDeploy;\n return targetsDevEnvironment(\n ctx.commandCode,\n devDeploy.branchNamespace === '' ? DEFAULT_DEV_BRANCH_NAMESPACE : devDeploy.branchNamespace,\n devDeploy.devBranch === '' ? DEFAULT_DEV_BRANCH : devDeploy.devBranch);\n }\n}\n"]}
|