@yagni-app/code 1.0.0 → 1.0.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/README.md +42 -0
- package/dist/cli.js +231 -6
- package/dist/crashReport.d.ts +8 -0
- package/dist/crashReport.js +13 -1
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +33 -0
- package/dist/extension/askAdvisorTool.d.ts +14 -0
- package/dist/extension/askAdvisorTool.js +16 -4
- package/dist/extension/askYagniTool.d.ts +1 -1
- package/dist/extension/askYagniTool.js +21 -0
- package/dist/extension/branding.d.ts +15 -0
- package/dist/extension/branding.js +76 -0
- package/dist/extension/childUsage.d.ts +40 -0
- package/dist/extension/childUsage.js +43 -0
- package/dist/extension/chipEditor.d.ts +22 -1
- package/dist/extension/chipEditor.js +58 -5
- package/dist/extension/condensedTools.d.ts +97 -0
- package/dist/extension/condensedTools.js +396 -0
- package/dist/extension/diffStat.d.ts +62 -0
- package/dist/extension/diffStat.js +158 -0
- package/dist/extension/footer.d.ts +15 -1
- package/dist/extension/footer.js +35 -15
- package/dist/extension/index.d.ts +6 -0
- package/dist/extension/index.js +126 -6
- package/dist/extension/permission/execPolicy.js +47 -0
- package/dist/extension/permission/gate.d.ts +6 -0
- package/dist/extension/permission/gate.js +11 -2
- package/dist/extension/permission/guardian.d.ts +20 -0
- package/dist/extension/permission/guardian.js +16 -1
- package/dist/extension/pipeline/goCommand.d.ts +8 -0
- package/dist/extension/pipeline/goCommand.js +8 -0
- package/dist/extension/pipeline/invocation.d.ts +7 -0
- package/dist/extension/pipeline/invocation.js +7 -0
- package/dist/extension/pipeline/personas.js +4 -4
- package/dist/extension/pipeline/runner.d.ts +1 -0
- package/dist/extension/pipeline/runner.js +15 -3
- package/dist/extension/pipeline/sessionWorktree.d.ts +64 -0
- package/dist/extension/pipeline/sessionWorktree.js +225 -0
- package/dist/extension/scratchpad.d.ts +66 -0
- package/dist/extension/scratchpad.js +93 -0
- package/dist/extension/slashCommandFilter.d.ts +30 -0
- package/dist/extension/slashCommandFilter.js +89 -0
- package/dist/extension/subagents.d.ts +21 -1
- package/dist/extension/subagents.js +34 -5
- package/dist/extension/todos.d.ts +1 -0
- package/dist/extension/todos.js +15 -0
- package/dist/extension/toolRuns.d.ts +92 -0
- package/dist/extension/toolRuns.js +201 -0
- package/dist/extension/webFetchTool.js +2 -0
- package/dist/extension/workingLine.d.ts +49 -0
- package/dist/extension/workingLine.js +116 -0
- package/dist/feedback.d.ts +77 -0
- package/dist/feedback.js +500 -0
- package/dist/goHeadless.d.ts +3 -0
- package/dist/goHeadless.js +13 -0
- package/dist/launch.d.ts +8 -0
- package/dist/launch.js +6 -0
- package/dist/otel.d.ts +150 -0
- package/dist/otel.js +291 -0
- package/dist/outputFormat.d.ts +83 -0
- package/dist/outputFormat.js +207 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +13 -0
- package/dist/worktreeArgs.d.ts +43 -0
- package/dist/worktreeArgs.js +96 -0
- package/package.json +3 -2
package/dist/extension/footer.js
CHANGED
|
@@ -42,6 +42,7 @@ import { spawnSync } from "node:child_process";
|
|
|
42
42
|
import { statSync } from "node:fs";
|
|
43
43
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
44
44
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
45
|
+
import { createDiffStatCache, formatDiffStat } from "./diffStat.js";
|
|
45
46
|
export const BRANCH_MAX_WIDTH = 60;
|
|
46
47
|
const WORKTREE_MAX_WIDTH = 30;
|
|
47
48
|
/** Section separator: single space + middle dot + single space. */
|
|
@@ -229,23 +230,34 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
229
230
|
// terminal edge. Truncation runs against the reduced content width.
|
|
230
231
|
const pad = " ".repeat(Math.max(0, Math.min(3, Math.floor(padX))));
|
|
231
232
|
const contentWidth = Math.max(1, width - pad.length);
|
|
232
|
-
// Line 1: folder · [worktree] · branch
|
|
233
|
+
// Line 1: folder · [worktree] · branch [+N -M]
|
|
233
234
|
const line1Parts = [theme.fg("accent", input.git.folder)];
|
|
234
235
|
if (input.git.inRepo) {
|
|
235
236
|
if (input.git.worktree)
|
|
236
237
|
line1Parts.push(theme.fg("warning", `[${input.git.worktree}]`));
|
|
237
|
-
if (input.git.branch)
|
|
238
|
-
|
|
238
|
+
if (input.git.branch) {
|
|
239
|
+
const branchSpan = theme.fg("border", truncateEnd(input.git.branch, BRANCH_MAX_WIDTH, dim("…")));
|
|
240
|
+
// Diff badge rides the branch segment (Kimi's `branch [ +N -M ]` shape),
|
|
241
|
+
// colored dim — the same grey as the cost stats on line 2.
|
|
242
|
+
const diffSpan = input.diff ? ` ${dim(`[${input.diff}]`)}` : "";
|
|
243
|
+
line1Parts.push(branchSpan + diffSpan);
|
|
244
|
+
}
|
|
239
245
|
}
|
|
240
246
|
const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
|
|
241
247
|
// Line 2: [mode ·] model · ↑in ↓out $cost · ctx%
|
|
248
|
+
// Child-process spend (subagents, advisor, /go) joins the driver totals so
|
|
249
|
+
// the footer matches the per-child receipts and /cost's session scope.
|
|
250
|
+
const child = input.childUsage;
|
|
251
|
+
const totalIn = input.usage.input + (child?.input ?? 0);
|
|
252
|
+
const totalOut = input.usage.output + (child?.output ?? 0);
|
|
253
|
+
const totalCost = input.usage.cost + (child?.cost ?? 0);
|
|
242
254
|
const statParts = [];
|
|
243
|
-
if (
|
|
244
|
-
statParts.push(`↑${formatTokens(
|
|
245
|
-
if (
|
|
246
|
-
statParts.push(`↓${formatTokens(
|
|
247
|
-
if (
|
|
248
|
-
statParts.push(`$${
|
|
255
|
+
if (totalIn)
|
|
256
|
+
statParts.push(`↑${formatTokens(totalIn)}`);
|
|
257
|
+
if (totalOut)
|
|
258
|
+
statParts.push(`↓${formatTokens(totalOut)}`);
|
|
259
|
+
if (totalCost)
|
|
260
|
+
statParts.push(`$${totalCost.toFixed(3)}`);
|
|
249
261
|
const stats = statParts.join(" ");
|
|
250
262
|
const percentText = input.contextPercent === null ? "?" : `${Math.round(input.contextPercent)}%`;
|
|
251
263
|
const line2Parts = [];
|
|
@@ -266,20 +278,26 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
266
278
|
}
|
|
267
279
|
return lines;
|
|
268
280
|
}
|
|
269
|
-
export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
281
|
+
export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle, childUsage) {
|
|
270
282
|
return (_tui, theme, footerData) => {
|
|
271
283
|
let gitCache;
|
|
272
|
-
|
|
284
|
+
let diffStatCache;
|
|
285
|
+
const clearCaches = () => {
|
|
273
286
|
gitCache = undefined;
|
|
274
|
-
|
|
287
|
+
diffStatCache = undefined;
|
|
288
|
+
};
|
|
289
|
+
const unsubscribeBranch = footerData.onBranchChange?.(clearCaches);
|
|
275
290
|
const gitInfo = () => {
|
|
276
291
|
if (!gitCache) {
|
|
277
|
-
|
|
292
|
+
// Read cwd once; both the git info and the diff cache key off it.
|
|
293
|
+
const dir = ctx.sessionManager.getCwd();
|
|
294
|
+
gitCache = detectGitInfo(dir, process.env.HOME || process.env.USERPROFILE);
|
|
295
|
+
diffStatCache = createDiffStatCache(dir);
|
|
278
296
|
}
|
|
279
297
|
return gitCache;
|
|
280
298
|
};
|
|
281
299
|
if (invalidateHandle) {
|
|
282
|
-
invalidateHandle.invalidateGit =
|
|
300
|
+
invalidateHandle.invalidateGit = clearCaches;
|
|
283
301
|
invalidateHandle.requestRender = () => { _tui?.requestRender?.(); };
|
|
284
302
|
}
|
|
285
303
|
return {
|
|
@@ -291,13 +309,15 @@ export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
|
291
309
|
git: gitInfo(),
|
|
292
310
|
model: ctx.model?.id ?? "no-model",
|
|
293
311
|
mode: modeHolder?.get() ?? null,
|
|
312
|
+
diff: formatDiffStat(diffStatCache?.get() ?? null),
|
|
294
313
|
usage: collectUsage(ctx.sessionManager),
|
|
314
|
+
childUsage: childUsage?.read(),
|
|
295
315
|
contextPercent: ctx.getContextUsage()?.percent ?? null,
|
|
296
316
|
statuses,
|
|
297
317
|
}, theme, width, resolveFooterPadX());
|
|
298
318
|
},
|
|
299
319
|
invalidate() {
|
|
300
|
-
|
|
320
|
+
clearCaches();
|
|
301
321
|
},
|
|
302
322
|
dispose() {
|
|
303
323
|
unsubscribeBranch?.();
|
|
@@ -186,6 +186,12 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
|
|
|
186
186
|
export type { TokenProvider, TokenProviderDeps, ScheduleFn } from "./tokenProvider.js";
|
|
187
187
|
export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
|
|
188
188
|
export type { SpoolEntry, SpoolClientOpts, FlushOutcome, JudgmentWriteOutcome } from "./spool.js";
|
|
189
|
+
export { registerCondensedTools, displayPath, isScratchpadPath, primaryArg, formatRowTitle, formatWriteBody, formatEditBody, formatBashErrorBody, formatBashPartialBody, formatExpandedOutput, splitBashError, countPatchAdditions, WRITE_PREVIEW_LINES, DIFF_PREVIEW_LINES, } from "./condensedTools.js";
|
|
190
|
+
export type { RegisterCondensedToolsDeps, RowStatus } from "./condensedTools.js";
|
|
191
|
+
export { ToolRunTracker, summarizeRun, isQuiet, kindForTool } from "./toolRuns.js";
|
|
192
|
+
export type { RowKind, ToolRow, ToolRowPatch } from "./toolRuns.js";
|
|
193
|
+
export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
|
|
194
|
+
export type { WorkingLineHandle, RegisterWorkingLineDeps } from "./workingLine.js";
|
|
189
195
|
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
190
196
|
export type { CrashReporter, CrashReporterOpts, FatalCrashOpts, SanitizedCrash } from "./crashReport.js";
|
|
191
197
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/extension/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
|
|
4
|
+
import { makeChildUsageState } from "./childUsage.js";
|
|
4
5
|
import { appendGrant, loadGrants, resolveRepoKey, storagePrefix } from "./permission/approvedPrefixes.js";
|
|
5
6
|
import { redactCommand } from "./redact.js";
|
|
6
7
|
import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs } from "./permission/guardian.js";
|
|
@@ -16,6 +17,7 @@ import { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
|
16
17
|
import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
|
|
17
18
|
import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA } from "./branding.js";
|
|
18
19
|
import { claudeRulesSection } from "./claudeRules.js";
|
|
20
|
+
import { ensureScratchpadDir, SCRATCHPAD_TMPDIR_ENV, scratchpadDir as scratchpadDirFor, scratchpadSection } from "./scratchpad.js";
|
|
19
21
|
import { registerCostCommand } from "./costHud.js";
|
|
20
22
|
import { isDebug } from "./diagnostics.js";
|
|
21
23
|
import { logEvent } from "./errorSink.js";
|
|
@@ -39,15 +41,19 @@ import { registerDecisionCommands } from "./decisions.js";
|
|
|
39
41
|
import { makeDecisionCapture } from "./decisionCapture.js";
|
|
40
42
|
import { registerAmbientRecall } from "./recall.js";
|
|
41
43
|
import { resilientFetch } from "./resilientFetch.js";
|
|
42
|
-
import { installUncaughtExceptionMonitor, makeCrashReporter } from "./crashReport.js";
|
|
44
|
+
import { installUncaughtExceptionMonitor, makeCrashReporter, runningUnderTest } from "./crashReport.js";
|
|
43
45
|
import { flushSpool as defaultFlushSpool } from "./spool.js";
|
|
44
46
|
import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
|
|
45
47
|
import { attributionHeaders, fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, isDriverCaller, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
|
|
46
48
|
import { buildYagniProvider } from "./provider.js";
|
|
47
49
|
import { registerChipEditor } from "./chipEditor.js";
|
|
50
|
+
import { registerSlashCommandFilter } from "./slashCommandFilter.js";
|
|
48
51
|
import { defaultMineBeatGit, fileMineBeatMarkers, maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, } from "./mineBeat.js";
|
|
49
52
|
import { makeFlywheelState } from "./flywheel.js";
|
|
50
53
|
import { registerSilentTurnReminder } from "./silentTurnReminder.js";
|
|
54
|
+
import { registerCondensedTools } from "./condensedTools.js";
|
|
55
|
+
import { isDesktopSurface } from "./surface.js";
|
|
56
|
+
import { registerWorkingLine, WORKING_INDICATOR_FRAMES } from "./workingLine.js";
|
|
51
57
|
function isEvalMode(env = process.env) {
|
|
52
58
|
return env.YAGNI_CODE_EVAL_MODE === "1";
|
|
53
59
|
}
|
|
@@ -160,6 +166,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
160
166
|
// no-position record suggestions reach the model) and record_decision
|
|
161
167
|
// (flywheel-attributed records send dedupe: true). Run 7.
|
|
162
168
|
const flywheelState = makeFlywheelState();
|
|
169
|
+
// The composed streaming status line ("Shaping… (12m 54s · ↓ 47.5k tokens)").
|
|
170
|
+
// Registered before the subagent/advisor tools: they publish their live
|
|
171
|
+
// progress through this handle so the elapsed/token suffix survives their
|
|
172
|
+
// overrides. TUI-gated internally (agent_start checks ctx.mode).
|
|
173
|
+
const workingLine = registerWorkingLine(pi);
|
|
163
174
|
pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
|
|
164
175
|
// WebFetch (YAG-578): read an arbitrary URL as clean markdown + a
|
|
165
176
|
// standard-tier extraction, replacing the bash + curl + python dance.
|
|
@@ -177,7 +188,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
177
188
|
// hide it forever from a session that switched TO Advanced. One state handle
|
|
178
189
|
// per session, shared with /advise so they draw on the same cap.
|
|
179
190
|
const advisorState = makeAdvisorState();
|
|
180
|
-
|
|
191
|
+
// Session child-usage accumulator: every child process's spend (subagent
|
|
192
|
+
// tasks, advisor consults, Guardian reviews, /go runs) lands here so the
|
|
193
|
+
// footer's session totals include what the per-child receipts print.
|
|
194
|
+
const childUsage = makeChildUsageState();
|
|
195
|
+
const askAdvisorTool = makeAskAdvisorTool({ state: advisorState, workingLine, childUsage });
|
|
181
196
|
pi.registerTool(askAdvisorTool);
|
|
182
197
|
// /advise runs the SAME tool, sharing the state handle, so a manual consult
|
|
183
198
|
// draws on the same cap rather than opening a side channel around it.
|
|
@@ -210,6 +225,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
210
225
|
// temp path, and registers the input transform that turns chips into image
|
|
211
226
|
// attachments for the model. No-op outside TUI mode.
|
|
212
227
|
registerChipEditor(pi);
|
|
228
|
+
// Hide pi's built-in slash commands that contradict YAGNI's single-provider,
|
|
229
|
+
// single-identity model (e.g. /share gist upload, /login <provider>) from
|
|
230
|
+
// slash-discovery. TUI autocomplete only; the desktop palette already
|
|
231
|
+
// excludes built-ins, and this is a no-op in RPC/print modes.
|
|
232
|
+
registerSlashCommandFilter(pi);
|
|
213
233
|
// The general subagent tool: delegate self-contained tasks (optionally in
|
|
214
234
|
// parallel) to fresh-context agents defined in .claude/agents / .pi/agents,
|
|
215
235
|
// riding the /go pipeline's child runner. /agents lists what's available.
|
|
@@ -217,7 +237,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
217
237
|
// driver's delegation identity for the diamond directive (see the
|
|
218
238
|
// before_agent_start handler below) and widens the tool's fan-out ceiling.
|
|
219
239
|
const ultraHolder = createUltraHolder();
|
|
220
|
-
registerSubagents(pi, { isUltra: () => ultraHolder.get() });
|
|
240
|
+
registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine, childUsage });
|
|
221
241
|
registerUltraCommand(pi, ultraHolder);
|
|
222
242
|
// Shared fetch timeout for the small, interactive display-path reads below
|
|
223
243
|
// (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
|
|
@@ -227,6 +247,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
227
247
|
// The grounded multi-agent pipeline entry point: /go <ticket> runs
|
|
228
248
|
// map → plan → implement → review → fix, each child grounded by inheritance.
|
|
229
249
|
registerGoCommand(pi, {
|
|
250
|
+
childUsage,
|
|
230
251
|
// /ultra is one dial for the whole session: the same holder the subagent
|
|
231
252
|
// tool reads widens the implement diamond's parallel ceiling (4 -> 8) for
|
|
232
253
|
// the fan and its fix turns. Read per run, so a toggle lands on the next /go.
|
|
@@ -317,7 +338,10 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
317
338
|
const { event: _ignored, ...fields } = payload;
|
|
318
339
|
logEvent({
|
|
319
340
|
source: "guardian",
|
|
320
|
-
|
|
341
|
+
// `malformed` is a real failure (this is the "unclear verdict" bug the
|
|
342
|
+
// whole capture exists to diagnose) — an error, not routine info. The
|
|
343
|
+
// failed-telemetry event is already an error.
|
|
344
|
+
level: event === "guardian_event_post_failed" || payload.outcome === "malformed" ? "error" : "info",
|
|
321
345
|
event,
|
|
322
346
|
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
323
347
|
fields,
|
|
@@ -341,8 +365,45 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
341
365
|
guardianLimits,
|
|
342
366
|
guardianTier,
|
|
343
367
|
guardianDisabled,
|
|
368
|
+
childUsage,
|
|
344
369
|
guardianReview: (command, deps) => reviewCommand(command, { ...deps, modelTier: guardianTier }),
|
|
345
|
-
onGuardianReview: (ev) =>
|
|
370
|
+
onGuardianReview: (ev) => {
|
|
371
|
+
guardianLogSink(ev);
|
|
372
|
+
// A malformed verdict is a real failure on our side that must reach
|
|
373
|
+
// Sentry for EVERY workspace (independent of the opt-in storage tier).
|
|
374
|
+
// Fire-and-forget to the dedicated telemetry endpoint; fail-soft, and
|
|
375
|
+
// suppressed under test/eval so a CI run never phones prod.
|
|
376
|
+
if (ev.outcome === "malformed" && !evalMode && !runningUnderTest(env)) {
|
|
377
|
+
void (async () => {
|
|
378
|
+
try {
|
|
379
|
+
const body = {
|
|
380
|
+
outcome: ev.outcome,
|
|
381
|
+
...(ev.rawOutput ? { rawOutput: ev.rawOutput } : {}),
|
|
382
|
+
...(ev.durationMs !== undefined ? { durationMs: ev.durationMs } : {}),
|
|
383
|
+
...(ev.tier ? { tier: ev.tier } : {}),
|
|
384
|
+
};
|
|
385
|
+
const res = await resilientFetch(`${baseUrl}/api/yagni-code/guardian-error`, {
|
|
386
|
+
method: "POST",
|
|
387
|
+
headers: {
|
|
388
|
+
"content-type": "application/json",
|
|
389
|
+
authorization: `Bearer ${getTokenFn() ?? ""}`,
|
|
390
|
+
...attributionHeaders(deps.env),
|
|
391
|
+
},
|
|
392
|
+
body: JSON.stringify(body),
|
|
393
|
+
}, {
|
|
394
|
+
fetchImpl: authedFetch,
|
|
395
|
+
policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
|
|
396
|
+
});
|
|
397
|
+
if (!res.ok) {
|
|
398
|
+
guardianLogSink({ event: "guardian_error_post_failed", status: res.status });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
guardianLogSink({ event: "guardian_error_post_failed", kind: "network" });
|
|
403
|
+
}
|
|
404
|
+
})();
|
|
405
|
+
}
|
|
406
|
+
},
|
|
346
407
|
grants: sessionGrants,
|
|
347
408
|
resolveRepoKey,
|
|
348
409
|
persistGrant: (grant) => {
|
|
@@ -593,6 +654,51 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
593
654
|
// pointers. Computed once per activation (rules are launch-time state, like
|
|
594
655
|
// pi's own skill discovery); fail-soft to null.
|
|
595
656
|
const rulesSection = claudeRulesSection(deps.env ?? process.env);
|
|
657
|
+
// YAG-575: session scratchpad — a permission-free dir the agent writes its
|
|
658
|
+
// working state to. Computed + ensured once at activation (like rulesSection,
|
|
659
|
+
// which must exist before the first before_agent_start), not on session_start,
|
|
660
|
+
// so the very first turn already carries the section. Fails closed: no
|
|
661
|
+
// sessionId (a bare pi run) or a failed mkdir means no section, and the mkdir
|
|
662
|
+
// failure is logged so a missing scratchpad is not silent.
|
|
663
|
+
const scratchpadDirPath = scratchpadDirFor({
|
|
664
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
665
|
+
cwd: process.cwd(),
|
|
666
|
+
tmp: env[SCRATCHPAD_TMPDIR_ENV] || undefined,
|
|
667
|
+
});
|
|
668
|
+
let scratchpadSectionText;
|
|
669
|
+
if (scratchpadDirPath) {
|
|
670
|
+
const ensured = ensureScratchpadDir(scratchpadDirPath);
|
|
671
|
+
if (ensured) {
|
|
672
|
+
scratchpadSectionText = scratchpadSection(ensured);
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
logEvent({
|
|
676
|
+
source: "scratchpad",
|
|
677
|
+
level: "error",
|
|
678
|
+
event: "scratchpad_mkdir_failed",
|
|
679
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// The condensed Claude Code-style transcript: the seven built-ins re-render
|
|
684
|
+
// as flat "● Tool(arg)" rows, quiet runs collapse into one "Read 2 files,
|
|
685
|
+
// ran 2 shell commands" summary line, and project writes/edits keep visible
|
|
686
|
+
// previews. Execution delegates to the same factory-made built-ins pi would
|
|
687
|
+
// have used (settings-honoring), so ONLY presentation changes — which is why
|
|
688
|
+
// eval mode keeps pi's stock registration (measured behavior stays
|
|
689
|
+
// byte-identical) and the desktop surface keeps the structured pipeline.
|
|
690
|
+
// YAGNI_CLASSIC_TOOL_ROWS=1 is the debugging escape hatch back to pi's
|
|
691
|
+
// boxed renderers.
|
|
692
|
+
if (!evalMode && !isDesktopSurface() && env.YAGNI_CLASSIC_TOOL_ROWS !== "1") {
|
|
693
|
+
try {
|
|
694
|
+
registerCondensedTools(pi, {
|
|
695
|
+
...(scratchpadDirPath ? { scratchpadDir: scratchpadDirPath } : {}),
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
// Rendering must never break activation; pi's built-ins remain.
|
|
700
|
+
}
|
|
701
|
+
}
|
|
596
702
|
// Own the identity + inject live company context (and repo rules) on every
|
|
597
703
|
// turn. The extension loads identically in every pi process this app spawns —
|
|
598
704
|
// the interactive driver AND every `/go` stage child, subagent, and advisor
|
|
@@ -617,6 +723,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
617
723
|
: YAGNI_IDENTITY_DRIVER
|
|
618
724
|
: undefined,
|
|
619
725
|
rulesSection,
|
|
726
|
+
scratchpadSection: scratchpadSectionText,
|
|
620
727
|
}),
|
|
621
728
|
});
|
|
622
729
|
// Turn-lifecycle WAL: a `turn_start` with no matching `turn_end` is the
|
|
@@ -800,7 +907,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
800
907
|
// context % on line 2, and extension statuses (brand, todos, mode) on
|
|
801
908
|
// line 3. The factory captures ctx so the footer can read session data
|
|
802
909
|
// (token stats, context usage) that isn't on the footerData provider.
|
|
803
|
-
ctx.ui?.setFooter?.((tui, theme, footerData) => createYagniFooterFactory(ctx, modeHolder, footerInvalidateHandle)(tui, theme, footerData));
|
|
910
|
+
ctx.ui?.setFooter?.((tui, theme, footerData) => createYagniFooterFactory(ctx, modeHolder, footerInvalidateHandle, childUsage)(tui, theme, footerData));
|
|
804
911
|
ctx.ui?.onTerminalInput?.((data) => {
|
|
805
912
|
if (isShiftTab(data)) {
|
|
806
913
|
modeHolder.set(cyclePermissionMode(modeHolder.get()));
|
|
@@ -819,6 +926,14 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
819
926
|
catch {
|
|
820
927
|
// A label must never disrupt session start.
|
|
821
928
|
}
|
|
929
|
+
// The pulsing star indicator that pairs with the composed working line.
|
|
930
|
+
// Fails closed to pi's default spinner.
|
|
931
|
+
try {
|
|
932
|
+
ctx.ui?.setWorkingIndicator?.({ frames: [...WORKING_INDICATOR_FRAMES], intervalMs: 140 });
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
// An indicator must never disrupt session start.
|
|
936
|
+
}
|
|
822
937
|
// One-time hint naming what changed now that reasoning is collapsed by
|
|
823
938
|
// default (option 2). Gated on the launcher's YAGNI_HIDE_THINKING_SEEDED
|
|
824
939
|
// marker so it fires only on the launch that actually seeded the key — it
|
|
@@ -1015,5 +1130,10 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
|
|
|
1015
1130
|
// W4 trust plumbing: the durable write-spool for the judgment-capture tools
|
|
1016
1131
|
// (idempotencyKey-replayed; server-side dedup makes flush safe).
|
|
1017
1132
|
export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
|
|
1133
|
+
// The condensed Claude Code-style transcript: flat tool rows, run summaries,
|
|
1134
|
+
// and the composed streaming status line.
|
|
1135
|
+
export { registerCondensedTools, displayPath, isScratchpadPath, primaryArg, formatRowTitle, formatWriteBody, formatEditBody, formatBashErrorBody, formatBashPartialBody, formatExpandedOutput, splitBashError, countPatchAdditions, WRITE_PREVIEW_LINES, DIFF_PREVIEW_LINES, } from "./condensedTools.js";
|
|
1136
|
+
export { ToolRunTracker, summarizeRun, isQuiet, kindForTool } from "./toolRuns.js";
|
|
1137
|
+
export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
|
|
1018
1138
|
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
1019
1139
|
//# sourceMappingURL=index.js.map
|
|
@@ -727,12 +727,59 @@ function isPipeToShell(command) {
|
|
|
727
727
|
* backticks, including inside double quotes). Matches FORBIDDEN rules only —
|
|
728
728
|
* can upgrade the classification, never relax it.
|
|
729
729
|
*/
|
|
730
|
+
const QUOTED_HEREDOC_OPEN = /^cat[ \t]+<<[ \t]*(?:'([^']+)'|"([^"]+)"|\\([A-Za-z_][A-Za-z0-9_]*))[ \t]*\r?\n/;
|
|
731
|
+
/**
|
|
732
|
+
* Recognize the `cat <<'QUOTED' … QUOTED` idiom inside a command substitution.
|
|
733
|
+
*
|
|
734
|
+
* This is the one way to pass multi-line body content to `gh`/`git` as a
|
|
735
|
+
* literal string: the quoted delimiter (`<<'EOF'`, `<<"EOF"`, or `<<\EOF`)
|
|
736
|
+
* makes the heredoc body literal — no `$`/backtick expansion — and `cat` merely
|
|
737
|
+
* prints it. A body like `rm -rf /tmp/test-db` inside such a heredoc is PROSE,
|
|
738
|
+
* not a command; sweeping it for forbidden rules is a false positive.
|
|
739
|
+
*
|
|
740
|
+
* Returns true ONLY for the exact safe shape: the whole substitution is `cat`
|
|
741
|
+
* with no arguments, followed by a heredoc whose delimiter is quoted and whose
|
|
742
|
+
* terminator is the delimiter on its own final line. Any deviation (unquoted
|
|
743
|
+
* delimiter, extra args to `cat`, a second command after the terminator)
|
|
744
|
+
* returns false and falls through to the ordinary danger scan. Safety does not
|
|
745
|
+
* hinge on this exemption: the outer command (the `gh pr create`, the `rm`) is
|
|
746
|
+
* still classified on its own merits, heredoc or not.
|
|
747
|
+
*/
|
|
748
|
+
function isLiteralQuotedCatHeredoc(inner) {
|
|
749
|
+
const text = inner.trim();
|
|
750
|
+
const m = QUOTED_HEREDOC_OPEN.exec(text);
|
|
751
|
+
if (!m)
|
|
752
|
+
return false;
|
|
753
|
+
const delim = m[1] ?? m[2] ?? m[3];
|
|
754
|
+
const bodyStart = m[0].length;
|
|
755
|
+
const lines = text.slice(bodyStart).split(/\r?\n/);
|
|
756
|
+
if (lines.length === 0)
|
|
757
|
+
return false;
|
|
758
|
+
// The final line must be the bare delimiter (the closing `)` is already
|
|
759
|
+
// stripped by extractSubstitutions).
|
|
760
|
+
if (lines[lines.length - 1].trim() !== delim)
|
|
761
|
+
return false;
|
|
762
|
+
// No line BEFORE the terminator may equal the delimiter: in real bash, a
|
|
763
|
+
// delimiter line terminates the heredoc there, turning everything after it
|
|
764
|
+
// into an executed command (e.g. a body with a bare `EOF` line followed by
|
|
765
|
+
// `rm -rf /x` would run the `rm`). Reject that shape and fall to the scan.
|
|
766
|
+
for (const line of lines.slice(0, -1)) {
|
|
767
|
+
if (line.trim() === delim)
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
730
772
|
function dangerScanSubstitutions(command, policy, depth) {
|
|
731
773
|
if (depth > MAX_SCAN_DEPTH)
|
|
732
774
|
return null;
|
|
733
775
|
for (const inner of extractSubstitutions(command)) {
|
|
734
776
|
if (inner.trim().length === 0)
|
|
735
777
|
continue;
|
|
778
|
+
// Quoted-heredoc cat: the body is literal data, not an executable
|
|
779
|
+
// command — skipping it avoids false "forbidden" hits on prose that happens
|
|
780
|
+
// to contain a forbidden-looking token. The outer command still classifies.
|
|
781
|
+
if (isLiteralQuotedCatHeredoc(inner))
|
|
782
|
+
continue;
|
|
736
783
|
if (isPipeToShell(inner)) {
|
|
737
784
|
return {
|
|
738
785
|
decision: "forbidden",
|
|
@@ -151,6 +151,12 @@ export interface RegisterPermissionDeps {
|
|
|
151
151
|
guardianState?: import("./guardian.js").GuardianStateHandle;
|
|
152
152
|
/** Guardian limits (timeouts, circuit breaker). Defaults to DEFAULT_GUARDIAN_LIMITS. */
|
|
153
153
|
guardianLimits?: import("./guardian.js").GuardianLimits;
|
|
154
|
+
/**
|
|
155
|
+
* Session child-usage accumulator (childUsage.ts): each completed Guardian
|
|
156
|
+
* review's spend is recorded so the footer's session totals include it.
|
|
157
|
+
* Absent = not wired: no recording, nothing else changes.
|
|
158
|
+
*/
|
|
159
|
+
childUsage?: import("../childUsage.js").ChildUsageHandle;
|
|
154
160
|
/** Override the Guardian model tier (default: efficient). */
|
|
155
161
|
guardianTier?: string;
|
|
156
162
|
/** Injectable Guardian review function (tests pass a stub). */
|
|
@@ -366,6 +366,10 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
366
366
|
const ERROR_ASK_CAP = 3;
|
|
367
367
|
let errorFallbackAsks = 0;
|
|
368
368
|
let breakerEscalationOffered = false;
|
|
369
|
+
// Uniquifies each Guardian review's childUsage key: parallel gated tool
|
|
370
|
+
// calls can start their reviews in the same millisecond, and a timestamp
|
|
371
|
+
// alone would silently drop the second review's spend as a "duplicate".
|
|
372
|
+
let reviewSpendSeq = 0;
|
|
369
373
|
const emitGateEvent = (event) => {
|
|
370
374
|
if (!deps.onGuardianEvent)
|
|
371
375
|
return;
|
|
@@ -561,13 +565,18 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
561
565
|
ctx.ui.setStatus?.("yagni-guardian", undefined);
|
|
562
566
|
}
|
|
563
567
|
const durationMs = Date.now() - startMs;
|
|
564
|
-
|
|
568
|
+
// Footer session totals include the review's spend. Each review
|
|
569
|
+
// records exactly once, right here, so the key only has to be unique
|
|
570
|
+
// per review — the seq keeps same-millisecond parallel reviews apart.
|
|
571
|
+
deps.childUsage?.record("guardian", `${startMs}#${reviewSpendSeq++}`, { cost: reviewResult.cost });
|
|
572
|
+
const emitDiag = (outcome, rationale, rawOutput) => {
|
|
565
573
|
if (!deps.onGuardianReview)
|
|
566
574
|
return;
|
|
567
575
|
void Promise.resolve(deps.onGuardianReview(buildDiagnosticEvent(outcome, {
|
|
568
576
|
durationMs,
|
|
569
577
|
tier: guardianTier,
|
|
570
578
|
...(rationale ? { rationale } : {}),
|
|
579
|
+
...(rawOutput ? { rawOutput } : {}),
|
|
571
580
|
debug: isDebug(),
|
|
572
581
|
}))).catch(() => { });
|
|
573
582
|
};
|
|
@@ -734,7 +743,7 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
734
743
|
}
|
|
735
744
|
// Guardian failed (timeout/malformed/network/empty/aborted).
|
|
736
745
|
const error = reviewResult.error ?? "network";
|
|
737
|
-
emitDiag(error);
|
|
746
|
+
emitDiag(error, undefined, reviewResult.rawOutput);
|
|
738
747
|
if (error === "aborted" || ctx?.signal?.aborted) {
|
|
739
748
|
// The user aborted mid-consult — silent block: no dialog, no
|
|
740
749
|
// "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)
|
|
@@ -110,6 +110,14 @@ export interface RegisterGoDeps {
|
|
|
110
110
|
* cares injects its own fake.
|
|
111
111
|
*/
|
|
112
112
|
fetchRunSpend?: (runId: string, signal?: AbortSignal) => Promise<SpendResponse | null>;
|
|
113
|
+
/**
|
|
114
|
+
* Session child-usage accumulator (childUsage.ts). Each attempt's newly-run
|
|
115
|
+
* stage usage is recorded (keyed by runId + attempt start, because a resumed
|
|
116
|
+
* attempt's stages are new spend, never a replay) so the footer's session
|
|
117
|
+
* totals include /go spend — the same figure `runCostNote` already prints.
|
|
118
|
+
* Absent = not wired.
|
|
119
|
+
*/
|
|
120
|
+
childUsage?: import("../childUsage.js").ChildUsageHandle;
|
|
113
121
|
/** Injectable fs existence check (worktree adoption on resume). */
|
|
114
122
|
exists?: (path: string) => boolean;
|
|
115
123
|
/** Clock seam for registry rows + staleness. */
|
|
@@ -969,6 +969,14 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
969
969
|
if (result.stopReason === "clean" && runSignal.aborted) {
|
|
970
970
|
result = { ...result, stopReason: "aborted" };
|
|
971
971
|
}
|
|
972
|
+
// Footer session totals include this run's spend. Keyed per ATTEMPT
|
|
973
|
+
// (runId + this invocation's start), not per runId: `result.stages`
|
|
974
|
+
// only ever holds the stages THIS attempt actually ran (a resume's
|
|
975
|
+
// replayed spend lives in the checkpoint's priorUsage, never here),
|
|
976
|
+
// so an abort → same-session resume must record both attempts — a
|
|
977
|
+
// runId-only key would silently drop the resume's new spend. Partial
|
|
978
|
+
// spend on an aborted attempt is still real spend worth counting.
|
|
979
|
+
deps.childUsage?.record("go", `${runId}@${startedAt}`, aggregateRunUsage(result.stages, result.rounds));
|
|
972
980
|
// FINISH stage (spec §3c): ONLY on a clean stop. Commit the run's work
|
|
973
981
|
// (worktree always; --here only over a clean pre-run baseline) with the
|
|
974
982
|
// provenance trailer, and push + PR when --pr asked for it. Iron
|
|
@@ -65,9 +65,16 @@ export declare function buildStageInvocation(stage: PipelineStage, ctx: {
|
|
|
65
65
|
* `yagni` provider) before a stage passthrough. Mirrors buildLaunch's
|
|
66
66
|
* `userChoseProvider` guard so a passthrough that already chose a provider is
|
|
67
67
|
* left untouched.
|
|
68
|
+
*
|
|
69
|
+
* `otelExtensionPath` (absent on a non-exporting launch) additionally loads
|
|
70
|
+
* pi-otel so child LLM spend is traced too — stage children are the bulk of a
|
|
71
|
+
* /go run's cost, and an OTel export that misses them undercounts. The path
|
|
72
|
+
* arrives via `YAGNI_OTEL_EXTENSION_PATH` from the launcher's gate (see the
|
|
73
|
+
* CLI's otel.ts); the capture-mode and endpoint env rides the inherited env.
|
|
68
74
|
*/
|
|
69
75
|
export declare function groundedChildArgv(stageArgv: string[], opts: {
|
|
70
76
|
piCli: string;
|
|
71
77
|
extensionPath: string;
|
|
78
|
+
otelExtensionPath?: string;
|
|
72
79
|
}): string[];
|
|
73
80
|
//# sourceMappingURL=invocation.d.ts.map
|
|
@@ -84,6 +84,12 @@ export function buildStageInvocation(stage, ctx) {
|
|
|
84
84
|
* `yagni` provider) before a stage passthrough. Mirrors buildLaunch's
|
|
85
85
|
* `userChoseProvider` guard so a passthrough that already chose a provider is
|
|
86
86
|
* left untouched.
|
|
87
|
+
*
|
|
88
|
+
* `otelExtensionPath` (absent on a non-exporting launch) additionally loads
|
|
89
|
+
* pi-otel so child LLM spend is traced too — stage children are the bulk of a
|
|
90
|
+
* /go run's cost, and an OTel export that misses them undercounts. The path
|
|
91
|
+
* arrives via `YAGNI_OTEL_EXTENSION_PATH` from the launcher's gate (see the
|
|
92
|
+
* CLI's otel.ts); the capture-mode and endpoint env rides the inherited env.
|
|
87
93
|
*/
|
|
88
94
|
export function groundedChildArgv(stageArgv, opts) {
|
|
89
95
|
const userChoseProvider = stageArgv.includes("--provider");
|
|
@@ -91,6 +97,7 @@ export function groundedChildArgv(stageArgv, opts) {
|
|
|
91
97
|
opts.piCli,
|
|
92
98
|
"-e",
|
|
93
99
|
opts.extensionPath,
|
|
100
|
+
...(opts.otelExtensionPath ? ["-e", opts.otelExtensionPath] : []),
|
|
94
101
|
...(userChoseProvider ? [] : ["--provider", "yagni"]),
|
|
95
102
|
...stageArgv,
|
|
96
103
|
];
|