@yagni-app/code 0.3.0 → 0.3.2
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/dist/cli.js +12 -0
- package/dist/connectClaudeCode.d.ts +77 -0
- package/dist/connectClaudeCode.js +228 -0
- package/dist/connectCodex.d.ts +75 -0
- package/dist/connectCodex.js +201 -0
- package/dist/crashReport.d.ts +12 -0
- package/dist/crashReport.js +28 -1
- package/dist/extension/approvedPrefixes.d.ts +11 -0
- package/dist/extension/approvedPrefixes.js +30 -0
- package/dist/extension/askAdvisorTool.d.ts +18 -3
- package/dist/extension/askAdvisorTool.js +121 -15
- package/dist/extension/askYagniTool.d.ts +23 -0
- package/dist/extension/askYagniTool.js +42 -2
- package/dist/extension/branding.d.ts +11 -1
- package/dist/extension/branding.js +47 -7
- package/dist/extension/config.d.ts +12 -0
- package/dist/extension/config.js +2 -1
- package/dist/extension/crashReport.d.ts +18 -0
- package/dist/extension/crashReport.js +35 -2
- package/dist/extension/execPolicy.d.ts +17 -1
- package/dist/extension/execPolicy.js +227 -33
- package/dist/extension/flywheel.d.ts +44 -0
- package/dist/extension/flywheel.js +53 -0
- package/dist/extension/footer.d.ts +8 -1
- package/dist/extension/footer.js +33 -19
- package/dist/extension/guardian.d.ts +14 -4
- package/dist/extension/guardian.js +35 -11
- package/dist/extension/index.d.ts +20 -3
- package/dist/extension/index.js +92 -13
- package/dist/extension/mineBeat.d.ts +95 -0
- package/dist/extension/mineBeat.js +193 -0
- package/dist/extension/permission.d.ts +2 -1
- package/dist/extension/permission.js +75 -22
- package/dist/extension/pipeline/goCommand.js +6 -4
- package/dist/extension/pipeline/invocation.d.ts +24 -2
- package/dist/extension/pipeline/invocation.js +30 -2
- package/dist/extension/pipeline/personas.js +2 -2
- package/dist/extension/pipeline/resilience.d.ts +2 -1
- package/dist/extension/pipeline/resilience.js +21 -2
- package/dist/extension/pipeline/runRegistry.d.ts +9 -1
- package/dist/extension/pipeline/runRegistry.js +22 -1
- package/dist/extension/recordDecisionTool.d.ts +8 -0
- package/dist/extension/recordDecisionTool.js +24 -0
- package/dist/extension/subagents.d.ts +7 -1
- package/dist/extension/subagents.js +73 -5
- package/dist/extension/todos.d.ts +28 -1
- package/dist/extension/todos.js +76 -1
- package/dist/extension/ultra.d.ts +27 -0
- package/dist/extension/ultra.js +76 -0
- package/dist/login.d.ts +4 -2
- package/dist/login.js +19 -4
- package/dist/promptEnrichment.d.ts +1 -1
- package/dist/promptEnrichment.js +1 -1
- package/dist/token.d.ts +25 -0
- package/dist/token.js +45 -0
- package/package.json +3 -2
|
@@ -40,6 +40,39 @@ export function crashReportsDisabled(env = process.env) {
|
|
|
40
40
|
const value = env[CRASH_REPORT_DISABLE_ENV];
|
|
41
41
|
return value !== undefined && value !== "" && value !== "0";
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* True when this process is a test run.
|
|
45
|
+
*
|
|
46
|
+
* A crash reporter that phones home from a test suite reports FICTIONAL
|
|
47
|
+
* crashes against real production telemetry. That is not theoretical: the
|
|
48
|
+
* `/go` terminal catch builds a live reporter whenever its caller doesn't
|
|
49
|
+
* inject one, so `goCommand.test.ts`'s deliberate `throw new Error("boom")`
|
|
50
|
+
* POSTed to https://yagni.app on every run — 915 events across Sentry
|
|
51
|
+
* YAGNI-BACKEND-2P / -2Q, from a team that had never witnessed a crash.
|
|
52
|
+
*
|
|
53
|
+
* Injecting a stub at each call site fixes one test; this makes the whole
|
|
54
|
+
* class impossible, so a future test that forgets cannot re-open the leak.
|
|
55
|
+
* Detection is deliberately broad — a false positive costs one unreported
|
|
56
|
+
* crash on a developer machine, a false negative pollutes production.
|
|
57
|
+
*/
|
|
58
|
+
export function runningUnderTest(env = process.env) {
|
|
59
|
+
if (env.NODE_ENV === "test")
|
|
60
|
+
return true;
|
|
61
|
+
// node:test sets this in every spawned test-file process.
|
|
62
|
+
if (env.NODE_TEST_CONTEXT)
|
|
63
|
+
return true;
|
|
64
|
+
if (env.VITEST || env.JEST_WORKER_ID)
|
|
65
|
+
return true;
|
|
66
|
+
// npm/pnpm export the script name being run (`pnpm test`, `pnpm test:file`).
|
|
67
|
+
const script = env.npm_lifecycle_event;
|
|
68
|
+
if (script === "test" || (script !== undefined && script.startsWith("test:")))
|
|
69
|
+
return true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/** Reporting is off when the user disabled it OR this is a test process. */
|
|
73
|
+
export function crashReportsSuppressed(env = process.env) {
|
|
74
|
+
return crashReportsDisabled(env) || runningUnderTest(env);
|
|
75
|
+
}
|
|
43
76
|
const HOME_DIR_RE = /(?:\/(?:Users|home)\/|[A-Za-z]:\\Users\\)[^\s/\\]+/g;
|
|
44
77
|
// Spaces deliberately allowed inside the token (real directories contain
|
|
45
78
|
// them); prose after a path may fold into the kept basename — over-redacts
|
|
@@ -122,7 +155,7 @@ export function makeCrashReporter(opts) {
|
|
|
122
155
|
return async (error, context, repoRoot) => {
|
|
123
156
|
try {
|
|
124
157
|
const env = opts.env ?? process.env;
|
|
125
|
-
if (
|
|
158
|
+
if (crashReportsSuppressed(env))
|
|
126
159
|
return;
|
|
127
160
|
const token = opts.getToken();
|
|
128
161
|
const sanitized = sanitizeCrashError(error, { env, repoRoot });
|
|
@@ -191,7 +224,7 @@ const DETACHED_SENDER_SRC = [
|
|
|
191
224
|
export function reportFatalCrash(error, opts, context) {
|
|
192
225
|
try {
|
|
193
226
|
const env = opts.env ?? process.env;
|
|
194
|
-
if (
|
|
227
|
+
if (crashReportsSuppressed(env))
|
|
195
228
|
return;
|
|
196
229
|
const token = opts.getToken();
|
|
197
230
|
if (!token)
|
|
@@ -35,7 +35,18 @@
|
|
|
35
35
|
* bundler), and external dependencies aren't resolvable from the bundled path.
|
|
36
36
|
*/
|
|
37
37
|
export type TokenEntry = string | {
|
|
38
|
-
op: "pipe" | "and" | "or" | "semi" | "
|
|
38
|
+
op: "pipe" | "and" | "or" | "semi" | "substitution";
|
|
39
|
+
} | {
|
|
40
|
+
op: "redirect";
|
|
41
|
+
direction: "out";
|
|
42
|
+
fd: "stdout" | "stderr";
|
|
43
|
+
target: string;
|
|
44
|
+
append: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
op: "redirect";
|
|
47
|
+
direction: "in";
|
|
48
|
+
} | {
|
|
49
|
+
op: "background";
|
|
39
50
|
};
|
|
40
51
|
/**
|
|
41
52
|
* Parse a shell command string into tokens and control operators.
|
|
@@ -48,6 +59,11 @@ export type TokenEntry = string | {
|
|
|
48
59
|
* - `#` comments (start-of-word to end-of-line, outside quotes)
|
|
49
60
|
* - Shell constructs we flag as unanalyzable: $(), backticks (INCLUDING
|
|
50
61
|
* inside double quotes — bash executes those), >, <, background &
|
|
62
|
+
* - Redirect metadata: stdout/stderr redirects carry fd + target so that
|
|
63
|
+
* safe redirects (2>/dev/null, 2>&1) can be distinguished from unsafe ones
|
|
64
|
+
* (> file.txt). Stdin redirects (<, <<) carry no metadata — they always
|
|
65
|
+
* floor. Background & emits a distinct `background` op (not `semi`) so
|
|
66
|
+
* hasUnhandledConstructs can always catch it.
|
|
51
67
|
*
|
|
52
68
|
* Does NOT handle: variable expansion, glob patterns, heredocs beyond the
|
|
53
69
|
* redirect flag, nested subshells beyond depth tracking. Commands using
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
* - `#` comments (start-of-word to end-of-line, outside quotes)
|
|
46
46
|
* - Shell constructs we flag as unanalyzable: $(), backticks (INCLUDING
|
|
47
47
|
* inside double quotes — bash executes those), >, <, background &
|
|
48
|
+
* - Redirect metadata: stdout/stderr redirects carry fd + target so that
|
|
49
|
+
* safe redirects (2>/dev/null, 2>&1) can be distinguished from unsafe ones
|
|
50
|
+
* (> file.txt). Stdin redirects (<, <<) carry no metadata — they always
|
|
51
|
+
* floor. Background & emits a distinct `background` op (not `semi`) so
|
|
52
|
+
* hasUnhandledConstructs can always catch it.
|
|
48
53
|
*
|
|
49
54
|
* Does NOT handle: variable expansion, glob patterns, heredocs beyond the
|
|
50
55
|
* redirect flag, nested subshells beyond depth tracking. Commands using
|
|
@@ -149,11 +154,13 @@ export function shellParse(command) {
|
|
|
149
154
|
i += 2;
|
|
150
155
|
}
|
|
151
156
|
else {
|
|
152
|
-
// Single & — background operator.
|
|
153
|
-
//
|
|
154
|
-
//
|
|
157
|
+
// Single & — background operator. Emits a distinct `background` op
|
|
158
|
+
// (not `semi`) so hasUnhandledConstructs can always catch it and
|
|
159
|
+
// floor the command. The command before it must still be rule-
|
|
160
|
+
// matched: `rm -rf / &` has to stay forbidden, so emit a separator
|
|
161
|
+
// rather than gluing.
|
|
155
162
|
pushCurrent();
|
|
156
|
-
tokens.push({ op: "
|
|
163
|
+
tokens.push({ op: "background" });
|
|
157
164
|
hasConstruct = true;
|
|
158
165
|
i++;
|
|
159
166
|
}
|
|
@@ -163,18 +170,124 @@ export function shellParse(command) {
|
|
|
163
170
|
tokens.push({ op: "semi" });
|
|
164
171
|
i++;
|
|
165
172
|
continue;
|
|
166
|
-
case ">":
|
|
167
|
-
case "<":
|
|
173
|
+
case ">": {
|
|
168
174
|
pushCurrent();
|
|
169
|
-
|
|
175
|
+
// Check for a preceding fd digit: `2>` → stderr, `1>` → stdout.
|
|
176
|
+
// The digit was emitted as a string token — pop it and use as fd.
|
|
177
|
+
let fd = "stdout";
|
|
178
|
+
if (tokens.length > 0 && typeof tokens[tokens.length - 1] === "string") {
|
|
179
|
+
const last = tokens[tokens.length - 1];
|
|
180
|
+
if (last === "2") {
|
|
181
|
+
fd = "stderr";
|
|
182
|
+
tokens.pop();
|
|
183
|
+
}
|
|
184
|
+
else if (last === "1") {
|
|
185
|
+
fd = "stdout";
|
|
186
|
+
tokens.pop();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
let append = false;
|
|
190
|
+
i++;
|
|
191
|
+
if (command[i] === ">") {
|
|
192
|
+
append = true;
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
while (command[i] === " " || command[i] === "\t")
|
|
196
|
+
i++;
|
|
197
|
+
// Read the target — handles quoted targets (mirrors the main loop's
|
|
198
|
+
// quote logic), fd merges (&N), and bare words.
|
|
199
|
+
let target = "";
|
|
200
|
+
if (command[i] === "&") {
|
|
201
|
+
// fd merge: &1, &2, etc.
|
|
202
|
+
i++;
|
|
203
|
+
let digits = "";
|
|
204
|
+
while (command[i] >= "0" && command[i] <= "9") {
|
|
205
|
+
digits += command[i];
|
|
206
|
+
i++;
|
|
207
|
+
}
|
|
208
|
+
target = "&" + digits;
|
|
209
|
+
}
|
|
210
|
+
else if (command[i] === "'") {
|
|
211
|
+
i++;
|
|
212
|
+
while (i < command.length && command[i] !== "'") {
|
|
213
|
+
target += command[i];
|
|
214
|
+
i++;
|
|
215
|
+
}
|
|
216
|
+
if (i < command.length)
|
|
217
|
+
i++;
|
|
218
|
+
}
|
|
219
|
+
else if (command[i] === '"') {
|
|
220
|
+
i++;
|
|
221
|
+
while (i < command.length && command[i] !== '"') {
|
|
222
|
+
if (command[i] === "\\" && i + 1 < command.length) {
|
|
223
|
+
target += command[i + 1];
|
|
224
|
+
i += 2;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
target += command[i];
|
|
228
|
+
i++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (i < command.length)
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
while (i < command.length &&
|
|
236
|
+
command[i] !== " " && command[i] !== "\t" &&
|
|
237
|
+
command[i] !== "\n" && command[i] !== "\r" &&
|
|
238
|
+
command[i] !== "|" && command[i] !== "&" &&
|
|
239
|
+
command[i] !== ";" && command[i] !== ">" &&
|
|
240
|
+
command[i] !== "<") {
|
|
241
|
+
target += command[i];
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
tokens.push({ op: "redirect", direction: "out", fd, target, append });
|
|
170
246
|
hasConstruct = true;
|
|
171
|
-
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
case "<": {
|
|
250
|
+
pushCurrent();
|
|
251
|
+
// Stdin redirect — always floors (changes program behavior).
|
|
172
252
|
i++;
|
|
173
|
-
if (command[i] ===
|
|
174
|
-
i++; //
|
|
253
|
+
if (command[i] === "<")
|
|
254
|
+
i++; // << heredoc — skip delimiter
|
|
175
255
|
while (command[i] === " " || command[i] === "\t")
|
|
176
256
|
i++;
|
|
257
|
+
// Consume the target (filename or heredoc delimiter) so it doesn't
|
|
258
|
+
// appear as a segment token — mirrors the > case's target reading.
|
|
259
|
+
if (command[i] === "'") {
|
|
260
|
+
i++;
|
|
261
|
+
while (i < command.length && command[i] !== "'")
|
|
262
|
+
i++;
|
|
263
|
+
if (i < command.length)
|
|
264
|
+
i++;
|
|
265
|
+
}
|
|
266
|
+
else if (command[i] === '"') {
|
|
267
|
+
i++;
|
|
268
|
+
while (i < command.length && command[i] !== '"') {
|
|
269
|
+
if (command[i] === "\\" && i + 1 < command.length)
|
|
270
|
+
i += 2;
|
|
271
|
+
else
|
|
272
|
+
i++;
|
|
273
|
+
}
|
|
274
|
+
if (i < command.length)
|
|
275
|
+
i++;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
while (i < command.length &&
|
|
279
|
+
command[i] !== " " && command[i] !== "\t" &&
|
|
280
|
+
command[i] !== "\n" && command[i] !== "\r" &&
|
|
281
|
+
command[i] !== "|" && command[i] !== "&" &&
|
|
282
|
+
command[i] !== ";" && command[i] !== ">" &&
|
|
283
|
+
command[i] !== "<") {
|
|
284
|
+
i++;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
tokens.push({ op: "redirect", direction: "in" });
|
|
288
|
+
hasConstruct = true;
|
|
177
289
|
continue;
|
|
290
|
+
}
|
|
178
291
|
case "$":
|
|
179
292
|
if (command[i + 1] === "(") {
|
|
180
293
|
pushCurrent();
|
|
@@ -213,9 +326,10 @@ export function shellParse(command) {
|
|
|
213
326
|
}
|
|
214
327
|
pushCurrent();
|
|
215
328
|
// If we detected constructs but didn't emit them as operator tokens
|
|
216
|
-
// (e.g.
|
|
217
|
-
//
|
|
218
|
-
|
|
329
|
+
// (e.g. double-quoted substitution), surface that via a trailing
|
|
330
|
+
// substitution token so hasUnhandledConstructs sees it. Background & now
|
|
331
|
+
// emits its own distinct op, so it no longer relies on this fallback.
|
|
332
|
+
if (hasConstruct && !tokens.some((t) => typeof t === "object" && (t.op === "redirect" || t.op === "substitution" || t.op === "background"))) {
|
|
219
333
|
tokens.push({ op: "substitution" });
|
|
220
334
|
}
|
|
221
335
|
return tokens;
|
|
@@ -294,47 +408,58 @@ const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
|
294
408
|
export function tokenize(command) {
|
|
295
409
|
return shellParse(command).filter((t) => typeof t === "string");
|
|
296
410
|
}
|
|
297
|
-
/**
|
|
298
|
-
const SPLIT_OPS = new Set(["pipe", "and", "or", "semi"]);
|
|
411
|
+
/** Ops that split compound commands into segments (pipe, &&, ||, ;, background &). */
|
|
412
|
+
const SPLIT_OPS = new Set(["pipe", "and", "or", "semi", "background"]);
|
|
413
|
+
/** Ops that are safe splittable separators — they do NOT trigger the construct floor.
|
|
414
|
+
* Background & is in SPLIT_OPS (splits segments) but NOT here (always floors). */
|
|
415
|
+
const SAFE_SPLIT_OPS = new Set(["pipe", "and", "or", "semi"]);
|
|
416
|
+
/** A redirect to /dev/null (discard) or &N (fd merge) is safe — no file created. */
|
|
417
|
+
function isSafeRedirect(t) {
|
|
418
|
+
if (typeof t === "string")
|
|
419
|
+
return false;
|
|
420
|
+
if (t.op !== "redirect")
|
|
421
|
+
return false;
|
|
422
|
+
if (t.direction === "in")
|
|
423
|
+
return false; // stdin redirect — always floor
|
|
424
|
+
if (t.target === "/dev/null")
|
|
425
|
+
return true; // discard stderr/stdout — safe
|
|
426
|
+
if (t.target.startsWith("&") && t.target.length > 1)
|
|
427
|
+
return true; // fd merge (2>&1, 1>&2) — safe; bare "&" (>& with no digit) is NOT safe
|
|
428
|
+
return false; // > file.txt, >> file.txt — unsafe
|
|
429
|
+
}
|
|
299
430
|
/**
|
|
300
431
|
* Detect whether the command uses shell constructs we can't statically
|
|
301
|
-
* classify (command substitution, redirects, background &) — anything
|
|
302
|
-
* is NOT a splittable operator
|
|
303
|
-
* carrying them is never auto-allowed, but
|
|
432
|
+
* classify (command substitution, unsafe redirects, background &) — anything
|
|
433
|
+
* that is NOT a safe splittable operator or a safe redirect. These impose a
|
|
434
|
+
* floor of `prompt`: a command carrying them is never auto-allowed, but
|
|
435
|
+
* forbidden matches still win.
|
|
304
436
|
*/
|
|
305
437
|
function hasUnhandledConstructs(command) {
|
|
306
|
-
return shellParse(command).some((t) => typeof t === "object" &&
|
|
438
|
+
return shellParse(command).some((t) => typeof t === "object" && !SAFE_SPLIT_OPS.has(t.op) && !isSafeRedirect(t));
|
|
307
439
|
}
|
|
308
440
|
/**
|
|
309
441
|
* Split a command into token-array segments at control operators (|, &&, ||,
|
|
310
|
-
* ;, newline). Redirect targets
|
|
311
|
-
*
|
|
312
|
-
* carried through (never re-joined into strings)
|
|
442
|
+
* ;, newline, background &). Redirect targets are consumed inside the
|
|
443
|
+
* tokenizer's > / < cases (stored on the redirect op), so no skipNext logic
|
|
444
|
+
* is needed. Token arrays are carried through (never re-joined into strings)
|
|
445
|
+
* so quoting survives.
|
|
313
446
|
*/
|
|
314
447
|
function splitSegmentsTokens(command) {
|
|
315
448
|
const parsed = shellParse(command);
|
|
316
449
|
const segments = [];
|
|
317
450
|
let current = [];
|
|
318
|
-
let skipNext = false;
|
|
319
451
|
for (const t of parsed) {
|
|
320
452
|
if (typeof t === "object") {
|
|
321
453
|
if (SPLIT_OPS.has(t.op)) {
|
|
322
454
|
if (current.length > 0)
|
|
323
455
|
segments.push(current);
|
|
324
456
|
current = [];
|
|
325
|
-
skipNext = false;
|
|
326
457
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// substitution ops are construct markers; the inner text is handled
|
|
331
|
-
// by dangerScan via extractSubstitutions.
|
|
458
|
+
// redirect and substitution ops are construct markers; their targets
|
|
459
|
+
// are consumed inside the tokenizer, and the inner text of $(...) is
|
|
460
|
+
// handled by dangerScan via extractSubstitutions.
|
|
332
461
|
}
|
|
333
462
|
else {
|
|
334
|
-
if (skipNext) {
|
|
335
|
-
skipNext = false;
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
463
|
current.push(t);
|
|
339
464
|
}
|
|
340
465
|
}
|
|
@@ -487,6 +612,16 @@ function classifySegmentTokens(rawTokens, policy, opts) {
|
|
|
487
612
|
}
|
|
488
613
|
const { tokens: strippedTokens, stripped } = stripLeadingTokens(rawTokens);
|
|
489
614
|
if (strippedTokens.length === 0) {
|
|
615
|
+
// env standalone: `env` or `env VAR=val` with no following command prints
|
|
616
|
+
// environment variables — a read-only operation. stripLeadingTokens removes
|
|
617
|
+
// `env` as a wrapper word, leaving empty tokens. Recognize this case instead
|
|
618
|
+
// of returning "empty command segment" (YAG-549).
|
|
619
|
+
if (!opts.forbiddenOnly && rawTokens.length > 0 && basenameToken(rawTokens[0]) === "env") {
|
|
620
|
+
const onlyEnvAndAssignments = rawTokens.every((t) => basenameToken(t) === "env" || ENV_ASSIGNMENT_RE.test(t));
|
|
621
|
+
if (onlyEnvAndAssignments) {
|
|
622
|
+
return { decision: "allow", justification: "print environment variables (read-only)" };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
490
625
|
return opts.forbiddenOnly
|
|
491
626
|
? { decision: "allow", justification: "no forbidden match" }
|
|
492
627
|
: { decision: "prompt", justification: "empty command segment" };
|
|
@@ -510,6 +645,9 @@ function classifySegmentTokens(rawTokens, policy, opts) {
|
|
|
510
645
|
const tailResult = classifySegmentTokens(tail, policy, { ...opts, depth: opts.depth + 1 });
|
|
511
646
|
if (tailResult.decision === "forbidden")
|
|
512
647
|
return tailResult;
|
|
648
|
+
if (tailResult.decision === "allow" && !neverAllow) {
|
|
649
|
+
return { decision: "allow", justification: "xargs forwards to a read-only command" };
|
|
650
|
+
}
|
|
513
651
|
}
|
|
514
652
|
if (opts.forbiddenOnly)
|
|
515
653
|
return { decision: "allow", justification: "no forbidden match" };
|
|
@@ -727,6 +865,7 @@ export const DEFAULT_EXEC_POLICY = {
|
|
|
727
865
|
{ pattern: ["jq"], decision: "allow", justification: "filter JSON to stdout" },
|
|
728
866
|
{ pattern: ["stat"], decision: "allow", justification: "show file metadata" },
|
|
729
867
|
{ pattern: ["file"], decision: "allow", justification: "identify file type" },
|
|
868
|
+
{ pattern: ["strings"], decision: "allow", justification: "extract printable strings from binary files (read-only)" },
|
|
730
869
|
{ pattern: ["basename"], decision: "allow", justification: "strip directory from path" },
|
|
731
870
|
{ pattern: ["dirname"], decision: "allow", justification: "extract directory from path" },
|
|
732
871
|
{ pattern: ["realpath"], decision: "allow", justification: "resolve a path" },
|
|
@@ -760,9 +899,64 @@ export const DEFAULT_EXEC_POLICY = {
|
|
|
760
899
|
{ pattern: ["node", "--version"], decision: "allow", justification: "check node version" },
|
|
761
900
|
{ pattern: ["node", "-v"], decision: "allow", justification: "check node version" },
|
|
762
901
|
{ pattern: ["npm", "ls"], decision: "allow", justification: "list installed packages" },
|
|
902
|
+
{ pattern: ["npm", "list"], decision: "allow", justification: "list installed packages" },
|
|
763
903
|
{ pattern: ["pnpm", "ls"], decision: "allow", justification: "list installed packages" },
|
|
904
|
+
{ pattern: ["pnpm", "list"], decision: "allow", justification: "list installed packages" },
|
|
764
905
|
{ pattern: ["pnpm", "--version"], decision: "allow", justification: "check pnpm version" },
|
|
765
906
|
{ pattern: ["tsc", "--version"], decision: "allow", justification: "check typescript version" },
|
|
907
|
+
// --- allow: CLI tool reads (YAG-549, based on prod Guardian data) ---
|
|
908
|
+
// linear CLI — read subcommands; write subcommands (create/update/delete/start/pr/attach/comment) stay prompt
|
|
909
|
+
{ pattern: ["linear", "issue", "comment", "list"], decision: "allow", justification: "list Linear issue comments (read-only)" },
|
|
910
|
+
{
|
|
911
|
+
pattern: ["linear", "issue"],
|
|
912
|
+
decision: "allow",
|
|
913
|
+
justification: "read Linear issue data",
|
|
914
|
+
unlessTokens: ["start", "create", "update", "delete", "pull-request", "pr", "attach", "comment"],
|
|
915
|
+
},
|
|
916
|
+
{ pattern: ["linear", ["--help", "-h"]], decision: "allow", justification: "show Linear CLI help" },
|
|
917
|
+
{ pattern: ["linear", ["--version", "-V"]], decision: "allow", justification: "show Linear CLI version" },
|
|
918
|
+
// gh api — REST GET (no params, no method override) and GraphQL queries (no mutation)
|
|
919
|
+
// gh api defaults to POST when -f/-F params are present, so params are disqualifying.
|
|
920
|
+
// GraphQL is always POST, but queries are reads; mutations/subscriptions are disqualifying.
|
|
921
|
+
{
|
|
922
|
+
pattern: ["gh", "api", "graphql"],
|
|
923
|
+
decision: "allow",
|
|
924
|
+
justification: "GraphQL query (read-only)",
|
|
925
|
+
unlessTokens: ["query=mutation*", "query=subscription*"],
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
pattern: ["gh", "api"],
|
|
929
|
+
decision: "allow",
|
|
930
|
+
justification: "GitHub API GET request (read-only)",
|
|
931
|
+
unlessTokens: ["--method", "-X", "-X*", "graphql", "POST", "PATCH", "DELETE", "PUT", "-f", "-F", "--raw-field", "--field"],
|
|
932
|
+
},
|
|
933
|
+
{ pattern: ["gh", "release", ["list", "view"]], decision: "allow", justification: "read GitHub release data" },
|
|
934
|
+
{ pattern: ["gh", "label", "list"], decision: "allow", justification: "list GitHub labels" },
|
|
935
|
+
{ pattern: ["gh", "milestone", "list"], decision: "allow", justification: "list GitHub milestones" },
|
|
936
|
+
{ pattern: ["gh", "auth", "status"], decision: "allow", justification: "show GitHub auth status" },
|
|
937
|
+
// git read-only subcommands
|
|
938
|
+
{ pattern: ["git", "tag"], decision: "allow", justification: "list tags (read-only)" },
|
|
939
|
+
{ pattern: ["git", "reflog"], decision: "allow", justification: "show reference log" },
|
|
940
|
+
{ pattern: ["git", "ls-remote"], decision: "allow", justification: "list remote refs" },
|
|
941
|
+
{ pattern: ["git", "cat-file"], decision: "allow", justification: "inspect git objects (read-only)" },
|
|
942
|
+
{ pattern: ["git", "for-each-ref"], decision: "allow", justification: "enumerate refs (read-only)" },
|
|
943
|
+
{ pattern: ["git", "rev-list"], decision: "allow", justification: "walk commit history (read-only)" },
|
|
944
|
+
{ pattern: ["git", "show-ref"], decision: "allow", justification: "list all refs" },
|
|
945
|
+
{ pattern: ["git", "name-rev"], decision: "allow", justification: "map commit to name (read-only)" },
|
|
946
|
+
{ pattern: ["git", "merge-base"], decision: "allow", justification: "find common ancestor (read-only)" },
|
|
947
|
+
// package manager test/lint — routine dev-loop operations
|
|
948
|
+
{ pattern: ["pnpm", "test"], decision: "allow", justification: "run tests (routine dev-loop operation)" },
|
|
949
|
+
{ pattern: ["pnpm", "lint"], decision: "allow", justification: "run linter (routine dev-loop operation)" },
|
|
950
|
+
{ pattern: ["npm", "test"], decision: "allow", justification: "run tests (routine dev-loop operation)" },
|
|
951
|
+
{ pattern: ["npm", "run", "lint"], decision: "allow", justification: "run linter (routine dev-loop operation)" },
|
|
952
|
+
{ pattern: ["npx", "tsc", "--noEmit"], decision: "allow", justification: "typecheck only (no file writes)" },
|
|
953
|
+
{ pattern: ["npx", "tsx", "--test"], decision: "allow", justification: "run tests (routine dev-loop operation)" },
|
|
954
|
+
{ pattern: ["npx", "vitest"], decision: "allow", justification: "run vitest tests (routine dev-loop operation)" },
|
|
955
|
+
{ pattern: ["npx", "vitest", "run"], decision: "allow", justification: "run vitest tests (routine dev-loop operation)" },
|
|
956
|
+
{ pattern: ["npx", "jest"], decision: "allow", justification: "run jest tests (routine dev-loop operation)" },
|
|
957
|
+
// misc read-only commands
|
|
958
|
+
{ pattern: ["printenv"], decision: "allow", justification: "print environment variables (read-only)" },
|
|
959
|
+
{ pattern: ["npm", ["view", "info"]], decision: "allow", justification: "read package metadata from registry" },
|
|
766
960
|
// --- prompt: potentially destructive but context-dependent ---
|
|
767
961
|
{ pattern: ["rm"], decision: "prompt", justification: "file deletion — review the target" },
|
|
768
962
|
{ pattern: ["git", "commit"], decision: "prompt", justification: "creates a commit — confirm intent" },
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session state for the miss-to-record flywheel (org plane, Run 7).
|
|
3
|
+
*
|
|
4
|
+
* When `ask_yagni` answers `no_position`, the server attaches a
|
|
5
|
+
* `recordSuggestion` telling the agent to bank the assumption it proceeds on.
|
|
6
|
+
* The extension owns two client-side guards the server cannot:
|
|
7
|
+
*
|
|
8
|
+
* - **The per-run cap.** At most {@link FLYWHEEL_RUN_CAP} suggestions are
|
|
9
|
+
* surfaced per session; past it the suggestion is suppressed so a busy
|
|
10
|
+
* session cannot flood the ledger with asserted rows.
|
|
11
|
+
* - **Dedupe attribution.** A `record_decision` call that answers the
|
|
12
|
+
* QUESTION a surfaced suggestion asked about is a mid-run agent with no
|
|
13
|
+
* human to adjudicate a near-duplicate, so it sends `dedupe: true` (the
|
|
14
|
+
* backend then returns the existing row instead of inserting). The
|
|
15
|
+
* attribution is correlated to the suggested question — an unrelated
|
|
16
|
+
* record_decision (a different judgment the agent banks mid-run, or a
|
|
17
|
+
* human `/decide`) never inherits the flag.
|
|
18
|
+
*/
|
|
19
|
+
/** Most flywheel suggestions surfaced per session. */
|
|
20
|
+
export declare const FLYWHEEL_RUN_CAP = 3;
|
|
21
|
+
export interface FlywheelState {
|
|
22
|
+
/** Suggestions surfaced so far this session. */
|
|
23
|
+
suggestionsShown: number;
|
|
24
|
+
/**
|
|
25
|
+
* The question of the most recent surfaced suggestion, until a matching
|
|
26
|
+
* record_decision consumes it. Only a record answering THIS question is
|
|
27
|
+
* flywheel-attributed (sends `dedupe: true`).
|
|
28
|
+
*/
|
|
29
|
+
pendingQuestion: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function makeFlywheelState(): FlywheelState;
|
|
32
|
+
/** May another suggestion be surfaced? */
|
|
33
|
+
export declare function canSurfaceSuggestion(state: FlywheelState): boolean;
|
|
34
|
+
/** Record that a suggestion (asking about `question`) reached the model. */
|
|
35
|
+
export declare function noteSuggestionSurfaced(state: FlywheelState, question: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Consume the flywheel attribution for a record_decision call. Returns true
|
|
38
|
+
* (and clears the pending question) only when the recorded question matches
|
|
39
|
+
* the surfaced suggestion's; a mismatch leaves the attribution pending — the
|
|
40
|
+
* agent may record other judgments before circling back, and a fail-safe
|
|
41
|
+
* mismatch simply means a normal insert (no dedupe), never a swallowed write.
|
|
42
|
+
*/
|
|
43
|
+
export declare function consumeFlywheelAttribution(state: FlywheelState, recordedQuestion: string): boolean;
|
|
44
|
+
//# sourceMappingURL=flywheel.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session state for the miss-to-record flywheel (org plane, Run 7).
|
|
3
|
+
*
|
|
4
|
+
* When `ask_yagni` answers `no_position`, the server attaches a
|
|
5
|
+
* `recordSuggestion` telling the agent to bank the assumption it proceeds on.
|
|
6
|
+
* The extension owns two client-side guards the server cannot:
|
|
7
|
+
*
|
|
8
|
+
* - **The per-run cap.** At most {@link FLYWHEEL_RUN_CAP} suggestions are
|
|
9
|
+
* surfaced per session; past it the suggestion is suppressed so a busy
|
|
10
|
+
* session cannot flood the ledger with asserted rows.
|
|
11
|
+
* - **Dedupe attribution.** A `record_decision` call that answers the
|
|
12
|
+
* QUESTION a surfaced suggestion asked about is a mid-run agent with no
|
|
13
|
+
* human to adjudicate a near-duplicate, so it sends `dedupe: true` (the
|
|
14
|
+
* backend then returns the existing row instead of inserting). The
|
|
15
|
+
* attribution is correlated to the suggested question — an unrelated
|
|
16
|
+
* record_decision (a different judgment the agent banks mid-run, or a
|
|
17
|
+
* human `/decide`) never inherits the flag.
|
|
18
|
+
*/
|
|
19
|
+
/** Most flywheel suggestions surfaced per session. */
|
|
20
|
+
export const FLYWHEEL_RUN_CAP = 3;
|
|
21
|
+
export function makeFlywheelState() {
|
|
22
|
+
return { suggestionsShown: 0, pendingQuestion: null };
|
|
23
|
+
}
|
|
24
|
+
/** May another suggestion be surfaced? */
|
|
25
|
+
export function canSurfaceSuggestion(state) {
|
|
26
|
+
return state.suggestionsShown < FLYWHEEL_RUN_CAP;
|
|
27
|
+
}
|
|
28
|
+
/** Loose textual identity: case- and whitespace-insensitive, terminal punctuation ignored. */
|
|
29
|
+
function normalizeQuestion(question) {
|
|
30
|
+
return question.toLowerCase().replace(/\s+/g, " ").replace(/[.?!\s]+$/g, "").trim();
|
|
31
|
+
}
|
|
32
|
+
/** Record that a suggestion (asking about `question`) reached the model. */
|
|
33
|
+
export function noteSuggestionSurfaced(state, question) {
|
|
34
|
+
state.suggestionsShown += 1;
|
|
35
|
+
state.pendingQuestion = question;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Consume the flywheel attribution for a record_decision call. Returns true
|
|
39
|
+
* (and clears the pending question) only when the recorded question matches
|
|
40
|
+
* the surfaced suggestion's; a mismatch leaves the attribution pending — the
|
|
41
|
+
* agent may record other judgments before circling back, and a fail-safe
|
|
42
|
+
* mismatch simply means a normal insert (no dedupe), never a swallowed write.
|
|
43
|
+
*/
|
|
44
|
+
export function consumeFlywheelAttribution(state, recordedQuestion) {
|
|
45
|
+
if (state.pendingQuestion === null)
|
|
46
|
+
return false;
|
|
47
|
+
if (normalizeQuestion(state.pendingQuestion) !== normalizeQuestion(recordedQuestion)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
state.pendingQuestion = null;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=flywheel.js.map
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
42
42
|
import type { ModeHolder, PermissionMode } from "./permission.js";
|
|
43
43
|
export declare const BRANCH_MAX_WIDTH = 60;
|
|
44
|
+
export declare function cyclePermissionMode(current: PermissionMode): PermissionMode;
|
|
45
|
+
export declare function isShiftTab(data: string): boolean;
|
|
46
|
+
export declare const GIT_MUTATING_PATTERN: RegExp;
|
|
44
47
|
/**
|
|
45
48
|
* Resolve the status bar's left pad from the launcher's `YAGNI_PAD_X` env so it
|
|
46
49
|
* aligns with the editor input and the chat/output area on one shared column.
|
|
@@ -102,7 +105,11 @@ export declare function renderFooterLines(input: {
|
|
|
102
105
|
* and returns the component `setFooter` expects. Called from the
|
|
103
106
|
* `session_start` handler in index.ts.
|
|
104
107
|
*/
|
|
105
|
-
export
|
|
108
|
+
export interface FooterInvalidateHandle {
|
|
109
|
+
invalidateGit(): void;
|
|
110
|
+
requestRender(): void;
|
|
111
|
+
}
|
|
112
|
+
export declare function createYagniFooterFactory(ctx: ExtensionContext, modeHolder?: ModeHolder, invalidateHandle?: FooterInvalidateHandle): (_tui: unknown, theme: Theme, footerData: ReadonlyFooterDataProvider) => {
|
|
106
113
|
render(width: number): string[];
|
|
107
114
|
invalidate(): void;
|
|
108
115
|
dispose(): void;
|