@yagni-app/code-staging 1.0.1-staging.1194.1 → 1.0.1-staging.1199.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/dist/extension/index.js +48 -3
- package/dist/extension/permission/gate.js +3 -2
- package/dist/extension/permission/guardian.d.ts +20 -0
- package/dist/extension/permission/guardian.js +16 -1
- package/dist/extension/slashCommandFilter.d.ts +30 -0
- package/dist/extension/slashCommandFilter.js +89 -0
- package/package.json +2 -2
package/dist/extension/index.js
CHANGED
|
@@ -40,12 +40,13 @@ import { registerDecisionCommands } from "./decisions.js";
|
|
|
40
40
|
import { makeDecisionCapture } from "./decisionCapture.js";
|
|
41
41
|
import { registerAmbientRecall } from "./recall.js";
|
|
42
42
|
import { resilientFetch } from "./resilientFetch.js";
|
|
43
|
-
import { installUncaughtExceptionMonitor, makeCrashReporter } from "./crashReport.js";
|
|
43
|
+
import { installUncaughtExceptionMonitor, makeCrashReporter, runningUnderTest } from "./crashReport.js";
|
|
44
44
|
import { flushSpool as defaultFlushSpool } from "./spool.js";
|
|
45
45
|
import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
|
|
46
46
|
import { attributionHeaders, fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, isDriverCaller, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
|
|
47
47
|
import { buildYagniProvider } from "./provider.js";
|
|
48
48
|
import { registerChipEditor } from "./chipEditor.js";
|
|
49
|
+
import { registerSlashCommandFilter } from "./slashCommandFilter.js";
|
|
49
50
|
import { defaultMineBeatGit, fileMineBeatMarkers, maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, } from "./mineBeat.js";
|
|
50
51
|
import { makeFlywheelState } from "./flywheel.js";
|
|
51
52
|
import { registerSilentTurnReminder } from "./silentTurnReminder.js";
|
|
@@ -219,6 +220,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
219
220
|
// temp path, and registers the input transform that turns chips into image
|
|
220
221
|
// attachments for the model. No-op outside TUI mode.
|
|
221
222
|
registerChipEditor(pi);
|
|
223
|
+
// Hide pi's built-in slash commands that contradict YAGNI's single-provider,
|
|
224
|
+
// single-identity model (e.g. /share gist upload, /login <provider>) from
|
|
225
|
+
// slash-discovery. TUI autocomplete only; the desktop palette already
|
|
226
|
+
// excludes built-ins, and this is a no-op in RPC/print modes.
|
|
227
|
+
registerSlashCommandFilter(pi);
|
|
222
228
|
// The general subagent tool: delegate self-contained tasks (optionally in
|
|
223
229
|
// parallel) to fresh-context agents defined in .claude/agents / .pi/agents,
|
|
224
230
|
// riding the /go pipeline's child runner. /agents lists what's available.
|
|
@@ -326,7 +332,10 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
326
332
|
const { event: _ignored, ...fields } = payload;
|
|
327
333
|
logEvent({
|
|
328
334
|
source: "guardian",
|
|
329
|
-
|
|
335
|
+
// `malformed` is a real failure (this is the "unclear verdict" bug the
|
|
336
|
+
// whole capture exists to diagnose) — an error, not routine info. The
|
|
337
|
+
// failed-telemetry event is already an error.
|
|
338
|
+
level: event === "guardian_event_post_failed" || payload.outcome === "malformed" ? "error" : "info",
|
|
330
339
|
event,
|
|
331
340
|
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
332
341
|
fields,
|
|
@@ -351,7 +360,43 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
351
360
|
guardianTier,
|
|
352
361
|
guardianDisabled,
|
|
353
362
|
guardianReview: (command, deps) => reviewCommand(command, { ...deps, modelTier: guardianTier }),
|
|
354
|
-
onGuardianReview: (ev) =>
|
|
363
|
+
onGuardianReview: (ev) => {
|
|
364
|
+
guardianLogSink(ev);
|
|
365
|
+
// A malformed verdict is a real failure on our side that must reach
|
|
366
|
+
// Sentry for EVERY workspace (independent of the opt-in storage tier).
|
|
367
|
+
// Fire-and-forget to the dedicated telemetry endpoint; fail-soft, and
|
|
368
|
+
// suppressed under test/eval so a CI run never phones prod.
|
|
369
|
+
if (ev.outcome === "malformed" && !evalMode && !runningUnderTest(env)) {
|
|
370
|
+
void (async () => {
|
|
371
|
+
try {
|
|
372
|
+
const body = {
|
|
373
|
+
outcome: ev.outcome,
|
|
374
|
+
...(ev.rawOutput ? { rawOutput: ev.rawOutput } : {}),
|
|
375
|
+
...(ev.durationMs !== undefined ? { durationMs: ev.durationMs } : {}),
|
|
376
|
+
...(ev.tier ? { tier: ev.tier } : {}),
|
|
377
|
+
};
|
|
378
|
+
const res = await resilientFetch(`${baseUrl}/api/yagni-code/guardian-error`, {
|
|
379
|
+
method: "POST",
|
|
380
|
+
headers: {
|
|
381
|
+
"content-type": "application/json",
|
|
382
|
+
authorization: `Bearer ${getTokenFn() ?? ""}`,
|
|
383
|
+
...attributionHeaders(deps.env),
|
|
384
|
+
},
|
|
385
|
+
body: JSON.stringify(body),
|
|
386
|
+
}, {
|
|
387
|
+
fetchImpl: authedFetch,
|
|
388
|
+
policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
|
|
389
|
+
});
|
|
390
|
+
if (!res.ok) {
|
|
391
|
+
guardianLogSink({ event: "guardian_error_post_failed", status: res.status });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
guardianLogSink({ event: "guardian_error_post_failed", kind: "network" });
|
|
396
|
+
}
|
|
397
|
+
})();
|
|
398
|
+
}
|
|
399
|
+
},
|
|
355
400
|
grants: sessionGrants,
|
|
356
401
|
resolveRepoKey,
|
|
357
402
|
persistGrant: (grant) => {
|
|
@@ -561,13 +561,14 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
561
561
|
ctx.ui.setStatus?.("yagni-guardian", undefined);
|
|
562
562
|
}
|
|
563
563
|
const durationMs = Date.now() - startMs;
|
|
564
|
-
const emitDiag = (outcome, rationale) => {
|
|
564
|
+
const emitDiag = (outcome, rationale, rawOutput) => {
|
|
565
565
|
if (!deps.onGuardianReview)
|
|
566
566
|
return;
|
|
567
567
|
void Promise.resolve(deps.onGuardianReview(buildDiagnosticEvent(outcome, {
|
|
568
568
|
durationMs,
|
|
569
569
|
tier: guardianTier,
|
|
570
570
|
...(rationale ? { rationale } : {}),
|
|
571
|
+
...(rawOutput ? { rawOutput } : {}),
|
|
571
572
|
debug: isDebug(),
|
|
572
573
|
}))).catch(() => { });
|
|
573
574
|
};
|
|
@@ -734,7 +735,7 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
734
735
|
}
|
|
735
736
|
// Guardian failed (timeout/malformed/network/empty/aborted).
|
|
736
737
|
const error = reviewResult.error ?? "network";
|
|
737
|
-
emitDiag(error);
|
|
738
|
+
emitDiag(error, undefined, reviewResult.rawOutput);
|
|
738
739
|
if (error === "aborted" || ctx?.signal?.aborted) {
|
|
739
740
|
// The user aborted mid-consult — silent block: no dialog, no
|
|
740
741
|
// "Guardian unavailable" warning on a turn they deliberately
|
|
@@ -106,7 +106,20 @@ export interface ReviewResult {
|
|
|
106
106
|
verdict: GuardianVerdict | null;
|
|
107
107
|
error?: GuardianError;
|
|
108
108
|
cost: number;
|
|
109
|
+
/**
|
|
110
|
+
* Scrubbed + capped copy of the model output when the verdict failed to
|
|
111
|
+
* parse (`error: "malformed"`). Present so the sink can capture the exact
|
|
112
|
+
* failure shape. Never contains the raw command unredacted: `scrubSecrets`
|
|
113
|
+
* removes secret-shaped values before this is stored.
|
|
114
|
+
*/
|
|
115
|
+
rawOutput?: string;
|
|
109
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Cap on the malformed-output capture. The model's unparseable verdict is
|
|
119
|
+
* usually a single short JSON-ish blob; this bounds a pathological output so
|
|
120
|
+
* it cannot balloon a log line or a Sentry payload.
|
|
121
|
+
*/
|
|
122
|
+
export declare const GUARDIAN_RAW_OUTPUT_CAP = 2048;
|
|
110
123
|
export interface ReviewCommandDeps {
|
|
111
124
|
runStage?: typeof defaultRunStage;
|
|
112
125
|
cwd: string;
|
|
@@ -143,6 +156,12 @@ export interface GuardianDiagnosticEvent {
|
|
|
143
156
|
commandHash?: string;
|
|
144
157
|
/** Debug-only: the Guardian's rationale. */
|
|
145
158
|
rationale?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Scrubbed + capped copy of the unparseable model output, present only for
|
|
161
|
+
* `outcome: "malformed"`. Always-on (NOT debug-gated): it is already
|
|
162
|
+
* `scrubSecrets`-redacted and size-capped at the source.
|
|
163
|
+
*/
|
|
164
|
+
rawOutput?: string;
|
|
146
165
|
}
|
|
147
166
|
/**
|
|
148
167
|
* Create a sanitized diagnostic event. Never includes the raw command text
|
|
@@ -153,6 +172,7 @@ export declare function buildDiagnosticEvent(outcome: GuardianOutcome | Guardian
|
|
|
153
172
|
tier?: string;
|
|
154
173
|
rationale?: string;
|
|
155
174
|
commandHash?: string;
|
|
175
|
+
rawOutput?: string;
|
|
156
176
|
debug?: boolean;
|
|
157
177
|
}): GuardianDiagnosticEvent;
|
|
158
178
|
//# sourceMappingURL=guardian.d.ts.map
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* alternating deny/ask.
|
|
29
29
|
*/
|
|
30
30
|
import { runStage as defaultRunStage } from "../pipeline/runner.js";
|
|
31
|
+
import { scrubSecrets } from "../pipeline/scrubSecrets.js";
|
|
31
32
|
export const DEFAULT_GUARDIAN_LIMITS = {
|
|
32
33
|
maxReviews: 120,
|
|
33
34
|
maxConsecutiveDenials: 3,
|
|
@@ -161,6 +162,12 @@ export function formatGuardianSubtotal(state, limits) {
|
|
|
161
162
|
const plural = state.reviews === 1 ? "review" : "reviews";
|
|
162
163
|
return `Guardian: ${state.reviews} ${plural}.`;
|
|
163
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Cap on the malformed-output capture. The model's unparseable verdict is
|
|
167
|
+
* usually a single short JSON-ish blob; this bounds a pathological output so
|
|
168
|
+
* it cannot balloon a log line or a Sentry payload.
|
|
169
|
+
*/
|
|
170
|
+
export const GUARDIAN_RAW_OUTPUT_CAP = 2048;
|
|
164
171
|
/**
|
|
165
172
|
* The synthetic stage a Guardian consult runs as. Borrows the `plan` StageId
|
|
166
173
|
* (same pattern as the advisor) so it doesn't ripple into feed/reducers. The
|
|
@@ -231,7 +238,14 @@ export async function reviewCommand(command, deps) {
|
|
|
231
238
|
}
|
|
232
239
|
const verdict = parseVerdict(output);
|
|
233
240
|
if (!verdict) {
|
|
234
|
-
|
|
241
|
+
// Scrubbed + capped so the local sink and Sentry can see the exact
|
|
242
|
+
// failure shape without carrying a raw command or a secret it echoed.
|
|
243
|
+
return {
|
|
244
|
+
verdict: null,
|
|
245
|
+
error: "malformed",
|
|
246
|
+
cost,
|
|
247
|
+
rawOutput: scrubSecrets(output).slice(0, GUARDIAN_RAW_OUTPUT_CAP),
|
|
248
|
+
};
|
|
235
249
|
}
|
|
236
250
|
return { verdict, cost };
|
|
237
251
|
}
|
|
@@ -261,6 +275,7 @@ export function buildDiagnosticEvent(outcome, opts) {
|
|
|
261
275
|
outcome,
|
|
262
276
|
...(opts.durationMs !== undefined ? { durationMs: opts.durationMs } : {}),
|
|
263
277
|
...(opts.tier !== undefined ? { tier: opts.tier } : {}),
|
|
278
|
+
...(opts.rawOutput !== undefined ? { rawOutput: opts.rawOutput } : {}),
|
|
264
279
|
};
|
|
265
280
|
if (opts.debug) {
|
|
266
281
|
if (opts.rationale)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* Slash-command filtering for the CLI.
|
|
4
|
+
*
|
|
5
|
+
* pi ships its own built-in slash commands (see `BUILTIN_SLASH_COMMANDS` in
|
|
6
|
+
* pi's `core/slash-commands.js`) and bakes them directly into its interactive
|
|
7
|
+
* autocomplete provider (`createBaseAutocompleteProvider`). Most are
|
|
8
|
+
* pi-generic and harmless (`/new`, `/compact`, `/settings`), but a subset
|
|
9
|
+
* contradict YAGNI's single-provider, single-identity model — or leak pi
|
|
10
|
+
* features YAGNI deliberately does not surface — and only confuse a YAGNI Code
|
|
11
|
+
* user. We hide those from slash-discovery.
|
|
12
|
+
*
|
|
13
|
+
* Scope note: this filters DISCOVERY only. pi dispatches built-in commands in
|
|
14
|
+
* its own private `onSubmit` chain (not from the autocomplete list), so a
|
|
15
|
+
* hidden command can still be typed by hand — the same bar pi itself draws for
|
|
16
|
+
* its easter-egg commands, which are also absent from autocomplete. Removing
|
|
17
|
+
* them from the list is the whole intent: a YAGNI user should not be offered
|
|
18
|
+
* `/share` (ships a transcript to a GitHub gist) or `/login <provider>` (pi
|
|
19
|
+
* provider auth) when YAGNI's model is a single `yagni` provider.
|
|
20
|
+
*
|
|
21
|
+
* The desktop palette is unaffected: pi's RPC `get_commands` (which the desktop
|
|
22
|
+
* app reads) already returns extension commands + prompt templates + skills and
|
|
23
|
+
* never includes built-ins. This wrapper is inert outside interactive TUI mode,
|
|
24
|
+
* where `addAutocompleteProvider` is handed a no-op factory.
|
|
25
|
+
*/
|
|
26
|
+
/** pi built-in slash commands hidden from YAGNI Code autocomplete. */
|
|
27
|
+
export declare const HIDDEN_BUILTIN_SLASH_COMMANDS: readonly string[];
|
|
28
|
+
/** Register the slash-command filter on `session_start` (TUI autocomplete only). */
|
|
29
|
+
export declare function registerSlashCommandFilter(pi: ExtensionAPI): void;
|
|
30
|
+
//# sourceMappingURL=slashCommandFilter.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash-command filtering for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* pi ships its own built-in slash commands (see `BUILTIN_SLASH_COMMANDS` in
|
|
5
|
+
* pi's `core/slash-commands.js`) and bakes them directly into its interactive
|
|
6
|
+
* autocomplete provider (`createBaseAutocompleteProvider`). Most are
|
|
7
|
+
* pi-generic and harmless (`/new`, `/compact`, `/settings`), but a subset
|
|
8
|
+
* contradict YAGNI's single-provider, single-identity model — or leak pi
|
|
9
|
+
* features YAGNI deliberately does not surface — and only confuse a YAGNI Code
|
|
10
|
+
* user. We hide those from slash-discovery.
|
|
11
|
+
*
|
|
12
|
+
* Scope note: this filters DISCOVERY only. pi dispatches built-in commands in
|
|
13
|
+
* its own private `onSubmit` chain (not from the autocomplete list), so a
|
|
14
|
+
* hidden command can still be typed by hand — the same bar pi itself draws for
|
|
15
|
+
* its easter-egg commands, which are also absent from autocomplete. Removing
|
|
16
|
+
* them from the list is the whole intent: a YAGNI user should not be offered
|
|
17
|
+
* `/share` (ships a transcript to a GitHub gist) or `/login <provider>` (pi
|
|
18
|
+
* provider auth) when YAGNI's model is a single `yagni` provider.
|
|
19
|
+
*
|
|
20
|
+
* The desktop palette is unaffected: pi's RPC `get_commands` (which the desktop
|
|
21
|
+
* app reads) already returns extension commands + prompt templates + skills and
|
|
22
|
+
* never includes built-ins. This wrapper is inert outside interactive TUI mode,
|
|
23
|
+
* where `addAutocompleteProvider` is handed a no-op factory.
|
|
24
|
+
*/
|
|
25
|
+
/** pi built-in slash commands hidden from YAGNI Code autocomplete. */
|
|
26
|
+
export const HIDDEN_BUILTIN_SLASH_COMMANDS = [
|
|
27
|
+
"share", // ships a session transcript to a secret GitHub gist (off-brand + privacy footgun)
|
|
28
|
+
"login", // pi provider auth; YAGNI auth is `yagni login`, one provider
|
|
29
|
+
"logout", // pi provider auth; YAGNI auth is `yagni logout`
|
|
30
|
+
"scoped-models", // pi multi-model Ctrl+P cycling; YAGNI pins a single tier
|
|
31
|
+
"fork", // pi session-fork feature YAGNI never surfaces
|
|
32
|
+
"clone", // pi session-clone feature YAGNI never surfaces
|
|
33
|
+
"tree", // pi session-tree feature YAGNI never surfaces
|
|
34
|
+
"trust", // pi project-trust prompt; collides with YAGNI guardrails
|
|
35
|
+
"changelog", // pi's own changelog, not YAGNI's
|
|
36
|
+
];
|
|
37
|
+
const HIDDEN = new Set(HIDDEN_BUILTIN_SLASH_COMMANDS);
|
|
38
|
+
/**
|
|
39
|
+
* True when the text before the cursor is a slash-COMMAND name being typed —
|
|
40
|
+
* a leading `/` plus a name with no interior `/` or space. This mirrors pi's
|
|
41
|
+
* own slash-command detection (its `applyCompletion` treats a prefix with an
|
|
42
|
+
* interior `/` as a file path, e.g. the absolute path `/usr/local/...`), so we
|
|
43
|
+
* never strip a legitimate file-named completion.
|
|
44
|
+
*/
|
|
45
|
+
function isSlashCommandName(textBeforeCursor) {
|
|
46
|
+
return textBeforeCursor.startsWith("/")
|
|
47
|
+
&& !textBeforeCursor.slice(1).includes("/")
|
|
48
|
+
&& !textBeforeCursor.slice(1).includes(" ");
|
|
49
|
+
}
|
|
50
|
+
function filterHiddenSlashSuggestions(suggestions) {
|
|
51
|
+
const items = suggestions.items.filter((item) => !HIDDEN.has(item.value));
|
|
52
|
+
if (items.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
return { ...suggestions, items };
|
|
55
|
+
}
|
|
56
|
+
/** Wrap pi's built-in autocomplete provider so hidden built-ins never complete. */
|
|
57
|
+
function makeYagniAutocompleteProvider(current) {
|
|
58
|
+
return {
|
|
59
|
+
triggerCharacters: current.triggerCharacters,
|
|
60
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
61
|
+
const textBeforeCursor = (lines[cursorLine] ?? "").slice(0, cursorCol);
|
|
62
|
+
if (!isSlashCommandName(textBeforeCursor)) {
|
|
63
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
64
|
+
}
|
|
65
|
+
const suggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
66
|
+
return suggestions ? filterHiddenSlashSuggestions(suggestions) : null;
|
|
67
|
+
},
|
|
68
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
69
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
70
|
+
},
|
|
71
|
+
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
72
|
+
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Register the slash-command filter on `session_start` (TUI autocomplete only). */
|
|
77
|
+
export function registerSlashCommandFilter(pi) {
|
|
78
|
+
pi.on("session_start", (_event, ctx) => {
|
|
79
|
+
if (ctx.mode !== "tui" || !ctx.hasUI)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
ctx.ui.addAutocompleteProvider(makeYagniAutocompleteProvider);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Filtering is cosmetic; a failure must never break session start.
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=slashCommandFilter.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.0.1-staging.
|
|
3
|
+
"version": "1.0.1-staging.1199.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"turndown": "^7.2.4",
|
|
42
42
|
"typebox": "^1.3.15"
|
|
43
43
|
},
|
|
44
|
-
"yagniSourceSha": "
|
|
44
|
+
"yagniSourceSha": "b28af96b1b6e11135b4be8a910f3a4b51b069cbe"
|
|
45
45
|
}
|