@webpieces/ai-hook-rules 0.4.630 → 0.4.632
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/adapters/claude-code-response.js +16 -1
- package/src/adapters/claude-code-response.js.map +1 -1
- package/src/adapters/hook-core.js +1 -1
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/l0-allowlist.d.ts +6 -2
- package/src/bin/l0-allowlist.js +60 -16
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/shim-audit-log.d.ts +4 -3
- package/src/bin/shim-audit-log.js +40 -12
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-deny-reason.d.ts +0 -5
- package/src/bin/shim-deny-reason.js +199 -108
- package/src/bin/shim-deny-reason.js.map +1 -1
- package/src/bin/shim-drift-fix.d.ts +1 -0
- package/src/bin/shim-drift-fix.js +53 -0
- package/src/bin/shim-drift-fix.js.map +1 -0
- package/src/bin/shim-testkit.d.ts +11 -0
- package/src/bin/shim-testkit.js +15 -0
- package/src/bin/shim-testkit.js.map +1 -1
- package/src/bin/shim.js +58 -15
- package/src/bin/shim.js.map +1 -1
- package/src/core/decision-log.d.ts +12 -4
- package/src/core/decision-log.js +13 -5
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-fault-codes.d.ts +46 -0
- package/src/core/l0-fault-codes.js +55 -1
- package/src/core/l0-fault-codes.js.map +1 -1
- package/src/core/l0-matrix.d.ts +23 -4
- package/src/core/l0-matrix.js +74 -24
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/runner.js +1 -1
- package/src/core/runner.js.map +1 -1
- package/templates/ai-hook.sh +73 -23
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.shimStaleDenyReason = shimStaleDenyReason;
|
|
4
4
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
5
|
+
const l0_fault_codes_1 = require("../core/l0-fault-codes");
|
|
5
6
|
const l0_allowlist_1 = require("./l0-allowlist");
|
|
6
7
|
const managed_env_1 = require("./managed-env");
|
|
7
8
|
const shim_1 = require("./shim");
|
|
@@ -9,115 +10,205 @@ const shim_1 = require("./shim");
|
|
|
9
10
|
* THE FAIL-CLOSED DENY TEXT for a drifted managed hook surface (L0 fault S) — its own module because
|
|
10
11
|
* shim.ts is at its line cap and this is one cohesive unit: the words a blocked agent reads, and
|
|
11
12
|
* nothing else. It imports FROM shim.ts and is never imported BY it, so the graph stays acyclic.
|
|
13
|
+
*
|
|
14
|
+
* IT IS RENDERED IN THE HOUSE FORMAT, the same skeleton core/report.ts (formatReport) gives every L1
|
|
15
|
+
* and L2 deny: a header naming what was blocked, a `[guard-name]` block listing the offenders with a
|
|
16
|
+
* one-line `→ why`, what is still allowed, then numbered `Fix Option N:` lines each with its command
|
|
17
|
+
* on its own line. L0 used to be the ONLY layer in webpieces that answered in one unbroken paragraph —
|
|
18
|
+
* ~3,300 characters of it here — so the two commands that matter were buried in prose and there was no
|
|
19
|
+
* guard name to grep for. Nothing about the DECISION changed; only the shape of the words.
|
|
20
|
+
*
|
|
21
|
+
* SHORTER IS NOT THE GOAL, SCANNABLE IS. The budget below still exists (a paragraph regrows when every
|
|
22
|
+
* new finding argues its case here), but a section that earns a line gets a line.
|
|
23
|
+
*
|
|
24
|
+
* CONSTRAINT: the returned string must contain no `"` and no `\` — it is JSON-serialized by denyJson()
|
|
25
|
+
* (a stray quote/backslash would corrupt the PreToolUse decision payload, not just the text). That is an
|
|
26
|
+
* INVARIANT, not a hope, so every interpolated path is STRIPPED of both rather than trusted — a
|
|
27
|
+
* Windows-style path or an odd directory name must not be able to corrupt the decision. Locked by unit
|
|
28
|
+
* tests. An unusual root is also dropped from the `cd` cure rather than quoted (CD_PREFIX would reject it).
|
|
29
|
+
*
|
|
30
|
+
* NEWLINES ARE SAFE HERE AND NEEDED NO NEW MECHANISM. denyJson() runs JSON.stringify, which escapes a
|
|
31
|
+
* real newline to the two-character `\n` on the wire, and Claude Code's parser turns it back. This is
|
|
32
|
+
* already proven in production — every L1 deny is formatReport()'s multi-line string down this exact
|
|
33
|
+
* path. The sh half of L0 (faults D/X/U/K in renderShim) cannot do this: it printf's REASON into a JSON
|
|
34
|
+
* string literal, so it spells its newlines `${NL}` the same way it spells the ANSI escape `${ESC}`.
|
|
35
|
+
* A real newline is neither a quote nor a backslash, so the JSON-safety assertions above are untouched.
|
|
12
36
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
37
|
+
class ShimStaleDeny {
|
|
38
|
+
installedVersion;
|
|
39
|
+
/** The governing root, STRIPPED of `"` and `\` for display. '' when there is none to name. */
|
|
40
|
+
safeRoot;
|
|
41
|
+
/** CLAUDE_PROJECT_DIR as this process sees it, stripped the same way. */
|
|
42
|
+
projectDir;
|
|
43
|
+
drifted;
|
|
44
|
+
inSubagent;
|
|
45
|
+
/**
|
|
46
|
+
* Whether the RAW root can carry a leading `cd <root> &&`. Tested against the raw root, never the
|
|
47
|
+
* stripped one: stripping is a display-safety measure, and cd-anchoring to a path we just mangled
|
|
48
|
+
* would prescribe a cd into a directory that does not exist. A root CD_PREFIX cannot express is
|
|
49
|
+
* simply not offered as a `cd` (raw ok ⇒ safeRoot === root).
|
|
50
|
+
*/
|
|
51
|
+
cdOk;
|
|
52
|
+
constructor(installedVersion, root, drifted, inSubagent) {
|
|
53
|
+
this.installedVersion = installedVersion;
|
|
54
|
+
this.safeRoot = root.replace(/["\\]/g, '');
|
|
55
|
+
this.projectDir = rules_config_1.claudeEnv.projectDirForLog().replace(/["\\]/g, '');
|
|
56
|
+
this.drifted = drifted;
|
|
57
|
+
this.inSubagent = inSubagent;
|
|
58
|
+
this.cdOk = root !== '' && /^[A-Za-z0-9._/@~+-]+$/.test(root);
|
|
59
|
+
}
|
|
60
|
+
render() {
|
|
61
|
+
return [
|
|
62
|
+
...this.header(),
|
|
63
|
+
...this.measured(),
|
|
64
|
+
...this.caller(),
|
|
65
|
+
...this.stillAllowed(),
|
|
66
|
+
...this.fixOptions(),
|
|
67
|
+
...this.footer(),
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* `[managed-hook-surface]` and the surfaces that drifted, one per line.
|
|
72
|
+
*
|
|
73
|
+
* `drifted` names WHICH of the three managed things moved — .claude/webpieces/ai-hook.sh, the
|
|
74
|
+
* .claude/settings.json hook registration, and its managed env entry (see hook-registration.ts). It
|
|
75
|
+
* is REQUIRED, not optional: this used to be a shim-only message, and an optional list would let a
|
|
76
|
+
* caller silently keep emitting the one-file text after the surface grew — the "two spellings of one
|
|
77
|
+
* thing" shape the compatibility policy rejects. It was FOUR; guarantee-root.sh (L-1) is gone,
|
|
78
|
+
* because the guard hooks are registered ABSOLUTE now and there is no second .sh to keep byte-locked.
|
|
79
|
+
*
|
|
80
|
+
* THE CAUSE IS A LIST. It used to assert flatly "(it was reverted or hand-edited)", which is
|
|
81
|
+
* frequently FALSE — the common case is a shim whose logic simply predates this binary — and that
|
|
82
|
+
* false certainty sent a real agent hunting for a tamper that never happened.
|
|
83
|
+
*/
|
|
84
|
+
header() {
|
|
85
|
+
const verNote = this.installedVersion ? ` (installed version ${this.installedVersion})` : '';
|
|
86
|
+
const n = this.drifted.length;
|
|
87
|
+
const label = n === 1 ? '1 surface drifted' : `${String(n)} surfaces drifted`;
|
|
88
|
+
return [
|
|
89
|
+
'❌ webpieces ai-hooks blocked this call: a webpieces-managed hook surface no longer matches the installed guard binary.',
|
|
90
|
+
'',
|
|
91
|
+
(0, l0_fault_codes_1.l0GuardHeader)(l0_fault_codes_1.L0_FAULT_SHIM_STALE, label),
|
|
92
|
+
...this.drifted.map((surface) => ` ${surface}`),
|
|
93
|
+
` → webpieces manages those THREE things as ONE set, GENERATED by the INSTALLED @webpieces/ai-hook-rules${verNote}; what is on disk is reverted, hand-edited, or predating this binary. The env entry is ${managed_env_1.BASH_CWD_ENV_KEY}=${managed_env_1.BASH_CWD_ENV_VALUE}, which pins the Bash cwd to the project root, identically for every subagent because settings env is inherited.`,
|
|
94
|
+
` → ${(0, l0_fault_codes_1.l0MatrixCitation)(l0_fault_codes_1.L0_FAULT_SHIM_STALE)}`,
|
|
95
|
+
'',
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* WHERE IT WAS MEASURED, in the deny itself and not only in the logs. #574 put `root=` and
|
|
100
|
+
* `projectDir=` on every L1 invocation line (see decision-log / ClaudeEnv.projectDirForLog, whose
|
|
101
|
+
* `<unset>` token keeps "variable absent" distinguishable from "set to empty"). The log is forensics
|
|
102
|
+
* AFTER the fact; the deny is what a blocked agent reads IN the moment, and the absence of exactly
|
|
103
|
+
* these two fields is what sent a real agent chasing the wrong mechanism for four cures. Same field
|
|
104
|
+
* names on purpose, so the deny text and the log lines grep together.
|
|
105
|
+
*
|
|
106
|
+
* Agreement is the routine case; DISAGREEMENT is the signature of the session-root-vs-cwd split this
|
|
107
|
+
* guard was rewritten to make unconstructible, so it gets said out loud rather than left to inference.
|
|
108
|
+
*/
|
|
109
|
+
measured() {
|
|
110
|
+
if (this.safeRoot === '')
|
|
111
|
+
return [];
|
|
112
|
+
return [
|
|
113
|
+
'Where this was measured:',
|
|
114
|
+
` root=${this.safeRoot} - the tree whose shim was compared, and the one to repair`,
|
|
115
|
+
` projectDir=${this.projectDir} - CLAUDE_PROJECT_DIR as this process sees it; <unset> = absent, not set-but-empty`,
|
|
116
|
+
` → ${this.verdict()}`,
|
|
117
|
+
'',
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
verdict() {
|
|
121
|
+
return this.callerIsInTheTree()
|
|
122
|
+
? 'These two AGREE, so this is the ordinary case - the tree you are in is the tree being judged.'
|
|
123
|
+
: 'These two DISAGREE - the tree being judged is NOT the one CLAUDE_PROJECT_DIR names, so cure the root= tree and do not assume your cwd is it.';
|
|
124
|
+
}
|
|
125
|
+
/** True when the tree needing repair is the caller's own tree — the input the caller branch gates on. */
|
|
126
|
+
callerIsInTheTree() {
|
|
127
|
+
return this.safeRoot === this.projectDir;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* THE CALLER-GATED BRANCH. It changes the WORDS ONLY — never the block/allow decision, never which
|
|
131
|
+
* command is printed, and never which tree anything acts on (that is decided from the path, which is
|
|
132
|
+
* why the deleted AgentIdentity class is NOT coming back for anything but message shape; agent
|
|
133
|
+
* identity was measured untrustworthy as a location signal when a worktree agent resumed on the
|
|
134
|
+
* primary clone after its tree was reaped).
|
|
135
|
+
*
|
|
136
|
+
* Two inputs, both already on hand:
|
|
137
|
+
* 1. `inSubagent` — from the payload's `agent_id`, which Claude Code populates ONLY off the main
|
|
138
|
+
* loop (main falls back to the session id, so the field is absent there). `agent_type` is NOT
|
|
139
|
+
* usable: it is always populated and discriminates nothing. REQUIRED for the same reason
|
|
140
|
+
* `drifted` is — an optional flag would let a caller keep emitting the main-loop text.
|
|
141
|
+
* 2. root= vs projectDir= — different means the caller is not standing in the tree to repair.
|
|
142
|
+
*
|
|
143
|
+
* A main agent, and a subagent whose cwd IS the tree, get the cure and nothing else: they can run it,
|
|
144
|
+
* see the result and commit it. A WORKTREE-ISOLATED subagent gets one extra step, and only because it
|
|
145
|
+
* is true — MEASURED 2026-08-11: such an agent CAN run `cd <main> && pnpm exec wp-upgrade-shim` and it
|
|
146
|
+
* works (the harness refuses cross-tree GIT operations, not this), but it can neither verify nor
|
|
147
|
+
* commit the result, because `git -C <main>` is refused. So the escalation is of the COMMIT, not of
|
|
148
|
+
* the repair, and the text must never tell it a local cure cannot work — the older wording asserted
|
|
149
|
+
* exactly that and was false in the window where it fired (measured 2026-08-10: a worktree subagent
|
|
150
|
+
* cured in place and the block lifted, with the deny's own root= naming that worktree).
|
|
151
|
+
*
|
|
152
|
+
* WHAT IS DELIBERATELY GONE: the "ask the coordinator to run pnpm install so both trees are on the
|
|
153
|
+
* same @webpieces version" clause. Both hooks are registered ABSOLUTE, so every tree is already
|
|
154
|
+
* judged by MAIN's shim and MAIN's binary — there is no version alignment left to ask for, and
|
|
155
|
+
* asking sends an agent after a non-problem.
|
|
156
|
+
*/
|
|
157
|
+
caller() {
|
|
158
|
+
// No governing root to name means no `Where this was measured` section either, so there is no
|
|
159
|
+
// root= for this text to point at and nothing to escalate ABOUT. Silence beats a dangling field.
|
|
160
|
+
if (this.safeRoot === '' || !this.inSubagent || this.callerIsInTheTree())
|
|
161
|
+
return [];
|
|
162
|
+
return [
|
|
163
|
+
'You are a SUBAGENT and root= is not the tree you are standing in, so this takes TWO steps:',
|
|
164
|
+
' 1. Run Fix Option 1 below exactly as printed. It is already anchored to the tree that must change, and a worktree-isolated subagent CAN run it against another tree - that was measured and it works, so never conclude a local cure cannot work.',
|
|
165
|
+
` 2. Then ESCALATE THE COMMIT, which is the part you cannot do: git -C ${this.safeRoot} is refused here, so you can neither verify nor commit what the cure regenerated. Tell the coordinator to run git status in ${this.safeRoot} and commit the regenerated shim.`,
|
|
166
|
+
'',
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
stillAllowed() {
|
|
170
|
+
return [
|
|
171
|
+
'Still allowed while this block is up:',
|
|
172
|
+
' - any Read',
|
|
173
|
+
' - any Write/Edit whose target is webpieces.config.json',
|
|
174
|
+
' - every command on the L0 allowlist, including both Fix Options below',
|
|
175
|
+
' THIS IS NOT A DEADLOCK: both options are explicitly ALLOWED through, so run one YOURSELF now - do not hand it back to the human. Every OTHER tool call is blocked until all three match again.',
|
|
176
|
+
'',
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The two cures, house-numbered. ORDER IS LOAD-BEARING: wp-upgrade-shim LEADS because it is the only
|
|
181
|
+
* cure that repairs all three managed surfaces, it touches no config and imports only fs/path, so it
|
|
182
|
+
* runs on a tree too broken to load the rule engine. The `cp` stays last as the pre-0.4.408 fallback.
|
|
183
|
+
*
|
|
184
|
+
* Both are anchored with a leading `cd <root> &&` when the root allows it — CD_PREFIX_*_ANCHORED
|
|
185
|
+
* tolerates exactly that one prefix (locked by unit test), and it is what keeps the cure curable when
|
|
186
|
+
* the AI's cwd is a DIFFERENT tree than the one being judged. OPTION 2 is a relative-path `cp`, so it
|
|
187
|
+
* is even MORE cwd-sensitive than OPTION 1 — it is anchored too. Never a SECOND `cd … &&`: the
|
|
188
|
+
* allowlist matches the whole command and three segments are denied.
|
|
189
|
+
*/
|
|
190
|
+
fixOptions() {
|
|
191
|
+
const anchor = (cmd) => (this.cdOk ? `cd ${this.safeRoot} && ${cmd}` : cmd);
|
|
192
|
+
return [
|
|
193
|
+
' Fix Option 1: (preferred) the only cure that repairs all three managed things, and it runs on a broken tree',
|
|
194
|
+
` run EXACTLY: '${anchor(l0_allowlist_1.UPGRADE_SHIM_CMD)}'`,
|
|
195
|
+
` Fix Option 2: PARTIAL - repairs ${shim_1.SHIM_MARKER} only. Pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where Fix Option 1 does not exist yet; then upgrade and run Fix Option 1.`,
|
|
196
|
+
` run EXACTLY: '${anchor(l0_allowlist_1.RESTORE_SHIM_CMD)}'`,
|
|
197
|
+
` NOT an option: do NOT use the bare '${l0_allowlist_1.INSTALL_HOOKS_CMD}' here - it also migrates your config and PROMPTS for a hook target twice, which hangs a non-interactive session.`,
|
|
198
|
+
'',
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
footer() {
|
|
202
|
+
return [
|
|
203
|
+
shim_1.NO_CHAINING_RULE,
|
|
204
|
+
`If you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json rather than reverting ${shim_1.SHIM_MARKER}.`,
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// The ONE entry point its two call sites (hook-core's fault-S deny, and the L0 fault table in
|
|
209
|
+
// l0-matrix) import. The rendering lives on ShimStaleDeny above, per CLAUDE.md; this is the seam.
|
|
210
|
+
// webpieces-disable no-function-outside-class -- one-line constructor+render seam for ShimStaleDeny, in the dependency-free shim module (it must stay callable from a tree too broken to build a DI container).
|
|
95
211
|
function shimStaleDenyReason(installedVersion, root, drifted, inSubagent) {
|
|
96
|
-
|
|
97
|
-
const what = drifted.join(', ');
|
|
98
|
-
const safeRoot = root.replace(/["\\]/g, '');
|
|
99
|
-
const projectDir = rules_config_1.claudeEnv.projectDirForLog().replace(/["\\]/g, '');
|
|
100
|
-
// Tested against the RAW root, never the stripped one: stripping is a display-safety measure, and
|
|
101
|
-
// cd-anchoring to a path we just mangled would prescribe a cd into a directory that does not exist.
|
|
102
|
-
// A root CD_PREFIX cannot express is simply not offered as a `cd` (raw ok ⇒ safeRoot === root).
|
|
103
|
-
const cdOk = root !== '' && /^[A-Za-z0-9._/@~+-]+$/.test(root);
|
|
104
|
-
// Agreement is the routine case; DISAGREEMENT is the signature of the session-root-vs-cwd split this
|
|
105
|
-
// guard was rewritten to make unconstructible, so it gets said out loud rather than left to inference.
|
|
106
|
-
const verdict = safeRoot === projectDir
|
|
107
|
-
? 'These two AGREE, so this is the ordinary case - the tree you are in is the tree being judged.'
|
|
108
|
-
: 'These two DISAGREE - the tree being judged is NOT the one CLAUDE_PROJECT_DIR names, so cure the root= tree and do not assume your cwd is it.';
|
|
109
|
-
const rootNote = safeRoot === '' ? '' : ` WHERE THIS WAS MEASURED: root=${safeRoot} (the tree whose shim was compared - the one to repair), projectDir=${projectDir} (CLAUDE_PROJECT_DIR as this process sees it; <unset> = absent, not set-but-empty). ${verdict}`;
|
|
110
|
-
const upgrade = cdOk ? `cd ${safeRoot} && ${l0_allowlist_1.UPGRADE_SHIM_CMD}` : l0_allowlist_1.UPGRADE_SHIM_CMD;
|
|
111
|
-
// OPTION 2 is a relative-path `cp`, so it is even MORE cwd-sensitive than OPTION 1 — anchor it too.
|
|
112
|
-
const restore = cdOk ? `cd ${safeRoot} && ${l0_allowlist_1.RESTORE_SHIM_CMD}` : l0_allowlist_1.RESTORE_SHIM_CMD;
|
|
113
|
-
// The subagent sentence goes BEFORE the options, so it is read before a cure is chosen rather than
|
|
114
|
-
// after one has already been run in the wrong tree. WHICH tree is already answered by `verdict`
|
|
115
|
-
// above, from root= vs projectDir=; this adds only what is specific to a subagent — that A and B are
|
|
116
|
-
// BOTH real and fix different things, and that B is an ESCALATION because a subagent cannot reach
|
|
117
|
-
// the main clone.
|
|
118
|
-
const subagentNote = inSubagent
|
|
119
|
-
? ' YOU ARE RUNNING IN A SUBAGENT, so TWO cures are real and they fix DIFFERENT things: A makes THIS tree work now, B stops the two trees disagreeing. A - run the OPTION below exactly as printed; it is already anchored to the tree that must change, and running it from here DOES lift this block (measured), so never conclude a local cure cannot work. B - you cannot reach the main clone, so ESCALATE: ask the coordinator to run pnpm install in the main tree so both trees are on the same @webpieces version. A worktree NEEDS its own node_modules (nx, vitest and the eslint plugin all load from it); the rule is not no-install-here, it is that this tree @webpieces must EQUAL the main tree.'
|
|
120
|
-
: '';
|
|
121
|
-
return `❌ webpieces-managed hook surface was changed: ${what} no longer matches what the INSTALLED @webpieces/ai-hook-rules${verNote} expects (reverted, hand-edited, or predating this binary).${rootNote} webpieces manages THREE things and they only work as a set: ${shim_1.SHIM_MARKER} (the guard shim), the .claude/settings.json entries that register it, and the .claude/settings.json env entry ${managed_env_1.BASH_CWD_ENV_KEY}=${managed_env_1.BASH_CWD_ENV_VALUE} (which pins the Bash cwd to the project root, identically for every subagent because settings env is inherited). Every OTHER tool call is blocked until all three match again.${subagentNote} THIS IS NOT A DEADLOCK: both options below are explicitly ALLOWED through while this guard is up, so run one YOURSELF now - do not hand it back to the human. OPTION 1 (preferred - the only one that repairs all three, and it runs on a broken tree) - run EXACTLY this command: '${upgrade}'. OPTION 2 (PARTIAL - repairs ${shim_1.SHIM_MARKER} only; pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where OPTION 1 does not exist yet, then upgrade and run OPTION 1) - run EXACTLY this command: '${restore}'. Do NOT use the bare '${l0_allowlist_1.INSTALL_HOOKS_CMD}' here: it also migrates your config and PROMPTS for a hook target twice, which hangs a non-interactive session. ${shim_1.NO_CHAINING_RULE} If you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json rather than reverting these files.`;
|
|
212
|
+
return new ShimStaleDeny(installedVersion, root, drifted, inSubagent).render();
|
|
122
213
|
}
|
|
123
214
|
//# sourceMappingURL=shim-deny-reason.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shim-deny-reason.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-deny-reason.ts"],"names":[],"mappings":"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","sourcesContent":["import { claudeEnv } from '@webpieces/rules-config';\n\nimport { INSTALL_HOOKS_CMD, RESTORE_SHIM_CMD, UPGRADE_SHIM_CMD } from './l0-allowlist';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { NO_CHAINING_RULE, SHIM_MARKER } from './shim';\n\n/**\n * THE FAIL-CLOSED DENY TEXT for a drifted managed hook surface (L0 fault S) — its own module because\n * shim.ts is at its line cap and this is one cohesive unit: the words a blocked agent reads, and\n * nothing else. It imports FROM shim.ts and is never imported BY it, so the graph stays acyclic.\n */\n// The fail-closed deny text for a drifted MANAGED HOOK SURFACE, built from the single-source cure\n// constants + NO_CHAINING_RULE.\n//\n// `drifted` names WHICH of the three managed things moved — .claude/webpieces/ai-hook.sh, the\n// .claude/settings.json hook registration, and its managed env entry\n// CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR (see hook-registration.ts). It is REQUIRED, not optional:\n// this used to be a shim-only message, and an optional list would let a caller silently keep emitting\n// the one-file text after the surface grew — which is the \"two spellings of one thing\" shape the\n// compatibility policy rejects.\n//\n// It was FOUR. guarantee-root.sh (L-1) is gone: the guard hooks are registered ABSOLUTE now, so the\n// launch guarantee L-1 provided is structural and there is no second .sh file to keep byte-locked.\n//\n// `installedVersion` names WHICH webpieces the cure re-arms to (the binary is that\n// version); pass '' to omit the note rather than print an empty one. `root` is the tree the deciding\n// binary GOVERNS (governingShimRoot) — naming it, and anchoring the cure to it with a leading\n// `cd <root> &&` (which CD_PREFIX_*_ANCHORED already tolerates, locked by a unit test), is what keeps\n// the cure curable when the AI's cwd is a DIFFERENT tree than the one being judged. Pass '' to omit.\n//\n// The cause list is deliberately a LIST: it used to assert flatly \"(it was reverted or hand-edited)\",\n// which is frequently FALSE — the common case is a shim whose logic simply predates this binary — and\n// that false certainty sent a real agent hunting for a tamper that never happened.\n//\n// WHERE IT WAS MEASURED, in the deny itself and not only in the logs. #574 put `root=` and\n// `projectDir=` on every L1 invocation line (see decision-log / ClaudeEnv.projectDirForLog, whose\n// `<unset>` token keeps \"variable absent\" distinguishable from \"set to empty\"). The log is forensics\n// AFTER the fact; the deny is what a blocked agent reads IN the moment, and the absence of exactly\n// these two fields is what sent a real agent chasing the wrong mechanism for four cures. Same field\n// names on purpose, so the deny text and the log lines grep together.\n//\n// THE MESSAGE DIET IS PART OF THE CONTRACT, and this deny is where it regresses. main landed a\n// deliberate L0 message diet (384cdae) and blocks grow straight back into a wall of text when each new\n// finding argues its case here; at eleven sections this one was ~4,000 chars, and `denyBudget` in\n// shim-deny-reason.spec.ts now fails the build if it climbs back. Keep what CHANGES what the reader\n// types (the two exact commands, that it is not a deadlock, the no-chaining rule, root=/projectDir=);\n// cut what merely argues. What was cut and why:\n// - the \"up to and including 0.4.588 OPTION 1 was inert, so EMPTY OUTPUT means it did not run\"\n// paragraph. It was UNREACHABLE by construction: this text and `wp-upgrade-shim` ship in the SAME\n// package at the SAME version, so a binary new enough to print this sentence necessarily has the\n// process entry point 0.4.589 added. The incident is still recorded where it can bite —\n// bin-process-entry.spec.ts, which asserts the entry point exists.\n// - the \"registered ABSOLUTE / a worktree borrows by walking up\" aside. That is the DRIFT fault's\n// subject (WP_BORROW_NOTE in shim.ts) and it is restated in the guard-matrix doc this deny points at.\n// - \"they are GENERATED and committed ... must NOT be reverted by hand\", which said twice over what\n// the closing sentence already says once.\n//\n// CONSTRAINT: the returned string must contain no `\"` and no `\\` — it is JSON-serialized by denyJson()\n// (a stray quote/backslash would corrupt the PreToolUse decision payload, not just the text). That is an\n// INVARIANT, not a hope, so every interpolated path is STRIPPED of both rather than trusted — a\n// Windows-style path or an odd directory name must not be able to corrupt the decision. Locked by unit\n// tests. An unusual root is also dropped from the `cd` cure rather than quoted (CD_PREFIX would reject it).\n//\n// `inSubagent` comes from the PreToolUse payload's `agent_id`, which Claude Code delivers on stdin and\n// populates ONLY off the main loop (main falls back to the session id, so the field is absent there).\n// `agent_type` is NOT usable for this — it is always populated and discriminates nothing. It is a\n// REQUIRED parameter for the same reason `drifted` is — an optional flag would let a caller keep\n// emitting the main-loop text from a subagent, which is the case that most needs the extra line.\n//\n// THE SUBAGENT SENTENCE REASONS FROM root= vs projectDir=, IT DOES NOT ASSERT A FIXED CONCLUSION. It\n// used to say flatly \"the hooks resolve through CLAUDE_PROJECT_DIR, which names the MAIN tree, so a cure\n// run only here CANNOT lift this block\". That is a POST-FLIP fact stated during the PRE-FLIP window, and\n// it is FALSE exactly when it fires: measured 2026-08-10, a worktree subagent hit fault S, ran OPTION 1\n// in ITS OWN worktree, and the block lifted — because the registration still in force was the RELATIVE\n// three-hook form, so the worktree's own ai-hook.sh ran against the worktree's own node_modules. The\n// deny's own `root=` field named the worktree while the sentence insisted otherwise.\n// WHICH tree to cure is already answered, for BOTH windows, by the root=/projectDir= verdict above:\n// committedShimStale compares shimPath(root) and `root` is the tree the RUNNING binary came from, so\n// repairing the root= tree clears the fault whichever registration form is live, and the cure is\n// cd-anchored there. Telling an agent its cure cannot work, while it demonstrably can, costs more than\n// saying nothing.\n//\n// SO THE SUBAGENT SENTENCE CARRIES THE PART THAT IS ACTUALLY SUBAGENT-SPECIFIC: there are TWO real\n// cures and they fix different things. A — run the printed cure here; it works, and it makes THIS tree\n// work NOW. B — a subagent cannot reach the main clone, so aligning the two trees is an ESCALATION\n// (\"ask the coordinator to run pnpm install in the main tree\"), and that is what stops the trees\n// disagreeing. Doing only A leaves the repo with two trees on two @webpieces releases — the live state\n// on 2026-08-10, main clone on 0.4.616 while origin/main and three worktrees were on 0.4.624. And the\n// rule it states is deliberately NOT \"do not install in a worktree\": a worktree NEEDS its own\n// node_modules (nx, vitest and the eslint plugin all execute there and load from it). The rule is that\n// its @webpieces must EQUAL the main tree's — the older WP_BORROW_NOTE wording got this backwards in\n// both directions at different times.\n// webpieces-disable no-function-outside-class -- pure string builder over exported constants; the single source of the self-guard deny text now that the sh copy is gone.\nexport function shimStaleDenyReason(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean): string {\n const verNote = installedVersion ? ` (installed version ${installedVersion})` : '';\n const what = drifted.join(', ');\n const safeRoot = root.replace(/[\"\\\\]/g, '');\n const projectDir = claudeEnv.projectDirForLog().replace(/[\"\\\\]/g, '');\n // Tested against the RAW root, never the stripped one: stripping is a display-safety measure, and\n // cd-anchoring to a path we just mangled would prescribe a cd into a directory that does not exist.\n // A root CD_PREFIX cannot express is simply not offered as a `cd` (raw ok ⇒ safeRoot === root).\n const cdOk = root !== '' && /^[A-Za-z0-9._/@~+-]+$/.test(root);\n // Agreement is the routine case; DISAGREEMENT is the signature of the session-root-vs-cwd split this\n // guard was rewritten to make unconstructible, so it gets said out loud rather than left to inference.\n const verdict = safeRoot === projectDir\n ? 'These two AGREE, so this is the ordinary case - the tree you are in is the tree being judged.'\n : 'These two DISAGREE - the tree being judged is NOT the one CLAUDE_PROJECT_DIR names, so cure the root= tree and do not assume your cwd is it.';\n const rootNote = safeRoot === '' ? '' : ` WHERE THIS WAS MEASURED: root=${safeRoot} (the tree whose shim was compared - the one to repair), projectDir=${projectDir} (CLAUDE_PROJECT_DIR as this process sees it; <unset> = absent, not set-but-empty). ${verdict}`;\n const upgrade = cdOk ? `cd ${safeRoot} && ${UPGRADE_SHIM_CMD}` : UPGRADE_SHIM_CMD;\n // OPTION 2 is a relative-path `cp`, so it is even MORE cwd-sensitive than OPTION 1 — anchor it too.\n const restore = cdOk ? `cd ${safeRoot} && ${RESTORE_SHIM_CMD}` : RESTORE_SHIM_CMD;\n // The subagent sentence goes BEFORE the options, so it is read before a cure is chosen rather than\n // after one has already been run in the wrong tree. WHICH tree is already answered by `verdict`\n // above, from root= vs projectDir=; this adds only what is specific to a subagent — that A and B are\n // BOTH real and fix different things, and that B is an ESCALATION because a subagent cannot reach\n // the main clone.\n const subagentNote = inSubagent\n ? ' YOU ARE RUNNING IN A SUBAGENT, so TWO cures are real and they fix DIFFERENT things: A makes THIS tree work now, B stops the two trees disagreeing. A - run the OPTION below exactly as printed; it is already anchored to the tree that must change, and running it from here DOES lift this block (measured), so never conclude a local cure cannot work. B - you cannot reach the main clone, so ESCALATE: ask the coordinator to run pnpm install in the main tree so both trees are on the same @webpieces version. A worktree NEEDS its own node_modules (nx, vitest and the eslint plugin all load from it); the rule is not no-install-here, it is that this tree @webpieces must EQUAL the main tree.'\n : '';\n return `❌ webpieces-managed hook surface was changed: ${what} no longer matches what the INSTALLED @webpieces/ai-hook-rules${verNote} expects (reverted, hand-edited, or predating this binary).${rootNote} webpieces manages THREE things and they only work as a set: ${SHIM_MARKER} (the guard shim), the .claude/settings.json entries that register it, and the .claude/settings.json env entry ${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE} (which pins the Bash cwd to the project root, identically for every subagent because settings env is inherited). Every OTHER tool call is blocked until all three match again.${subagentNote} THIS IS NOT A DEADLOCK: both options below are explicitly ALLOWED through while this guard is up, so run one YOURSELF now - do not hand it back to the human. OPTION 1 (preferred - the only one that repairs all three, and it runs on a broken tree) - run EXACTLY this command: '${upgrade}'. OPTION 2 (PARTIAL - repairs ${SHIM_MARKER} only; pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where OPTION 1 does not exist yet, then upgrade and run OPTION 1) - run EXACTLY this command: '${restore}'. Do NOT use the bare '${INSTALL_HOOKS_CMD}' here: it also migrates your config and PROMPTS for a hook target twice, which hangs a non-interactive session. ${NO_CHAINING_RULE} If you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json rather than reverting these files.`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shim-deny-reason.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-deny-reason.ts"],"names":[],"mappings":"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","sourcesContent":["import { claudeEnv } from '@webpieces/rules-config';\n\nimport { L0_FAULT_SHIM_STALE, l0GuardHeader, l0MatrixCitation } from '../core/l0-fault-codes';\nimport { INSTALL_HOOKS_CMD, RESTORE_SHIM_CMD, UPGRADE_SHIM_CMD } from './l0-allowlist';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { NO_CHAINING_RULE, SHIM_MARKER } from './shim';\n\n/**\n * THE FAIL-CLOSED DENY TEXT for a drifted managed hook surface (L0 fault S) — its own module because\n * shim.ts is at its line cap and this is one cohesive unit: the words a blocked agent reads, and\n * nothing else. It imports FROM shim.ts and is never imported BY it, so the graph stays acyclic.\n *\n * IT IS RENDERED IN THE HOUSE FORMAT, the same skeleton core/report.ts (formatReport) gives every L1\n * and L2 deny: a header naming what was blocked, a `[guard-name]` block listing the offenders with a\n * one-line `→ why`, what is still allowed, then numbered `Fix Option N:` lines each with its command\n * on its own line. L0 used to be the ONLY layer in webpieces that answered in one unbroken paragraph —\n * ~3,300 characters of it here — so the two commands that matter were buried in prose and there was no\n * guard name to grep for. Nothing about the DECISION changed; only the shape of the words.\n *\n * SHORTER IS NOT THE GOAL, SCANNABLE IS. The budget below still exists (a paragraph regrows when every\n * new finding argues its case here), but a section that earns a line gets a line.\n *\n * CONSTRAINT: the returned string must contain no `\"` and no `\\` — it is JSON-serialized by denyJson()\n * (a stray quote/backslash would corrupt the PreToolUse decision payload, not just the text). That is an\n * INVARIANT, not a hope, so every interpolated path is STRIPPED of both rather than trusted — a\n * Windows-style path or an odd directory name must not be able to corrupt the decision. Locked by unit\n * tests. An unusual root is also dropped from the `cd` cure rather than quoted (CD_PREFIX would reject it).\n *\n * NEWLINES ARE SAFE HERE AND NEEDED NO NEW MECHANISM. denyJson() runs JSON.stringify, which escapes a\n * real newline to the two-character `\\n` on the wire, and Claude Code's parser turns it back. This is\n * already proven in production — every L1 deny is formatReport()'s multi-line string down this exact\n * path. The sh half of L0 (faults D/X/U/K in renderShim) cannot do this: it printf's REASON into a JSON\n * string literal, so it spells its newlines `${NL}` the same way it spells the ANSI escape `${ESC}`.\n * A real newline is neither a quote nor a backslash, so the JSON-safety assertions above are untouched.\n */\nclass ShimStaleDeny {\n private readonly installedVersion: string;\n /** The governing root, STRIPPED of `\"` and `\\` for display. '' when there is none to name. */\n private readonly safeRoot: string;\n /** CLAUDE_PROJECT_DIR as this process sees it, stripped the same way. */\n private readonly projectDir: string;\n private readonly drifted: readonly string[];\n private readonly inSubagent: boolean;\n /**\n * Whether the RAW root can carry a leading `cd <root> &&`. Tested against the raw root, never the\n * stripped one: stripping is a display-safety measure, and cd-anchoring to a path we just mangled\n * would prescribe a cd into a directory that does not exist. A root CD_PREFIX cannot express is\n * simply not offered as a `cd` (raw ok ⇒ safeRoot === root).\n */\n private readonly cdOk: boolean;\n\n constructor(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean) {\n this.installedVersion = installedVersion;\n this.safeRoot = root.replace(/[\"\\\\]/g, '');\n this.projectDir = claudeEnv.projectDirForLog().replace(/[\"\\\\]/g, '');\n this.drifted = drifted;\n this.inSubagent = inSubagent;\n this.cdOk = root !== '' && /^[A-Za-z0-9._/@~+-]+$/.test(root);\n }\n\n render(): string {\n return [\n ...this.header(),\n ...this.measured(),\n ...this.caller(),\n ...this.stillAllowed(),\n ...this.fixOptions(),\n ...this.footer(),\n ].join('\\n');\n }\n\n /**\n * `[managed-hook-surface]` and the surfaces that drifted, one per line.\n *\n * `drifted` names WHICH of the three managed things moved — .claude/webpieces/ai-hook.sh, the\n * .claude/settings.json hook registration, and its managed env entry (see hook-registration.ts). It\n * is REQUIRED, not optional: this used to be a shim-only message, and an optional list would let a\n * caller silently keep emitting the one-file text after the surface grew — the \"two spellings of one\n * thing\" shape the compatibility policy rejects. It was FOUR; guarantee-root.sh (L-1) is gone,\n * because the guard hooks are registered ABSOLUTE now and there is no second .sh to keep byte-locked.\n *\n * THE CAUSE IS A LIST. It used to assert flatly \"(it was reverted or hand-edited)\", which is\n * frequently FALSE — the common case is a shim whose logic simply predates this binary — and that\n * false certainty sent a real agent hunting for a tamper that never happened.\n */\n private header(): string[] {\n const verNote = this.installedVersion ? ` (installed version ${this.installedVersion})` : '';\n const n = this.drifted.length;\n const label = n === 1 ? '1 surface drifted' : `${String(n)} surfaces drifted`;\n return [\n '❌ webpieces ai-hooks blocked this call: a webpieces-managed hook surface no longer matches the installed guard binary.',\n '',\n l0GuardHeader(L0_FAULT_SHIM_STALE, label),\n ...this.drifted.map((surface: string): string => ` ${surface}`),\n ` → webpieces manages those THREE things as ONE set, GENERATED by the INSTALLED @webpieces/ai-hook-rules${verNote}; what is on disk is reverted, hand-edited, or predating this binary. The env entry is ${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE}, which pins the Bash cwd to the project root, identically for every subagent because settings env is inherited.`,\n ` → ${l0MatrixCitation(L0_FAULT_SHIM_STALE)}`,\n '',\n ];\n }\n\n /**\n * WHERE IT WAS MEASURED, in the deny itself and not only in the logs. #574 put `root=` and\n * `projectDir=` on every L1 invocation line (see decision-log / ClaudeEnv.projectDirForLog, whose\n * `<unset>` token keeps \"variable absent\" distinguishable from \"set to empty\"). The log is forensics\n * AFTER the fact; the deny is what a blocked agent reads IN the moment, and the absence of exactly\n * these two fields is what sent a real agent chasing the wrong mechanism for four cures. Same field\n * names on purpose, so the deny text and the log lines grep together.\n *\n * Agreement is the routine case; DISAGREEMENT is the signature of the session-root-vs-cwd split this\n * guard was rewritten to make unconstructible, so it gets said out loud rather than left to inference.\n */\n private measured(): string[] {\n if (this.safeRoot === '') return [];\n return [\n 'Where this was measured:',\n ` root=${this.safeRoot} - the tree whose shim was compared, and the one to repair`,\n ` projectDir=${this.projectDir} - CLAUDE_PROJECT_DIR as this process sees it; <unset> = absent, not set-but-empty`,\n ` → ${this.verdict()}`,\n '',\n ];\n }\n\n private verdict(): string {\n return this.callerIsInTheTree()\n ? 'These two AGREE, so this is the ordinary case - the tree you are in is the tree being judged.'\n : 'These two DISAGREE - the tree being judged is NOT the one CLAUDE_PROJECT_DIR names, so cure the root= tree and do not assume your cwd is it.';\n }\n\n /** True when the tree needing repair is the caller's own tree — the input the caller branch gates on. */\n private callerIsInTheTree(): boolean {\n return this.safeRoot === this.projectDir;\n }\n\n /**\n * THE CALLER-GATED BRANCH. It changes the WORDS ONLY — never the block/allow decision, never which\n * command is printed, and never which tree anything acts on (that is decided from the path, which is\n * why the deleted AgentIdentity class is NOT coming back for anything but message shape; agent\n * identity was measured untrustworthy as a location signal when a worktree agent resumed on the\n * primary clone after its tree was reaped).\n *\n * Two inputs, both already on hand:\n * 1. `inSubagent` — from the payload's `agent_id`, which Claude Code populates ONLY off the main\n * loop (main falls back to the session id, so the field is absent there). `agent_type` is NOT\n * usable: it is always populated and discriminates nothing. REQUIRED for the same reason\n * `drifted` is — an optional flag would let a caller keep emitting the main-loop text.\n * 2. root= vs projectDir= — different means the caller is not standing in the tree to repair.\n *\n * A main agent, and a subagent whose cwd IS the tree, get the cure and nothing else: they can run it,\n * see the result and commit it. A WORKTREE-ISOLATED subagent gets one extra step, and only because it\n * is true — MEASURED 2026-08-11: such an agent CAN run `cd <main> && pnpm exec wp-upgrade-shim` and it\n * works (the harness refuses cross-tree GIT operations, not this), but it can neither verify nor\n * commit the result, because `git -C <main>` is refused. So the escalation is of the COMMIT, not of\n * the repair, and the text must never tell it a local cure cannot work — the older wording asserted\n * exactly that and was false in the window where it fired (measured 2026-08-10: a worktree subagent\n * cured in place and the block lifted, with the deny's own root= naming that worktree).\n *\n * WHAT IS DELIBERATELY GONE: the \"ask the coordinator to run pnpm install so both trees are on the\n * same @webpieces version\" clause. Both hooks are registered ABSOLUTE, so every tree is already\n * judged by MAIN's shim and MAIN's binary — there is no version alignment left to ask for, and\n * asking sends an agent after a non-problem.\n */\n private caller(): string[] {\n // No governing root to name means no `Where this was measured` section either, so there is no\n // root= for this text to point at and nothing to escalate ABOUT. Silence beats a dangling field.\n if (this.safeRoot === '' || !this.inSubagent || this.callerIsInTheTree()) return [];\n return [\n 'You are a SUBAGENT and root= is not the tree you are standing in, so this takes TWO steps:',\n ' 1. Run Fix Option 1 below exactly as printed. It is already anchored to the tree that must change, and a worktree-isolated subagent CAN run it against another tree - that was measured and it works, so never conclude a local cure cannot work.',\n ` 2. Then ESCALATE THE COMMIT, which is the part you cannot do: git -C ${this.safeRoot} is refused here, so you can neither verify nor commit what the cure regenerated. Tell the coordinator to run git status in ${this.safeRoot} and commit the regenerated shim.`,\n '',\n ];\n }\n\n private stillAllowed(): string[] {\n return [\n 'Still allowed while this block is up:',\n ' - any Read',\n ' - any Write/Edit whose target is webpieces.config.json',\n ' - every command on the L0 allowlist, including both Fix Options below',\n ' THIS IS NOT A DEADLOCK: both options are explicitly ALLOWED through, so run one YOURSELF now - do not hand it back to the human. Every OTHER tool call is blocked until all three match again.',\n '',\n ];\n }\n\n /**\n * The two cures, house-numbered. ORDER IS LOAD-BEARING: wp-upgrade-shim LEADS because it is the only\n * cure that repairs all three managed surfaces, it touches no config and imports only fs/path, so it\n * runs on a tree too broken to load the rule engine. The `cp` stays last as the pre-0.4.408 fallback.\n *\n * Both are anchored with a leading `cd <root> &&` when the root allows it — CD_PREFIX_*_ANCHORED\n * tolerates exactly that one prefix (locked by unit test), and it is what keeps the cure curable when\n * the AI's cwd is a DIFFERENT tree than the one being judged. OPTION 2 is a relative-path `cp`, so it\n * is even MORE cwd-sensitive than OPTION 1 — it is anchored too. Never a SECOND `cd … &&`: the\n * allowlist matches the whole command and three segments are denied.\n */\n private fixOptions(): string[] {\n const anchor = (cmd: string): string => (this.cdOk ? `cd ${this.safeRoot} && ${cmd}` : cmd);\n return [\n ' Fix Option 1: (preferred) the only cure that repairs all three managed things, and it runs on a broken tree',\n ` run EXACTLY: '${anchor(UPGRADE_SHIM_CMD)}'`,\n ` Fix Option 2: PARTIAL - repairs ${SHIM_MARKER} only. Pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where Fix Option 1 does not exist yet; then upgrade and run Fix Option 1.`,\n ` run EXACTLY: '${anchor(RESTORE_SHIM_CMD)}'`,\n ` NOT an option: do NOT use the bare '${INSTALL_HOOKS_CMD}' here - it also migrates your config and PROMPTS for a hook target twice, which hangs a non-interactive session.`,\n '',\n ];\n }\n\n private footer(): string[] {\n return [\n NO_CHAINING_RULE,\n `If you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json rather than reverting ${SHIM_MARKER}.`,\n ];\n }\n}\n\n// The ONE entry point its two call sites (hook-core's fault-S deny, and the L0 fault table in\n// l0-matrix) import. The rendering lives on ShimStaleDeny above, per CLAUDE.md; this is the seam.\n// webpieces-disable no-function-outside-class -- one-line constructor+render seam for ShimStaleDeny, in the dependency-free shim module (it must stay callable from a tree too broken to build a DI container).\nexport function shimStaleDenyReason(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean): string {\n return new ShimStaleDeny(installedVersion, root, drifted, inSubagent).render();\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DRIFT_INVERSE_FIX_SH = "WP_BRANCH=\"$(git -C \"$ROOT\" branch --show-current 2>/dev/null)\"\n [ -n \"$WP_LOG_DIR\" ] || wp_resolve_log_dir\n if [ \"$WP_BRANCH\" = main ]; then\n WP_FIX=\" Fix Option 1: (preferred) you are on main and want what origin pins - move forward${NL} run EXACTLY: 'git checkout main && git pull origin main', then 'pnpm install'${NL} Fix Option 2: you mean to stay on this code - the downgrade is the point${NL} run EXACTLY: 'pnpm install'\"\n else\n WP_LOG_PATHS=\"$WP_LOG_DIR/L0-shim/\"\n [ \"$WP_PRIMARY_LOG_DIR\" = \"$WP_LOG_DIR\" ] || WP_LOG_PATHS=\"${WP_LOG_PATHS}${NL} and, for the primary clone: $WP_PRIMARY_LOG_DIR/L0-shim/\"\n WP_FIX=\" Fix Option 1: (preferred) off main, align node_modules to YOUR branch pin - usually right${NL} run EXACTLY: 'pnpm install'${NL} Fix Option 2: you actually need the NEWER pin ON THIS BRANCH - there is no cure to run, and this guard will not invent one${NL} Do NOT reach for 'git pull origin main': pulling main into a feature branch destroys the fork point the build gate --base and the PR review diff are computed from, and the guards block it.${NL} You hit a weird case of needing a downgrade. Contact Dean - he needs the audit logs to understand why you are downgrading, so the guard logic can account for it.${NL} L0 audit logs: $WP_LOG_PATHS\"\n fi";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DRIFT_INVERSE_FIX_SH = void 0;
|
|
4
|
+
const log_streams_1 = require("../core/log-streams");
|
|
5
|
+
const l0_allowlist_1 = require("./l0-allowlist");
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Shell fragment: the Fix Options for the INVERSE half of L0 fault D — node_modules is NEWER than the
|
|
8
|
+
// pin (or the two could not be ordered), so a bare `pnpm install` is a DOWNGRADE.
|
|
9
|
+
//
|
|
10
|
+
// Its own module for the same reason shim-audit-log.ts is: shim.ts renders the whole shim body and is
|
|
11
|
+
// at its file-size cap. It imports FROM ./l0-allowlist and is never imported BY it, so the graph stays
|
|
12
|
+
// acyclic, and it stays as dependency-free as the rest of the shim — this text has to render on a tree
|
|
13
|
+
// too broken to load the rule engine.
|
|
14
|
+
//
|
|
15
|
+
// WHY THE FIX OPTIONS ARE COMPUTED RATHER THAN LISTED (2026-08-10, audit finding C6).
|
|
16
|
+
//
|
|
17
|
+
// This message used to offer a bare `git pull origin main` on every branch, and the L0 allowlist
|
|
18
|
+
// terminally ALLOWED it — so on a FEATURE branch the guard told the agent to merge main into the branch
|
|
19
|
+
// and then waved the command past redirect-how-to-merge-main, the guard whose whole job is stopping
|
|
20
|
+
// exactly that. The fork point it destroys is what the 3-point merge, `nx affected --base=` and the PR
|
|
21
|
+
// review diff are computed from, so nothing fails at the time; it surfaces later as a build that covered
|
|
22
|
+
// the wrong scope and a PR diff describing work nobody did. A menu whose first option is blocked on the
|
|
23
|
+
// branch you are standing on is worse than no menu, so the branch is asked instead.
|
|
24
|
+
//
|
|
25
|
+
// ON MAIN the forward move is `git checkout main && git pull origin main` — it ends ON main, merges
|
|
26
|
+
// nothing into anything, is a no-op checkout when you are already there, and it is the ONE pull spelling
|
|
27
|
+
// still on the L0 allowlist (see CHECKOUT_MAIN_PULL_BODY_ERE, which also records why it is a narrow
|
|
28
|
+
// literal). It is word for word what stale-main-bash-guard prescribes.
|
|
29
|
+
//
|
|
30
|
+
// ON A FEATURE BRANCH there is no honest forward move to name, and this deliberately does not invent
|
|
31
|
+
// one. `pnpm install` aligns node_modules to YOUR branch pin and is the usual right answer, but it is a
|
|
32
|
+
// DOWNGRADE — and every drift event logged in this repo to date has been the OTHER direction, so a
|
|
33
|
+
// feature branch that genuinely needs the NEWER pin is a shape the guard logic has never seen. Option 2
|
|
34
|
+
// therefore says that plainly and prints the L0 audit-log paths, because those logs are the evidence a
|
|
35
|
+
// real cure would have to be designed from, and an escalation nobody can act on is not an option at all.
|
|
36
|
+
//
|
|
37
|
+
// `git branch --show-current`, NOT `git rev-parse --abbrev-ref HEAD`: it answers on an UNBORN branch
|
|
38
|
+
// (`rev-parse` fatals there), and it prints an EMPTY string on a detached HEAD — which falls to the
|
|
39
|
+
// conservative, non-main half, exactly as an unknown branch should.
|
|
40
|
+
//
|
|
41
|
+
// CONSTRAINT, same as every other deny fragment: no `"` and no backslash may reach the rendered text —
|
|
42
|
+
// it is interpolated into a `REASON="…"` shell assignment and then printf'd into a JSON string.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
exports.DRIFT_INVERSE_FIX_SH = `WP_BRANCH="$(git -C "$ROOT" branch --show-current 2>/dev/null)"
|
|
45
|
+
[ -n "$WP_LOG_DIR" ] || wp_resolve_log_dir
|
|
46
|
+
if [ "$WP_BRANCH" = main ]; then
|
|
47
|
+
WP_FIX=" Fix Option 1: (preferred) you are on main and want what origin pins - move forward\${NL} run EXACTLY: '${l0_allowlist_1.CHECKOUT_MAIN_PULL_CMD}', then 'pnpm install'\${NL} Fix Option 2: you mean to stay on this code - the downgrade is the point\${NL} run EXACTLY: 'pnpm install'"
|
|
48
|
+
else
|
|
49
|
+
WP_LOG_PATHS="$WP_LOG_DIR/${log_streams_1.L0_SHIM_STREAM}/"
|
|
50
|
+
[ "$WP_PRIMARY_LOG_DIR" = "$WP_LOG_DIR" ] || WP_LOG_PATHS="\${WP_LOG_PATHS}\${NL} and, for the primary clone: $WP_PRIMARY_LOG_DIR/${log_streams_1.L0_SHIM_STREAM}/"
|
|
51
|
+
WP_FIX=" Fix Option 1: (preferred) off main, align node_modules to YOUR branch pin - usually right\${NL} run EXACTLY: 'pnpm install'\${NL} Fix Option 2: you actually need the NEWER pin ON THIS BRANCH - there is no cure to run, and this guard will not invent one\${NL} Do NOT reach for 'git pull origin main': pulling main into a feature branch destroys the fork point the build gate --base and the PR review diff are computed from, and the guards block it.\${NL} You hit a weird case of needing a downgrade. Contact Dean - he needs the audit logs to understand why you are downgrading, so the guard logic can account for it.\${NL} L0 audit logs: $WP_LOG_PATHS"
|
|
52
|
+
fi`;
|
|
53
|
+
//# sourceMappingURL=shim-drift-fix.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shim-drift-fix.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-drift-fix.ts"],"names":[],"mappings":";;;AAAA,qDAAqD;AAErD,iDAAwD;AAExD,8EAA8E;AAC9E,sGAAsG;AACtG,kFAAkF;AAClF,EAAE;AACF,sGAAsG;AACtG,uGAAuG;AACvG,uGAAuG;AACvG,sCAAsC;AACtC,EAAE;AACF,sFAAsF;AACtF,EAAE;AACF,iGAAiG;AACjG,wGAAwG;AACxG,oGAAoG;AACpG,uGAAuG;AACvG,yGAAyG;AACzG,wGAAwG;AACxG,oFAAoF;AACpF,EAAE;AACF,oGAAoG;AACpG,yGAAyG;AACzG,oGAAoG;AACpG,uEAAuE;AACvE,EAAE;AACF,qGAAqG;AACrG,wGAAwG;AACxG,mGAAmG;AACnG,wGAAwG;AACxG,uGAAuG;AACvG,yGAAyG;AACzG,EAAE;AACF,qGAAqG;AACrG,oGAAoG;AACpG,oEAAoE;AACpE,EAAE;AACF,uGAAuG;AACvG,gGAAgG;AAChG,8EAA8E;AACjE,QAAA,oBAAoB,GAAG;;;4HAGwF,qCAAsB;;kCAEhH,4BAAc;+IAC+F,4BAAc;;OAEtJ,CAAC","sourcesContent":["import { L0_SHIM_STREAM } from '../core/log-streams';\n\nimport { CHECKOUT_MAIN_PULL_CMD } from './l0-allowlist';\n\n// ---------------------------------------------------------------------------\n// Shell fragment: the Fix Options for the INVERSE half of L0 fault D — node_modules is NEWER than the\n// pin (or the two could not be ordered), so a bare `pnpm install` is a DOWNGRADE.\n//\n// Its own module for the same reason shim-audit-log.ts is: shim.ts renders the whole shim body and is\n// at its file-size cap. It imports FROM ./l0-allowlist and is never imported BY it, so the graph stays\n// acyclic, and it stays as dependency-free as the rest of the shim — this text has to render on a tree\n// too broken to load the rule engine.\n//\n// WHY THE FIX OPTIONS ARE COMPUTED RATHER THAN LISTED (2026-08-10, audit finding C6).\n//\n// This message used to offer a bare `git pull origin main` on every branch, and the L0 allowlist\n// terminally ALLOWED it — so on a FEATURE branch the guard told the agent to merge main into the branch\n// and then waved the command past redirect-how-to-merge-main, the guard whose whole job is stopping\n// exactly that. The fork point it destroys is what the 3-point merge, `nx affected --base=` and the PR\n// review diff are computed from, so nothing fails at the time; it surfaces later as a build that covered\n// the wrong scope and a PR diff describing work nobody did. A menu whose first option is blocked on the\n// branch you are standing on is worse than no menu, so the branch is asked instead.\n//\n// ON MAIN the forward move is `git checkout main && git pull origin main` — it ends ON main, merges\n// nothing into anything, is a no-op checkout when you are already there, and it is the ONE pull spelling\n// still on the L0 allowlist (see CHECKOUT_MAIN_PULL_BODY_ERE, which also records why it is a narrow\n// literal). It is word for word what stale-main-bash-guard prescribes.\n//\n// ON A FEATURE BRANCH there is no honest forward move to name, and this deliberately does not invent\n// one. `pnpm install` aligns node_modules to YOUR branch pin and is the usual right answer, but it is a\n// DOWNGRADE — and every drift event logged in this repo to date has been the OTHER direction, so a\n// feature branch that genuinely needs the NEWER pin is a shape the guard logic has never seen. Option 2\n// therefore says that plainly and prints the L0 audit-log paths, because those logs are the evidence a\n// real cure would have to be designed from, and an escalation nobody can act on is not an option at all.\n//\n// `git branch --show-current`, NOT `git rev-parse --abbrev-ref HEAD`: it answers on an UNBORN branch\n// (`rev-parse` fatals there), and it prints an EMPTY string on a detached HEAD — which falls to the\n// conservative, non-main half, exactly as an unknown branch should.\n//\n// CONSTRAINT, same as every other deny fragment: no `\"` and no backslash may reach the rendered text —\n// it is interpolated into a `REASON=\"…\"` shell assignment and then printf'd into a JSON string.\n// ---------------------------------------------------------------------------\nexport const DRIFT_INVERSE_FIX_SH = `WP_BRANCH=\"$(git -C \"$ROOT\" branch --show-current 2>/dev/null)\"\n [ -n \"$WP_LOG_DIR\" ] || wp_resolve_log_dir\n if [ \"$WP_BRANCH\" = main ]; then\n WP_FIX=\" Fix Option 1: (preferred) you are on main and want what origin pins - move forward\\${NL} run EXACTLY: '${CHECKOUT_MAIN_PULL_CMD}', then 'pnpm install'\\${NL} Fix Option 2: you mean to stay on this code - the downgrade is the point\\${NL} run EXACTLY: 'pnpm install'\"\n else\n WP_LOG_PATHS=\"$WP_LOG_DIR/${L0_SHIM_STREAM}/\"\n [ \"$WP_PRIMARY_LOG_DIR\" = \"$WP_LOG_DIR\" ] || WP_LOG_PATHS=\"\\${WP_LOG_PATHS}\\${NL} and, for the primary clone: $WP_PRIMARY_LOG_DIR/${L0_SHIM_STREAM}/\"\n WP_FIX=\" Fix Option 1: (preferred) off main, align node_modules to YOUR branch pin - usually right\\${NL} run EXACTLY: 'pnpm install'\\${NL} Fix Option 2: you actually need the NEWER pin ON THIS BRANCH - there is no cure to run, and this guard will not invent one\\${NL} Do NOT reach for 'git pull origin main': pulling main into a feature branch destroys the fork point the build gate --base and the PR review diff are computed from, and the guards block it.\\${NL} You hit a weird case of needing a downgrade. Contact Dean - he needs the audit logs to understand why you are downgrading, so the guard logic can account for it.\\${NL} L0 audit logs: $WP_LOG_PATHS\"\n fi`;\n"]}
|
|
@@ -54,6 +54,17 @@ export declare class ShimTestkit {
|
|
|
54
54
|
* second copy is a second definition of what "drift" means.
|
|
55
55
|
*/
|
|
56
56
|
stageDriftRoot(declared: string, installed: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Make `root` a real git repo whose CURRENT BRANCH is `branch`, and return it.
|
|
59
|
+
*
|
|
60
|
+
* The fault-D deny asks `git branch --show-current` to decide whether "move forward to what origin
|
|
61
|
+
* pins" is a legal command here (it is on main, and it destroys the fork point on a feature branch),
|
|
62
|
+
* so the branch is now an INPUT to the message and has to be stageable. `symbolic-ref` rather than a
|
|
63
|
+
* commit: `--show-current` answers on an UNBORN branch, so this needs no user identity, no index and
|
|
64
|
+
* no object write — which is also why a staged root with NO git dir at all keeps answering '' and
|
|
65
|
+
* lands on the conservative, non-main half.
|
|
66
|
+
*/
|
|
67
|
+
stageBranch(root: string, branch: string): string;
|
|
57
68
|
/**
|
|
58
69
|
* A repo root that DECLARES @webpieces/ai-hook-rules but has nothing installed — fault X, the
|
|
59
70
|
* ordinary fresh-clone / new-worktree case whose cure really is `pnpm install`.
|
package/src/bin/shim-testkit.js
CHANGED
|
@@ -103,6 +103,21 @@ class ShimTestkit {
|
|
|
103
103
|
fs.writeFileSync(path.join(manifestDir, 'package.json'), JSON.stringify({ name: '@webpieces/pr-gate', version: installed }, null, 2) + '\n');
|
|
104
104
|
return root;
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Make `root` a real git repo whose CURRENT BRANCH is `branch`, and return it.
|
|
108
|
+
*
|
|
109
|
+
* The fault-D deny asks `git branch --show-current` to decide whether "move forward to what origin
|
|
110
|
+
* pins" is a legal command here (it is on main, and it destroys the fork point on a feature branch),
|
|
111
|
+
* so the branch is now an INPUT to the message and has to be stageable. `symbolic-ref` rather than a
|
|
112
|
+
* commit: `--show-current` answers on an UNBORN branch, so this needs no user identity, no index and
|
|
113
|
+
* no object write — which is also why a staged root with NO git dir at all keeps answering '' and
|
|
114
|
+
* lands on the conservative, non-main half.
|
|
115
|
+
*/
|
|
116
|
+
stageBranch(root, branch) {
|
|
117
|
+
(0, child_process_1.spawnSync)('git', ['init', '-q'], { cwd: root, encoding: 'utf8' });
|
|
118
|
+
(0, child_process_1.spawnSync)('git', ['symbolic-ref', 'HEAD', `refs/heads/${branch}`], { cwd: root, encoding: 'utf8' });
|
|
119
|
+
return root;
|
|
120
|
+
}
|
|
106
121
|
/**
|
|
107
122
|
* A repo root that DECLARES @webpieces/ai-hook-rules but has nothing installed — fault X, the
|
|
108
123
|
* ordinary fresh-clone / new-worktree case whose cure really is `pnpm install`.
|