@webpieces/hook-runtime 0.0.1
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 +37 -0
- package/src/adapters/agent-adapters.d.ts +13 -0
- package/src/adapters/agent-adapters.js +23 -0
- package/src/adapters/agent-adapters.js.map +1 -0
- package/src/adapters/agent-payload.d.ts +48 -0
- package/src/adapters/agent-payload.js +30 -0
- package/src/adapters/agent-payload.js.map +1 -0
- package/src/adapters/claude-code-adapter.d.ts +26 -0
- package/src/adapters/claude-code-adapter.js +69 -0
- package/src/adapters/claude-code-adapter.js.map +1 -0
- package/src/adapters/codex-adapter.d.ts +19 -0
- package/src/adapters/codex-adapter.js +48 -0
- package/src/adapters/codex-adapter.js.map +1 -0
- package/src/adapters/detect-ai.d.ts +70 -0
- package/src/adapters/detect-ai.js +82 -0
- package/src/adapters/detect-ai.js.map +1 -0
- package/src/core/apply-patch-parse.d.ts +36 -0
- package/src/core/apply-patch-parse.js +155 -0
- package/src/core/apply-patch-parse.js.map +1 -0
- package/src/core/build-context.d.ts +11 -0
- package/src/core/build-context.js +69 -0
- package/src/core/build-context.js.map +1 -0
- package/src/core/command-scan.d.ts +122 -0
- package/src/core/command-scan.js +282 -0
- package/src/core/command-scan.js.map +1 -0
- package/src/core/custom-rule-adapter.d.ts +23 -0
- package/src/core/custom-rule-adapter.js +50 -0
- package/src/core/custom-rule-adapter.js.map +1 -0
- package/src/core/delete-scoped-rules.d.ts +9 -0
- package/src/core/delete-scoped-rules.js +31 -0
- package/src/core/delete-scoped-rules.js.map +1 -0
- package/src/core/disable-directives.d.ts +8 -0
- package/src/core/disable-directives.js +86 -0
- package/src/core/disable-directives.js.map +1 -0
- package/src/core/effective-tree.d.ts +197 -0
- package/src/core/effective-tree.js +269 -0
- package/src/core/effective-tree.js.map +1 -0
- package/src/core/excluded-paths.d.ts +23 -0
- package/src/core/excluded-paths.js +54 -0
- package/src/core/excluded-paths.js.map +1 -0
- package/src/core/file-evaluation.d.ts +11 -0
- package/src/core/file-evaluation.js +45 -0
- package/src/core/file-evaluation.js.map +1 -0
- package/src/core/fix-hint.d.ts +40 -0
- package/src/core/fix-hint.js +53 -0
- package/src/core/fix-hint.js.map +1 -0
- package/src/core/glob.d.ts +1 -0
- package/src/core/glob.js +42 -0
- package/src/core/glob.js.map +1 -0
- package/src/core/report.d.ts +21 -0
- package/src/core/report.js +74 -0
- package/src/core/report.js.map +1 -0
- package/src/core/root-manifest.d.ts +1 -0
- package/src/core/root-manifest.js +21 -0
- package/src/core/root-manifest.js.map +1 -0
- package/src/core/rule-base.d.ts +40 -0
- package/src/core/rule-base.js +37 -0
- package/src/core/rule-base.js.map +1 -0
- package/src/core/rule-evaluation.d.ts +18 -0
- package/src/core/rule-evaluation.js +179 -0
- package/src/core/rule-evaluation.js.map +1 -0
- package/src/core/rules/shell-segment-scan.d.ts +64 -0
- package/src/core/rules/shell-segment-scan.js +88 -0
- package/src/core/rules/shell-segment-scan.js.map +1 -0
- package/src/core/shell-read-parity.d.ts +34 -0
- package/src/core/shell-read-parity.js +157 -0
- package/src/core/shell-read-parity.js.map +1 -0
- package/src/core/strip-ts-noise.d.ts +1 -0
- package/src/core/strip-ts-noise.js +191 -0
- package/src/core/strip-ts-noise.js.map +1 -0
- package/src/core/target-tree.d.ts +84 -0
- package/src/core/target-tree.js +148 -0
- package/src/core/target-tree.js.map +1 -0
- package/src/core/types.d.ts +149 -0
- package/src/core/types.js +190 -0
- package/src/core/types.js.map +1 -0
- package/src/harness-vocabulary.d.ts +2 -0
- package/src/harness-vocabulary.js +10 -0
- package/src/harness-vocabulary.js.map +1 -0
- package/src/hook-app.d.ts +15 -0
- package/src/hook-app.js +58 -0
- package/src/hook-app.js.map +1 -0
- package/src/hook-evaluator.d.ts +5 -0
- package/src/hook-evaluator.js +13 -0
- package/src/hook-evaluator.js.map +1 -0
- package/src/hook-ports.d.ts +9 -0
- package/src/hook-ports.js +40 -0
- package/src/hook-ports.js.map +1 -0
- package/src/index.d.ts +31 -0
- package/src/index.js +59 -0
- package/src/index.js.map +1 -0
- package/src/outcome.d.ts +14 -0
- package/src/outcome.js +20 -0
- package/src/outcome.js.map +1 -0
- package/src/protocol.d.ts +40 -0
- package/src/protocol.js +55 -0
- package/src/protocol.js.map +1 -0
- package/src/response.d.ts +2 -0
- package/src/response.js +21 -0
- package/src/response.js.map +1 -0
- package/src/to-error.d.ts +1 -0
- package/src/to-error.js +9 -0
- package/src/to-error.js.map +1 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stripTsNoise = stripTsNoise;
|
|
4
|
+
class StripState {
|
|
5
|
+
source;
|
|
6
|
+
len;
|
|
7
|
+
out;
|
|
8
|
+
stack;
|
|
9
|
+
state;
|
|
10
|
+
braceDepth;
|
|
11
|
+
i;
|
|
12
|
+
constructor(source) {
|
|
13
|
+
this.source = source;
|
|
14
|
+
this.len = source.length;
|
|
15
|
+
this.out = new Array(source.length);
|
|
16
|
+
this.stack = [];
|
|
17
|
+
this.state = 'code';
|
|
18
|
+
this.braceDepth = 0;
|
|
19
|
+
this.i = 0;
|
|
20
|
+
}
|
|
21
|
+
ch() { return this.source[this.i]; }
|
|
22
|
+
next() { return this.i + 1 < this.len ? this.source[this.i + 1] : ''; }
|
|
23
|
+
emit(idx, ch) { this.out[idx] = ch; }
|
|
24
|
+
blank(idx, ch) { this.out[idx] = ch === '\n' ? '\n' : ' '; }
|
|
25
|
+
pushState(newState) {
|
|
26
|
+
this.stack.push({ state: this.state, braceDepth: this.braceDepth });
|
|
27
|
+
this.state = newState;
|
|
28
|
+
}
|
|
29
|
+
popState() {
|
|
30
|
+
const frame = this.stack.pop();
|
|
31
|
+
this.state = frame.state;
|
|
32
|
+
this.braceDepth = frame.braceDepth;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
36
|
+
function handleCodeOrInterp(s) {
|
|
37
|
+
const ch = s.ch();
|
|
38
|
+
const next = s.next();
|
|
39
|
+
if (s.state === 'templateInterp') {
|
|
40
|
+
if (ch === '{') {
|
|
41
|
+
s.braceDepth += 1;
|
|
42
|
+
s.emit(s.i, ch);
|
|
43
|
+
s.i += 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '}') {
|
|
47
|
+
if (s.braceDepth === 0) {
|
|
48
|
+
s.emit(s.i, ch);
|
|
49
|
+
s.i += 1;
|
|
50
|
+
s.popState();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
s.braceDepth -= 1;
|
|
54
|
+
s.emit(s.i, ch);
|
|
55
|
+
s.i += 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (ch === '/' && next === '/') {
|
|
60
|
+
s.emit(s.i, '/');
|
|
61
|
+
s.emit(s.i + 1, '/');
|
|
62
|
+
s.i += 2;
|
|
63
|
+
s.pushState('lineComment');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (ch === '/' && next === '*') {
|
|
67
|
+
s.emit(s.i, '/');
|
|
68
|
+
s.emit(s.i + 1, '*');
|
|
69
|
+
s.i += 2;
|
|
70
|
+
s.pushState('blockComment');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (ch === '"') {
|
|
74
|
+
s.emit(s.i, '"');
|
|
75
|
+
s.i += 1;
|
|
76
|
+
s.pushState('dquote');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (ch === "'") {
|
|
80
|
+
s.emit(s.i, "'");
|
|
81
|
+
s.i += 1;
|
|
82
|
+
s.pushState('squote');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (ch === '`') {
|
|
86
|
+
s.emit(s.i, '`');
|
|
87
|
+
s.i += 1;
|
|
88
|
+
s.pushState('template');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
s.emit(s.i, ch);
|
|
92
|
+
s.i += 1;
|
|
93
|
+
}
|
|
94
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
95
|
+
function handleLineComment(s) {
|
|
96
|
+
if (s.ch() === '\n') {
|
|
97
|
+
s.emit(s.i, '\n');
|
|
98
|
+
s.i += 1;
|
|
99
|
+
s.popState();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
s.blank(s.i, s.ch());
|
|
103
|
+
s.i += 1;
|
|
104
|
+
}
|
|
105
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
106
|
+
function handleBlockComment(s) {
|
|
107
|
+
if (s.ch() === '*' && s.next() === '/') {
|
|
108
|
+
s.emit(s.i, '*');
|
|
109
|
+
s.emit(s.i + 1, '/');
|
|
110
|
+
s.i += 2;
|
|
111
|
+
s.popState();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
s.blank(s.i, s.ch());
|
|
115
|
+
s.i += 1;
|
|
116
|
+
}
|
|
117
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
118
|
+
function handleStringLiteral(s, quoteChar) {
|
|
119
|
+
const ch = s.ch();
|
|
120
|
+
if (ch === '\\' && s.i + 1 < s.len) {
|
|
121
|
+
s.blank(s.i, ch);
|
|
122
|
+
s.blank(s.i + 1, s.source[s.i + 1]);
|
|
123
|
+
s.i += 2;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (ch === quoteChar) {
|
|
127
|
+
s.emit(s.i, quoteChar);
|
|
128
|
+
s.i += 1;
|
|
129
|
+
s.popState();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
s.blank(s.i, ch);
|
|
133
|
+
s.i += 1;
|
|
134
|
+
}
|
|
135
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
136
|
+
function handleTemplate(s) {
|
|
137
|
+
const ch = s.ch();
|
|
138
|
+
if (ch === '\\' && s.i + 1 < s.len) {
|
|
139
|
+
s.blank(s.i, ch);
|
|
140
|
+
s.blank(s.i + 1, s.source[s.i + 1]);
|
|
141
|
+
s.i += 2;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (ch === '`') {
|
|
145
|
+
s.emit(s.i, '`');
|
|
146
|
+
s.i += 1;
|
|
147
|
+
s.popState();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (ch === '$' && s.next() === '{') {
|
|
151
|
+
s.emit(s.i, '$');
|
|
152
|
+
s.emit(s.i + 1, '{');
|
|
153
|
+
s.i += 2;
|
|
154
|
+
s.stack.push({ state: 'template', braceDepth: 0 });
|
|
155
|
+
s.state = 'templateInterp';
|
|
156
|
+
s.braceDepth = 0;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
s.blank(s.i, ch);
|
|
160
|
+
s.i += 1;
|
|
161
|
+
}
|
|
162
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
163
|
+
function stripTsNoise(source) {
|
|
164
|
+
const s = new StripState(source);
|
|
165
|
+
while (s.i < s.len) {
|
|
166
|
+
if (s.state === 'code' || s.state === 'templateInterp') {
|
|
167
|
+
handleCodeOrInterp(s);
|
|
168
|
+
}
|
|
169
|
+
else if (s.state === 'lineComment') {
|
|
170
|
+
handleLineComment(s);
|
|
171
|
+
}
|
|
172
|
+
else if (s.state === 'blockComment') {
|
|
173
|
+
handleBlockComment(s);
|
|
174
|
+
}
|
|
175
|
+
else if (s.state === 'dquote') {
|
|
176
|
+
handleStringLiteral(s, '"');
|
|
177
|
+
}
|
|
178
|
+
else if (s.state === 'squote') {
|
|
179
|
+
handleStringLiteral(s, "'");
|
|
180
|
+
}
|
|
181
|
+
else if (s.state === 'template') {
|
|
182
|
+
handleTemplate(s);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
s.emit(s.i, s.ch());
|
|
186
|
+
s.i += 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return s.out.join('');
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=strip-ts-noise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strip-ts-noise.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/core/strip-ts-noise.ts"],"names":[],"mappings":"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","sourcesContent":["interface StackFrame {\n readonly state: string;\n readonly braceDepth: number;\n}\n\nclass StripState {\n readonly source: string;\n readonly len: number;\n readonly out: string[];\n readonly stack: StackFrame[];\n state: string;\n braceDepth: number;\n i: number;\n\n constructor(source: string) {\n this.source = source;\n this.len = source.length;\n this.out = new Array<string>(source.length);\n this.stack = [];\n this.state = 'code';\n this.braceDepth = 0;\n this.i = 0;\n }\n\n ch(): string { return this.source[this.i]; }\n next(): string { return this.i + 1 < this.len ? this.source[this.i + 1] : ''; }\n emit(idx: number, ch: string): void { this.out[idx] = ch; }\n blank(idx: number, ch: string): void { this.out[idx] = ch === '\\n' ? '\\n' : ' '; }\n\n pushState(newState: string): void {\n this.stack.push({ state: this.state, braceDepth: this.braceDepth });\n this.state = newState;\n }\n\n popState(): void {\n const frame = this.stack.pop()!;\n this.state = frame.state;\n this.braceDepth = frame.braceDepth;\n }\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nfunction handleCodeOrInterp(s: StripState): void {\n const ch = s.ch();\n const next = s.next();\n\n if (s.state === 'templateInterp') {\n if (ch === '{') { s.braceDepth += 1; s.emit(s.i, ch); s.i += 1; return; }\n if (ch === '}') {\n if (s.braceDepth === 0) { s.emit(s.i, ch); s.i += 1; s.popState(); return; }\n s.braceDepth -= 1; s.emit(s.i, ch); s.i += 1; return;\n }\n }\n if (ch === '/' && next === '/') { s.emit(s.i, '/'); s.emit(s.i + 1, '/'); s.i += 2; s.pushState('lineComment'); return; }\n if (ch === '/' && next === '*') { s.emit(s.i, '/'); s.emit(s.i + 1, '*'); s.i += 2; s.pushState('blockComment'); return; }\n if (ch === '\"') { s.emit(s.i, '\"'); s.i += 1; s.pushState('dquote'); return; }\n if (ch === \"'\") { s.emit(s.i, \"'\"); s.i += 1; s.pushState('squote'); return; }\n if (ch === '`') { s.emit(s.i, '`'); s.i += 1; s.pushState('template'); return; }\n s.emit(s.i, ch); s.i += 1;\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nfunction handleLineComment(s: StripState): void {\n if (s.ch() === '\\n') { s.emit(s.i, '\\n'); s.i += 1; s.popState(); return; }\n s.blank(s.i, s.ch()); s.i += 1;\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nfunction handleBlockComment(s: StripState): void {\n if (s.ch() === '*' && s.next() === '/') { s.emit(s.i, '*'); s.emit(s.i + 1, '/'); s.i += 2; s.popState(); return; }\n s.blank(s.i, s.ch()); s.i += 1;\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nfunction handleStringLiteral(s: StripState, quoteChar: string): void {\n const ch = s.ch();\n if (ch === '\\\\' && s.i + 1 < s.len) { s.blank(s.i, ch); s.blank(s.i + 1, s.source[s.i + 1]); s.i += 2; return; }\n if (ch === quoteChar) { s.emit(s.i, quoteChar); s.i += 1; s.popState(); return; }\n s.blank(s.i, ch); s.i += 1;\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nfunction handleTemplate(s: StripState): void {\n const ch = s.ch();\n if (ch === '\\\\' && s.i + 1 < s.len) { s.blank(s.i, ch); s.blank(s.i + 1, s.source[s.i + 1]); s.i += 2; return; }\n if (ch === '`') { s.emit(s.i, '`'); s.i += 1; s.popState(); return; }\n if (ch === '$' && s.next() === '{') {\n s.emit(s.i, '$'); s.emit(s.i + 1, '{'); s.i += 2;\n s.stack.push({ state: 'template', braceDepth: 0 });\n s.state = 'templateInterp'; s.braceDepth = 0;\n return;\n }\n s.blank(s.i, ch); s.i += 1;\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nexport function stripTsNoise(source: string): string {\n const s = new StripState(source);\n while (s.i < s.len) {\n if (s.state === 'code' || s.state === 'templateInterp') { handleCodeOrInterp(s); }\n else if (s.state === 'lineComment') { handleLineComment(s); }\n else if (s.state === 'blockComment') { handleBlockComment(s); }\n else if (s.state === 'dquote') { handleStringLiteral(s, '\"'); }\n else if (s.state === 'squote') { handleStringLiteral(s, \"'\"); }\n else if (s.state === 'template') { handleTemplate(s); }\n else { s.emit(s.i, s.ch()); s.i += 1; }\n }\n return s.out.join('');\n}\n"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { EffectiveTree, EffectiveTreeResolver } from './effective-tree';
|
|
2
|
+
/**
|
|
3
|
+
* WHICH TREE owns the FILE a Write/Edit/Read is aimed at? The file-path counterpart to
|
|
4
|
+
* EffectiveTreeResolver, which answers the same question for a Bash command's effective cwd.
|
|
5
|
+
*
|
|
6
|
+
* WHY IT HAS TO EXIST (issue #851). The bash half of this question has been answered by git since
|
|
7
|
+
* effective-tree.ts was written — `isLinkedWorktree` is `--git-dir !== --git-common-dir`, and its header
|
|
8
|
+
* states the principle in capitals: *PLACEMENT IS NOT IDENTITY*. The FILE half never consumed that
|
|
9
|
+
* answer. Every file-scoped guard judged `ctx.workspaceRoot`, which is the walk-up from the SESSION's
|
|
10
|
+
* cwd to whichever `webpieces.config.json` governs it — the PRIMARY clone, for a main-session edit into
|
|
11
|
+
* an agent worktree. Because Claude Code checks a worktree out INSIDE the repo at
|
|
12
|
+
* `<repo>/.claude/worktrees/agent-XXXX`, that walk-up answers "primary" for a path that is manifestly
|
|
13
|
+
* not the primary's, and nothing downstream re-examined it.
|
|
14
|
+
*
|
|
15
|
+
* Measured live on 2026-09-03: an Edit of one import line inside a worktree on a CLEAN branch was
|
|
16
|
+
* refused, citing `pnpm-lock.yaml` and `pnpm-workspace.yaml` — the PRIMARY clone's branch's conflict
|
|
17
|
+
* files. The main-sync cache is already keyed by branch and already held the correct, non-conflicting
|
|
18
|
+
* entry for the worktree's branch, one key over. Only the lookup key was wrong. So this class exists to
|
|
19
|
+
* make the lookup ask the same authority the bash half asks, rather than to introduce a second one.
|
|
20
|
+
*
|
|
21
|
+
* IT ASKS EffectiveTreeResolver, and adds NOTHING of its own beyond finding a directory to ask about.
|
|
22
|
+
* That is deliberate and it is the whole design: two resolvers WILL disagree about which tree a path is
|
|
23
|
+
* in, and this module exists precisely because two layers already did. There is no `.claude/worktrees`
|
|
24
|
+
* string match here and there must never be one — matching on placement is the bug, not the fix.
|
|
25
|
+
*
|
|
26
|
+
* The only thing worth naming is the walk-up: a Write legitimately names a file, and directories, that
|
|
27
|
+
* do not exist yet, and git cannot be asked about a directory that is not there. So we climb to the
|
|
28
|
+
* nearest EXISTING ancestor and ask about that. A parent directory is in the same worktree as the child
|
|
29
|
+
* it is about to contain — that is a property of the filesystem, not a guess.
|
|
30
|
+
*/
|
|
31
|
+
export declare class TargetTreeResolver {
|
|
32
|
+
private readonly trees;
|
|
33
|
+
constructor(trees?: EffectiveTreeResolver);
|
|
34
|
+
/**
|
|
35
|
+
* The tree that owns `targetPath`, classified exactly as a Bash command's cwd would be.
|
|
36
|
+
*
|
|
37
|
+
* `governedRoot` is the fallback for every case with no better answer — an empty path, a path with
|
|
38
|
+
* no existing ancestor at all — and is also what `EffectiveTree.governedRoot` keeps meaning: whose
|
|
39
|
+
* config and excludePaths apply. It is NOT the tree to judge; `EffectiveTree.root` is.
|
|
40
|
+
*/
|
|
41
|
+
resolve(targetPath: string, governedRoot: string): EffectiveTree;
|
|
42
|
+
/**
|
|
43
|
+
* The two relative spellings of one target path, resolved together so a caller cannot accidentally
|
|
44
|
+
* use the wrong one. See GovernedPath for why there are two.
|
|
45
|
+
*/
|
|
46
|
+
governedPath(targetPath: string, governedRoot: string): GovernedPath;
|
|
47
|
+
private nearestExistingDir;
|
|
48
|
+
/**
|
|
49
|
+
* `targetPath` made absolute AND symlink-resolved, including when it does not exist yet.
|
|
50
|
+
*
|
|
51
|
+
* `fs.realpathSync` throws on a missing path, and a Write's target is missing by definition, so the
|
|
52
|
+
* resolvable HEAD of the path (its nearest existing ancestor) is resolved and the not-yet-existing
|
|
53
|
+
* TAIL is re-appended verbatim. That keeps it comparable with `tree.root`, which is git's answer and
|
|
54
|
+
* always real.
|
|
55
|
+
*/
|
|
56
|
+
private realAbsolute;
|
|
57
|
+
private realDir;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* One target path, in the TWO relative spellings the hook path needs — data-only, per CLAUDE.md.
|
|
61
|
+
*
|
|
62
|
+
* They are different questions and conflating them is issue #851's secondary defect:
|
|
63
|
+
*
|
|
64
|
+
* `relativePath` — relative to the GOVERNED root. What `excludePaths`' globs are written against,
|
|
65
|
+
* and what the violation report prints.
|
|
66
|
+
* `treeRelativePath` — relative to the tree that OWNS the file. What `isWebpiecesStateDir` must be
|
|
67
|
+
* asked about, because `.webpieces/` is the tooling's state dir *of a tree*, and
|
|
68
|
+
* a worktree has its own. Governed-root-relative, a reviewer subagent's
|
|
69
|
+
* `<primary>/.claude/worktrees/agent-X/.webpieces/pr-review/<branch>/summary.json`
|
|
70
|
+
* reads as `.claude/...` and was NOT exempt — while
|
|
71
|
+
* `<primary>/.webpieces/worktrees/agent-X/pr-review/.../review-1.json`, the same
|
|
72
|
+
* kind of file one directory over, was. `wp-review-upsert-pr` REQUIRES that file
|
|
73
|
+
* before `wp-finish-upsert-pr` will open a PR, so the guard could forbid a file
|
|
74
|
+
* the gate demands.
|
|
75
|
+
*
|
|
76
|
+
* In the primary clone the two strings are identical, which is why the gap was invisible for so long.
|
|
77
|
+
*/
|
|
78
|
+
export declare class GovernedPath {
|
|
79
|
+
readonly relativePath: string;
|
|
80
|
+
readonly treeRelativePath: string;
|
|
81
|
+
/** The git tree that owns the target path; policy still belongs to the separately loaded governed root. */
|
|
82
|
+
readonly treeRoot: string;
|
|
83
|
+
constructor(relativePath: string, treeRelativePath: string, treeRoot: string);
|
|
84
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GovernedPath = exports.TargetTreeResolver = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const effective_tree_1 = require("./effective-tree");
|
|
8
|
+
/**
|
|
9
|
+
* WHICH TREE owns the FILE a Write/Edit/Read is aimed at? The file-path counterpart to
|
|
10
|
+
* EffectiveTreeResolver, which answers the same question for a Bash command's effective cwd.
|
|
11
|
+
*
|
|
12
|
+
* WHY IT HAS TO EXIST (issue #851). The bash half of this question has been answered by git since
|
|
13
|
+
* effective-tree.ts was written — `isLinkedWorktree` is `--git-dir !== --git-common-dir`, and its header
|
|
14
|
+
* states the principle in capitals: *PLACEMENT IS NOT IDENTITY*. The FILE half never consumed that
|
|
15
|
+
* answer. Every file-scoped guard judged `ctx.workspaceRoot`, which is the walk-up from the SESSION's
|
|
16
|
+
* cwd to whichever `webpieces.config.json` governs it — the PRIMARY clone, for a main-session edit into
|
|
17
|
+
* an agent worktree. Because Claude Code checks a worktree out INSIDE the repo at
|
|
18
|
+
* `<repo>/.claude/worktrees/agent-XXXX`, that walk-up answers "primary" for a path that is manifestly
|
|
19
|
+
* not the primary's, and nothing downstream re-examined it.
|
|
20
|
+
*
|
|
21
|
+
* Measured live on 2026-09-03: an Edit of one import line inside a worktree on a CLEAN branch was
|
|
22
|
+
* refused, citing `pnpm-lock.yaml` and `pnpm-workspace.yaml` — the PRIMARY clone's branch's conflict
|
|
23
|
+
* files. The main-sync cache is already keyed by branch and already held the correct, non-conflicting
|
|
24
|
+
* entry for the worktree's branch, one key over. Only the lookup key was wrong. So this class exists to
|
|
25
|
+
* make the lookup ask the same authority the bash half asks, rather than to introduce a second one.
|
|
26
|
+
*
|
|
27
|
+
* IT ASKS EffectiveTreeResolver, and adds NOTHING of its own beyond finding a directory to ask about.
|
|
28
|
+
* That is deliberate and it is the whole design: two resolvers WILL disagree about which tree a path is
|
|
29
|
+
* in, and this module exists precisely because two layers already did. There is no `.claude/worktrees`
|
|
30
|
+
* string match here and there must never be one — matching on placement is the bug, not the fix.
|
|
31
|
+
*
|
|
32
|
+
* The only thing worth naming is the walk-up: a Write legitimately names a file, and directories, that
|
|
33
|
+
* do not exist yet, and git cannot be asked about a directory that is not there. So we climb to the
|
|
34
|
+
* nearest EXISTING ancestor and ask about that. A parent directory is in the same worktree as the child
|
|
35
|
+
* it is about to contain — that is a property of the filesystem, not a guess.
|
|
36
|
+
*/
|
|
37
|
+
class TargetTreeResolver {
|
|
38
|
+
trees;
|
|
39
|
+
constructor(trees = new effective_tree_1.EffectiveTreeResolver()) {
|
|
40
|
+
this.trees = trees;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The tree that owns `targetPath`, classified exactly as a Bash command's cwd would be.
|
|
44
|
+
*
|
|
45
|
+
* `governedRoot` is the fallback for every case with no better answer — an empty path, a path with
|
|
46
|
+
* no existing ancestor at all — and is also what `EffectiveTree.governedRoot` keeps meaning: whose
|
|
47
|
+
* config and excludePaths apply. It is NOT the tree to judge; `EffectiveTree.root` is.
|
|
48
|
+
*/
|
|
49
|
+
resolve(targetPath, governedRoot) {
|
|
50
|
+
// REAL paths on BOTH sides, or git's answers cannot be compared with each other. Identity here
|
|
51
|
+
// is `commonDir(target) === commonDir(governed)`, and git prints an ABSOLUTE, symlink-resolved
|
|
52
|
+
// path from a linked worktree while printing a bare relative `.git` from the primary clone. So
|
|
53
|
+
// on any repo reached through a symlink — `/var/folders/...` on macOS, which is `/private/var`,
|
|
54
|
+
// and every `os.tmpdir()` path under it — the two strings differ for the same `.git` and the
|
|
55
|
+
// repo's OWN worktree classifies as `foreign`, i.e. every guard silently off. Resolving both
|
|
56
|
+
// sides first is what makes the comparison an answer about git rather than about spelling.
|
|
57
|
+
const governed = this.realDir(governedRoot);
|
|
58
|
+
return this.trees.resolve('', this.nearestExistingDir(targetPath, governed), governed);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The two relative spellings of one target path, resolved together so a caller cannot accidentally
|
|
62
|
+
* use the wrong one. See GovernedPath for why there are two.
|
|
63
|
+
*/
|
|
64
|
+
governedPath(targetPath, governedRoot) {
|
|
65
|
+
const absolute = this.realAbsolute(targetPath);
|
|
66
|
+
const tree = this.resolve(targetPath, governedRoot);
|
|
67
|
+
// `tree.root` and `tree.governedRoot` are already resolved by resolve() above, so both
|
|
68
|
+
// subtractions are real-against-real. Mixing one resolved side with one unresolved one produces
|
|
69
|
+
// a `../../../..` climb out of the filesystem — which reads as "outside the workspace" and
|
|
70
|
+
// would quietly hand every path the wrong verdict on a symlinked checkout.
|
|
71
|
+
return new GovernedPath(path.relative(tree.governedRoot, absolute), path.relative(tree.root, absolute), tree.root);
|
|
72
|
+
}
|
|
73
|
+
// The nearest ancestor directory of `targetPath` that EXISTS — the deepest directory git can be
|
|
74
|
+
// asked about — with symlinks resolved. Bounded by the filesystem root (path.dirname is its own
|
|
75
|
+
// fixed point at `/`), so the loop terminates without a counter.
|
|
76
|
+
nearestExistingDir(targetPath, governedRoot) {
|
|
77
|
+
if (targetPath === '')
|
|
78
|
+
return governedRoot;
|
|
79
|
+
let dir = path.dirname(path.resolve(targetPath));
|
|
80
|
+
for (;;) {
|
|
81
|
+
if (fs.existsSync(dir))
|
|
82
|
+
return fs.realpathSync(dir);
|
|
83
|
+
const parent = path.dirname(dir);
|
|
84
|
+
if (parent === dir)
|
|
85
|
+
return governedRoot;
|
|
86
|
+
dir = parent;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `targetPath` made absolute AND symlink-resolved, including when it does not exist yet.
|
|
91
|
+
*
|
|
92
|
+
* `fs.realpathSync` throws on a missing path, and a Write's target is missing by definition, so the
|
|
93
|
+
* resolvable HEAD of the path (its nearest existing ancestor) is resolved and the not-yet-existing
|
|
94
|
+
* TAIL is re-appended verbatim. That keeps it comparable with `tree.root`, which is git's answer and
|
|
95
|
+
* always real.
|
|
96
|
+
*/
|
|
97
|
+
realAbsolute(targetPath) {
|
|
98
|
+
const absolute = path.resolve(targetPath);
|
|
99
|
+
const tail = [];
|
|
100
|
+
let dir = absolute;
|
|
101
|
+
for (;;) {
|
|
102
|
+
if (fs.existsSync(dir))
|
|
103
|
+
return path.join(fs.realpathSync(dir), ...tail);
|
|
104
|
+
const parent = path.dirname(dir);
|
|
105
|
+
if (parent === dir)
|
|
106
|
+
return absolute;
|
|
107
|
+
tail.unshift(path.basename(dir));
|
|
108
|
+
dir = parent;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// A directory made real when it is there, and merely absolute when it is not.
|
|
112
|
+
realDir(dir) {
|
|
113
|
+
return fs.existsSync(dir) ? fs.realpathSync(dir) : path.resolve(dir);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.TargetTreeResolver = TargetTreeResolver;
|
|
117
|
+
/**
|
|
118
|
+
* One target path, in the TWO relative spellings the hook path needs — data-only, per CLAUDE.md.
|
|
119
|
+
*
|
|
120
|
+
* They are different questions and conflating them is issue #851's secondary defect:
|
|
121
|
+
*
|
|
122
|
+
* `relativePath` — relative to the GOVERNED root. What `excludePaths`' globs are written against,
|
|
123
|
+
* and what the violation report prints.
|
|
124
|
+
* `treeRelativePath` — relative to the tree that OWNS the file. What `isWebpiecesStateDir` must be
|
|
125
|
+
* asked about, because `.webpieces/` is the tooling's state dir *of a tree*, and
|
|
126
|
+
* a worktree has its own. Governed-root-relative, a reviewer subagent's
|
|
127
|
+
* `<primary>/.claude/worktrees/agent-X/.webpieces/pr-review/<branch>/summary.json`
|
|
128
|
+
* reads as `.claude/...` and was NOT exempt — while
|
|
129
|
+
* `<primary>/.webpieces/worktrees/agent-X/pr-review/.../review-1.json`, the same
|
|
130
|
+
* kind of file one directory over, was. `wp-review-upsert-pr` REQUIRES that file
|
|
131
|
+
* before `wp-finish-upsert-pr` will open a PR, so the guard could forbid a file
|
|
132
|
+
* the gate demands.
|
|
133
|
+
*
|
|
134
|
+
* In the primary clone the two strings are identical, which is why the gap was invisible for so long.
|
|
135
|
+
*/
|
|
136
|
+
class GovernedPath {
|
|
137
|
+
relativePath;
|
|
138
|
+
treeRelativePath;
|
|
139
|
+
/** The git tree that owns the target path; policy still belongs to the separately loaded governed root. */
|
|
140
|
+
treeRoot;
|
|
141
|
+
constructor(relativePath, treeRelativePath, treeRoot) {
|
|
142
|
+
this.relativePath = relativePath;
|
|
143
|
+
this.treeRelativePath = treeRelativePath;
|
|
144
|
+
this.treeRoot = treeRoot;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.GovernedPath = GovernedPath;
|
|
148
|
+
//# sourceMappingURL=target-tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target-tree.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/core/target-tree.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { EffectiveTree, EffectiveTreeResolver } from './effective-tree';\n\n/**\n * WHICH TREE owns the FILE a Write/Edit/Read is aimed at? The file-path counterpart to\n * EffectiveTreeResolver, which answers the same question for a Bash command's effective cwd.\n *\n * WHY IT HAS TO EXIST (issue #851). The bash half of this question has been answered by git since\n * effective-tree.ts was written — `isLinkedWorktree` is `--git-dir !== --git-common-dir`, and its header\n * states the principle in capitals: *PLACEMENT IS NOT IDENTITY*. The FILE half never consumed that\n * answer. Every file-scoped guard judged `ctx.workspaceRoot`, which is the walk-up from the SESSION's\n * cwd to whichever `webpieces.config.json` governs it — the PRIMARY clone, for a main-session edit into\n * an agent worktree. Because Claude Code checks a worktree out INSIDE the repo at\n * `<repo>/.claude/worktrees/agent-XXXX`, that walk-up answers \"primary\" for a path that is manifestly\n * not the primary's, and nothing downstream re-examined it.\n *\n * Measured live on 2026-09-03: an Edit of one import line inside a worktree on a CLEAN branch was\n * refused, citing `pnpm-lock.yaml` and `pnpm-workspace.yaml` — the PRIMARY clone's branch's conflict\n * files. The main-sync cache is already keyed by branch and already held the correct, non-conflicting\n * entry for the worktree's branch, one key over. Only the lookup key was wrong. So this class exists to\n * make the lookup ask the same authority the bash half asks, rather than to introduce a second one.\n *\n * IT ASKS EffectiveTreeResolver, and adds NOTHING of its own beyond finding a directory to ask about.\n * That is deliberate and it is the whole design: two resolvers WILL disagree about which tree a path is\n * in, and this module exists precisely because two layers already did. There is no `.claude/worktrees`\n * string match here and there must never be one — matching on placement is the bug, not the fix.\n *\n * The only thing worth naming is the walk-up: a Write legitimately names a file, and directories, that\n * do not exist yet, and git cannot be asked about a directory that is not there. So we climb to the\n * nearest EXISTING ancestor and ask about that. A parent directory is in the same worktree as the child\n * it is about to contain — that is a property of the filesystem, not a guess.\n */\nexport class TargetTreeResolver {\n constructor(private readonly trees: EffectiveTreeResolver = new EffectiveTreeResolver()) {}\n\n /**\n * The tree that owns `targetPath`, classified exactly as a Bash command's cwd would be.\n *\n * `governedRoot` is the fallback for every case with no better answer — an empty path, a path with\n * no existing ancestor at all — and is also what `EffectiveTree.governedRoot` keeps meaning: whose\n * config and excludePaths apply. It is NOT the tree to judge; `EffectiveTree.root` is.\n */\n resolve(targetPath: string, governedRoot: string): EffectiveTree {\n // REAL paths on BOTH sides, or git's answers cannot be compared with each other. Identity here\n // is `commonDir(target) === commonDir(governed)`, and git prints an ABSOLUTE, symlink-resolved\n // path from a linked worktree while printing a bare relative `.git` from the primary clone. So\n // on any repo reached through a symlink — `/var/folders/...` on macOS, which is `/private/var`,\n // and every `os.tmpdir()` path under it — the two strings differ for the same `.git` and the\n // repo's OWN worktree classifies as `foreign`, i.e. every guard silently off. Resolving both\n // sides first is what makes the comparison an answer about git rather than about spelling.\n const governed = this.realDir(governedRoot);\n return this.trees.resolve('', this.nearestExistingDir(targetPath, governed), governed);\n }\n\n /**\n * The two relative spellings of one target path, resolved together so a caller cannot accidentally\n * use the wrong one. See GovernedPath for why there are two.\n */\n governedPath(targetPath: string, governedRoot: string): GovernedPath {\n const absolute = this.realAbsolute(targetPath);\n const tree = this.resolve(targetPath, governedRoot);\n // `tree.root` and `tree.governedRoot` are already resolved by resolve() above, so both\n // subtractions are real-against-real. Mixing one resolved side with one unresolved one produces\n // a `../../../..` climb out of the filesystem — which reads as \"outside the workspace\" and\n // would quietly hand every path the wrong verdict on a symlinked checkout.\n return new GovernedPath(\n path.relative(tree.governedRoot, absolute),\n path.relative(tree.root, absolute),\n tree.root,\n );\n }\n\n // The nearest ancestor directory of `targetPath` that EXISTS — the deepest directory git can be\n // asked about — with symlinks resolved. Bounded by the filesystem root (path.dirname is its own\n // fixed point at `/`), so the loop terminates without a counter.\n private nearestExistingDir(targetPath: string, governedRoot: string): string {\n if (targetPath === '') return governedRoot;\n let dir = path.dirname(path.resolve(targetPath));\n for (;;) {\n if (fs.existsSync(dir)) return fs.realpathSync(dir);\n const parent = path.dirname(dir);\n if (parent === dir) return governedRoot;\n dir = parent;\n }\n }\n\n /**\n * `targetPath` made absolute AND symlink-resolved, including when it does not exist yet.\n *\n * `fs.realpathSync` throws on a missing path, and a Write's target is missing by definition, so the\n * resolvable HEAD of the path (its nearest existing ancestor) is resolved and the not-yet-existing\n * TAIL is re-appended verbatim. That keeps it comparable with `tree.root`, which is git's answer and\n * always real.\n */\n private realAbsolute(targetPath: string): string {\n const absolute = path.resolve(targetPath);\n const tail: string[] = [];\n let dir = absolute;\n for (;;) {\n if (fs.existsSync(dir)) return path.join(fs.realpathSync(dir), ...tail);\n const parent = path.dirname(dir);\n if (parent === dir) return absolute;\n tail.unshift(path.basename(dir));\n dir = parent;\n }\n }\n\n // A directory made real when it is there, and merely absolute when it is not.\n private realDir(dir: string): string {\n return fs.existsSync(dir) ? fs.realpathSync(dir) : path.resolve(dir);\n }\n}\n\n/**\n * One target path, in the TWO relative spellings the hook path needs — data-only, per CLAUDE.md.\n *\n * They are different questions and conflating them is issue #851's secondary defect:\n *\n * `relativePath` — relative to the GOVERNED root. What `excludePaths`' globs are written against,\n * and what the violation report prints.\n * `treeRelativePath` — relative to the tree that OWNS the file. What `isWebpiecesStateDir` must be\n * asked about, because `.webpieces/` is the tooling's state dir *of a tree*, and\n * a worktree has its own. Governed-root-relative, a reviewer subagent's\n * `<primary>/.claude/worktrees/agent-X/.webpieces/pr-review/<branch>/summary.json`\n * reads as `.claude/...` and was NOT exempt — while\n * `<primary>/.webpieces/worktrees/agent-X/pr-review/.../review-1.json`, the same\n * kind of file one directory over, was. `wp-review-upsert-pr` REQUIRES that file\n * before `wp-finish-upsert-pr` will open a PR, so the guard could forbid a file\n * the gate demands.\n *\n * In the primary clone the two strings are identical, which is why the gap was invisible for so long.\n */\nexport class GovernedPath {\n readonly relativePath: string;\n readonly treeRelativePath: string;\n /** The git tree that owns the target path; policy still belongs to the separately loaded governed root. */\n readonly treeRoot: string;\n\n constructor(relativePath: string, treeRelativePath: string, treeRoot: string) {\n this.relativePath = relativePath;\n this.treeRelativePath = treeRelativePath;\n this.treeRoot = treeRoot;\n }\n}\n"]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { RuleOptions } from '@webpieces/rules-config';
|
|
2
|
+
import { ToolKind } from '../protocol';
|
|
3
|
+
import { FixHint } from './fix-hint';
|
|
4
|
+
export type RuleScope = 'edit' | 'file' | 'bash';
|
|
5
|
+
export type IsLineDisabled = (lineNum: number, ruleName: string) => boolean;
|
|
6
|
+
export declare class Violation {
|
|
7
|
+
readonly line: number;
|
|
8
|
+
readonly snippet: string;
|
|
9
|
+
readonly message: string | undefined;
|
|
10
|
+
editIndex: number | undefined;
|
|
11
|
+
editCount: number | undefined;
|
|
12
|
+
constructor(line: number, snippet: string, message?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class EditContext {
|
|
15
|
+
readonly tool: ToolKind;
|
|
16
|
+
readonly editIndex: number;
|
|
17
|
+
readonly editCount: number;
|
|
18
|
+
readonly filePath: string;
|
|
19
|
+
readonly relativePath: string;
|
|
20
|
+
readonly workspaceRoot: string;
|
|
21
|
+
readonly addedContent: string;
|
|
22
|
+
readonly strippedContent: string;
|
|
23
|
+
readonly lines: readonly string[];
|
|
24
|
+
readonly strippedLines: readonly string[];
|
|
25
|
+
readonly removedContent: string;
|
|
26
|
+
readonly isLineDisabled: IsLineDisabled;
|
|
27
|
+
options: RuleOptions;
|
|
28
|
+
constructor(tool: ToolKind, editIndex: number, editCount: number, filePath: string, relativePath: string, workspaceRoot: string, addedContent: string, strippedContent: string, lines: readonly string[], strippedLines: readonly string[], removedContent: string, isLineDisabled: IsLineDisabled);
|
|
29
|
+
}
|
|
30
|
+
export declare class BashContext {
|
|
31
|
+
readonly tool: 'Bash';
|
|
32
|
+
readonly command: string;
|
|
33
|
+
/**
|
|
34
|
+
* `command` with heredoc bodies and prose-in-quotes removed — what a guard should MATCH on, while
|
|
35
|
+
* `command` stays raw for the violation message (a human needs to see what they actually typed).
|
|
36
|
+
*
|
|
37
|
+
* A commit message that merely MENTIONS a blocked command is not that command. This repo's whole
|
|
38
|
+
* subject matter is the git workflow, so its commit messages, docs and the guards' own fix-hint
|
|
39
|
+
* text are full of `git push` / `gh pr …` strings; matching raw text blocks writing about the
|
|
40
|
+
* tooling. A guard that cries wolf is a guard someone eventually turns OFF, which costs far more
|
|
41
|
+
* than the theoretical bypass this opens (`sh -c '<blocked command>'` stops matching). That trade
|
|
42
|
+
* is deliberate: these guards exist to catch a FORGETFUL agent, not an adversarial one.
|
|
43
|
+
*
|
|
44
|
+
* ONLY for blocklist-shaped guards ("if the command matches X, block"), where stripping can only
|
|
45
|
+
* ever block LESS. Never use it in an allowlist-shaped guard such as merged-branch-bash-guard,
|
|
46
|
+
* where stripping could turn a permitted recovery command into a blocked one and deadlock the
|
|
47
|
+
* session — there, match on the raw `command`.
|
|
48
|
+
*/
|
|
49
|
+
readonly commandCode: string;
|
|
50
|
+
/**
|
|
51
|
+
* The tree this command is JUDGED against — the root of the worktree it actually acts on, which
|
|
52
|
+
* is NOT the shell's cwd whenever the command carries a leading `cd <path> &&` (see
|
|
53
|
+
* EffectiveTreeResolver). Guards read git state, the main-sync cache and the decision log from
|
|
54
|
+
* here, so a command aimed at a linked worktree is judged on that worktree's branch, never the
|
|
55
|
+
* primary clone's.
|
|
56
|
+
*/
|
|
57
|
+
readonly workspaceRoot: string;
|
|
58
|
+
/** The directory the command really runs in (post-`cd`). Relative paths resolve against THIS. */
|
|
59
|
+
readonly effectiveCwd: string;
|
|
60
|
+
/** The root owning webpieces.config.json. Equals workspaceRoot except in a linked worktree. */
|
|
61
|
+
readonly governedRoot: string;
|
|
62
|
+
options: RuleOptions;
|
|
63
|
+
constructor(command: string, workspaceRoot: string, effectiveCwd?: string, governedRoot?: string);
|
|
64
|
+
/**
|
|
65
|
+
* A quoted span WITHOUT whitespace is kept: `git checkout "main"` is a real command with a quoted
|
|
66
|
+
* argument, not prose. One containing whitespace is a sentence, and becomes a single space.
|
|
67
|
+
*
|
|
68
|
+
* ONE exception, and it is a false positive that actually fired: a whitespace-free span carrying
|
|
69
|
+
* SHELL METACHARACTERS is dropped too. Keeping the content strips only the quotes, so a jq filter —
|
|
70
|
+
* `--jq '[.[]|select(.title|test("x"))]|length'` — was handed to the guards as bare shell syntax.
|
|
71
|
+
* Every guard then split it on its `|`, `(` and `;` and saw a segment reading exactly `test`, which
|
|
72
|
+
* whole-repo-build-guard classified as the workspace-wide test script and BLOCKED. There was no
|
|
73
|
+
* build anywhere in that command; it polled npm and GitHub.
|
|
74
|
+
*
|
|
75
|
+
* Quoting is precisely how a shell says "this is DATA, not syntax", so honouring the quotes is the
|
|
76
|
+
* correct reading. And because commandCode only ever feeds blocklist-shaped guards, dropping more
|
|
77
|
+
* can only ever block LESS — never turn an allowed command into a refused one.
|
|
78
|
+
*/
|
|
79
|
+
private stripProse;
|
|
80
|
+
}
|
|
81
|
+
export declare class FileContext {
|
|
82
|
+
readonly tool: ToolKind;
|
|
83
|
+
readonly filePath: string;
|
|
84
|
+
readonly relativePath: string;
|
|
85
|
+
readonly workspaceRoot: string;
|
|
86
|
+
/** The git tree that owns filePath. workspaceRoot remains the root that owns policy/config. */
|
|
87
|
+
readonly targetRoot: string;
|
|
88
|
+
/** filePath relative to targetRoot, independent of its governed-root spelling. */
|
|
89
|
+
readonly targetRelativePath: string;
|
|
90
|
+
readonly currentFileLines: number;
|
|
91
|
+
readonly linesAdded: number;
|
|
92
|
+
readonly linesRemoved: number;
|
|
93
|
+
readonly projectedFileLines: number;
|
|
94
|
+
options: RuleOptions;
|
|
95
|
+
constructor(tool: ToolKind, filePath: string, relativePath: string, workspaceRoot: string, currentFileLines: number, linesAdded: number, linesRemoved: number, projectedFileLines: number, targetRoot?: string, targetRelativePath?: string);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The shape of a custom rule loaded from a `rulesDir` (a plain object returned by require()).
|
|
99
|
+
* It carries only metadata + a `check` method — no on/off logic of its own.
|
|
100
|
+
*/
|
|
101
|
+
export interface PlainRule {
|
|
102
|
+
readonly name: string;
|
|
103
|
+
readonly description: string;
|
|
104
|
+
readonly scope: RuleScope;
|
|
105
|
+
readonly files: readonly string[];
|
|
106
|
+
readonly defaultOptions: RuleOptions;
|
|
107
|
+
readonly fixHint: FixHint;
|
|
108
|
+
check(ctx: EditContext | FileContext | BashContext): readonly Violation[];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The runtime contract the runner iterates. Both the built-in rule classes (which extend
|
|
112
|
+
* EditRuleBase/FileRuleBase/BashRuleBase) and the custom-rule adapter satisfy this: they add
|
|
113
|
+
* `shouldRun()` (mode + escape-hatch decision) on top of the PlainRule metadata.
|
|
114
|
+
*/
|
|
115
|
+
export interface Rule extends PlainRule {
|
|
116
|
+
/**
|
|
117
|
+
* The webpieces.config.json key whose entry configures this rule — NOT necessarily `name`.
|
|
118
|
+
*
|
|
119
|
+
* `name` is the operator identity (what a decision-log line and a deny report carry); `configKey`
|
|
120
|
+
* is the switch. They differ wherever several classes implement one POLICY: all four branch-state
|
|
121
|
+
* guards read `branch-state-guard`, all four PR-lifecycle guards read `pr-lifecycle-guard`. Every
|
|
122
|
+
* consumer that asks "which section does this live in?" (filterByMode) or "does this have an
|
|
123
|
+
* entry?" (the fault-Y config-sync check) must read THIS field, because asking for `name` there
|
|
124
|
+
* demands entries under keys the validator rejects as retired.
|
|
125
|
+
*/
|
|
126
|
+
readonly configKey: string;
|
|
127
|
+
shouldRun(): boolean;
|
|
128
|
+
}
|
|
129
|
+
export declare class RuleGroup {
|
|
130
|
+
readonly ruleName: string;
|
|
131
|
+
readonly ruleDescription: string;
|
|
132
|
+
readonly fixHint: FixHint;
|
|
133
|
+
readonly violations: readonly Violation[];
|
|
134
|
+
constructor(ruleName: string, ruleDescription: string, fixHint: FixHint, violations: readonly Violation[]);
|
|
135
|
+
}
|
|
136
|
+
export declare class BlockedResult {
|
|
137
|
+
readonly report: string;
|
|
138
|
+
/**
|
|
139
|
+
* The L0 fault this block IS, in the codebook's own letter (see core/l0-fault-codes.ts), or `-` for
|
|
140
|
+
* an ordinary rule block.
|
|
141
|
+
*
|
|
142
|
+
* It rides on the result rather than being re-derived from the report text because the block is
|
|
143
|
+
* decided deep in the runner (fault C in configMissingBlock, fault Y in checkConfigSync) and
|
|
144
|
+
* STAMPED by the adapter at the terminal boundary, several frames up. Scraping the report for a
|
|
145
|
+
* fault would be a second answer to a question the producer already knows.
|
|
146
|
+
*/
|
|
147
|
+
readonly fault: string;
|
|
148
|
+
constructor(report: string, fault?: string);
|
|
149
|
+
}
|