@yagni-app/code 0.3.5 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +7 -0
- package/dist/extension/askAdvisorTool.js +13 -3
- package/dist/extension/askUserQuestionTool.d.ts +54 -0
- package/dist/extension/askUserQuestionTool.js +621 -0
- package/dist/extension/askYagniTool.js +2 -0
- package/dist/extension/branding.d.ts +15 -0
- package/dist/extension/branding.js +76 -0
- package/dist/extension/chipEditor.d.ts +22 -1
- package/dist/extension/chipEditor.js +58 -5
- package/dist/extension/cmux/state.js +9 -16
- package/dist/extension/condensedTools.d.ts +93 -0
- package/dist/extension/condensedTools.js +392 -0
- package/dist/extension/crashReport.js +12 -0
- package/dist/extension/decisionCapture.js +3 -0
- package/dist/extension/decisions.js +4 -0
- package/dist/extension/diagnostics.d.ts +31 -0
- package/dist/extension/diagnostics.js +53 -55
- package/dist/extension/diffStat.d.ts +62 -0
- package/dist/extension/diffStat.js +158 -0
- package/dist/extension/errorSink.d.ts +64 -0
- package/dist/extension/errorSink.js +180 -0
- package/dist/extension/feedbackCommand.d.ts +38 -0
- package/dist/extension/feedbackCommand.js +151 -0
- package/dist/extension/footer.d.ts +2 -0
- package/dist/extension/footer.js +21 -8
- package/dist/extension/hooks.js +12 -12
- package/dist/extension/index.d.ts +7 -0
- package/dist/extension/index.js +161 -42
- package/dist/extension/mineBeat.js +13 -0
- package/dist/extension/permission/execPolicy.js +47 -0
- package/dist/extension/pipeline/goCommand.js +2 -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 +24 -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/silentTurnReminder.js +18 -14
- package/dist/extension/subagents.d.ts +10 -0
- package/dist/extension/subagents.js +18 -4
- 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/turnLog.js +17 -46
- package/dist/extension/webFetch.d.ts +85 -0
- package/dist/extension/webFetch.js +192 -0
- package/dist/extension/webFetchTool.d.ts +34 -0
- package/dist/extension/webFetchTool.js +106 -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 +4 -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,13 +230,18 @@ 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%
|
|
@@ -269,17 +275,23 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
269
275
|
export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
270
276
|
return (_tui, theme, footerData) => {
|
|
271
277
|
let gitCache;
|
|
272
|
-
|
|
278
|
+
let diffStatCache;
|
|
279
|
+
const clearCaches = () => {
|
|
273
280
|
gitCache = undefined;
|
|
274
|
-
|
|
281
|
+
diffStatCache = undefined;
|
|
282
|
+
};
|
|
283
|
+
const unsubscribeBranch = footerData.onBranchChange?.(clearCaches);
|
|
275
284
|
const gitInfo = () => {
|
|
276
285
|
if (!gitCache) {
|
|
277
|
-
|
|
286
|
+
// Read cwd once; both the git info and the diff cache key off it.
|
|
287
|
+
const dir = ctx.sessionManager.getCwd();
|
|
288
|
+
gitCache = detectGitInfo(dir, process.env.HOME || process.env.USERPROFILE);
|
|
289
|
+
diffStatCache = createDiffStatCache(dir);
|
|
278
290
|
}
|
|
279
291
|
return gitCache;
|
|
280
292
|
};
|
|
281
293
|
if (invalidateHandle) {
|
|
282
|
-
invalidateHandle.invalidateGit =
|
|
294
|
+
invalidateHandle.invalidateGit = clearCaches;
|
|
283
295
|
invalidateHandle.requestRender = () => { _tui?.requestRender?.(); };
|
|
284
296
|
}
|
|
285
297
|
return {
|
|
@@ -291,13 +303,14 @@ export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
|
291
303
|
git: gitInfo(),
|
|
292
304
|
model: ctx.model?.id ?? "no-model",
|
|
293
305
|
mode: modeHolder?.get() ?? null,
|
|
306
|
+
diff: formatDiffStat(diffStatCache?.get() ?? null),
|
|
294
307
|
usage: collectUsage(ctx.sessionManager),
|
|
295
308
|
contextPercent: ctx.getContextUsage()?.percent ?? null,
|
|
296
309
|
statuses,
|
|
297
310
|
}, theme, width, resolveFooterPadX());
|
|
298
311
|
},
|
|
299
312
|
invalidate() {
|
|
300
|
-
|
|
313
|
+
clearCaches();
|
|
301
314
|
},
|
|
302
315
|
dispose() {
|
|
303
316
|
unsubscribeBranch?.();
|
package/dist/extension/hooks.js
CHANGED
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
* Fail-soft: a broken hook degrades to "no hook," never to "broken session."
|
|
18
18
|
*/
|
|
19
19
|
import { spawn } from "node:child_process";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { codeStateHome } from "./stateHome.js";
|
|
24
24
|
import { isDebug } from "./diagnostics.js";
|
|
25
|
+
import { logEvent } from "./errorSink.js";
|
|
25
26
|
const SUPPORTED_EVENTS = [
|
|
26
27
|
"SessionStart",
|
|
27
28
|
"UserPromptSubmit",
|
|
@@ -286,16 +287,15 @@ export function parseCompactCancel(stdout) {
|
|
|
286
287
|
function logHookEvent(env, payload) {
|
|
287
288
|
if (!isDebug(env))
|
|
288
289
|
return;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
290
|
+
const event = typeof payload.event === "string" ? payload.event : "hook_event";
|
|
291
|
+
const { event: _ignored, ...fields } = payload;
|
|
292
|
+
logEvent({
|
|
293
|
+
source: "hooks",
|
|
294
|
+
level: "debug",
|
|
295
|
+
event,
|
|
296
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
297
|
+
fields,
|
|
298
|
+
});
|
|
299
299
|
}
|
|
300
300
|
/** Filter hook groups by workspace trust: project-level groups are skipped when untrusted. */
|
|
301
301
|
function filterByTrust(groups, isTrusted) {
|
|
@@ -120,6 +120,7 @@ export default function (pi: ExtensionAPI): Promise<void>;
|
|
|
120
120
|
export { makeAskYagniTool } from "./askYagniTool.js";
|
|
121
121
|
export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
122
122
|
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
123
|
+
export { makeAskUserQuestionTool } from "./askUserQuestionTool.js";
|
|
123
124
|
export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
|
|
124
125
|
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs, } from "./permission/guardian.js";
|
|
125
126
|
export type { GuardianOutcome, GuardianVerdict, GuardianState, GuardianStateHandle, GuardianLimits, ReviewResult, ReviewCommandDeps, } from "./permission/guardian.js";
|
|
@@ -185,6 +186,12 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
|
|
|
185
186
|
export type { TokenProvider, TokenProviderDeps, ScheduleFn } from "./tokenProvider.js";
|
|
186
187
|
export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
|
|
187
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";
|
|
188
195
|
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
189
196
|
export type { CrashReporter, CrashReporterOpts, FatalCrashOpts, SanitizedCrash } from "./crashReport.js";
|
|
190
197
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/extension/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
3
|
import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
|
|
6
4
|
import { appendGrant, loadGrants, resolveRepoKey, storagePrefix } from "./permission/approvedPrefixes.js";
|
|
7
5
|
import { redactCommand } from "./redact.js";
|
|
8
6
|
import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs } from "./permission/guardian.js";
|
|
9
7
|
import { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
8
|
+
import { makeAskUserQuestionTool } from "./askUserQuestionTool.js";
|
|
10
9
|
import { makeAskYagniTool } from "./askYagniTool.js";
|
|
10
|
+
import { makeWebFetchTool } from "./webFetchTool.js";
|
|
11
11
|
import { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
12
12
|
import { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
13
13
|
import { registerCmuxBridge } from "./cmux/index.js";
|
|
@@ -16,8 +16,11 @@ import { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
|
16
16
|
import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
|
|
17
17
|
import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA } from "./branding.js";
|
|
18
18
|
import { claudeRulesSection } from "./claudeRules.js";
|
|
19
|
+
import { ensureScratchpadDir, SCRATCHPAD_TMPDIR_ENV, scratchpadDir as scratchpadDirFor, scratchpadSection } from "./scratchpad.js";
|
|
19
20
|
import { registerCostCommand } from "./costHud.js";
|
|
20
21
|
import { isDebug } from "./diagnostics.js";
|
|
22
|
+
import { logEvent } from "./errorSink.js";
|
|
23
|
+
import { registerFeedbackCommands } from "./feedbackCommand.js";
|
|
21
24
|
import { droppedSessionRuns, sessionRunIds } from "./sessionRuns.js";
|
|
22
25
|
import { codeStateHome } from "./stateHome.js";
|
|
23
26
|
import { logTurnLifecycle } from "./turnLog.js";
|
|
@@ -46,6 +49,9 @@ import { registerChipEditor } from "./chipEditor.js";
|
|
|
46
49
|
import { defaultMineBeatGit, fileMineBeatMarkers, maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, } from "./mineBeat.js";
|
|
47
50
|
import { makeFlywheelState } from "./flywheel.js";
|
|
48
51
|
import { registerSilentTurnReminder } from "./silentTurnReminder.js";
|
|
52
|
+
import { registerCondensedTools } from "./condensedTools.js";
|
|
53
|
+
import { isDesktopSurface } from "./surface.js";
|
|
54
|
+
import { registerWorkingLine, WORKING_INDICATOR_FRAMES } from "./workingLine.js";
|
|
49
55
|
function isEvalMode(env = process.env) {
|
|
50
56
|
return env.YAGNI_CODE_EVAL_MODE === "1";
|
|
51
57
|
}
|
|
@@ -158,7 +164,15 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
158
164
|
// no-position record suggestions reach the model) and record_decision
|
|
159
165
|
// (flywheel-attributed records send dedupe: true). Run 7.
|
|
160
166
|
const flywheelState = makeFlywheelState();
|
|
167
|
+
// The composed streaming status line ("Shaping… (12m 54s · ↓ 47.5k tokens)").
|
|
168
|
+
// Registered before the subagent/advisor tools: they publish their live
|
|
169
|
+
// progress through this handle so the elapsed/token suffix survives their
|
|
170
|
+
// overrides. TUI-gated internally (agent_start checks ctx.mode).
|
|
171
|
+
const workingLine = registerWorkingLine(pi);
|
|
161
172
|
pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
|
|
173
|
+
// WebFetch (YAG-578): read an arbitrary URL as clean markdown + a
|
|
174
|
+
// standard-tier extraction, replacing the bash + curl + python dance.
|
|
175
|
+
pi.registerTool(makeWebFetchTool(toolOpts));
|
|
162
176
|
// Ticket write-back (spec 2026-08-09): explicit user-intent writes to the
|
|
163
177
|
// workspace tracker, attributed to the developer via per-user credentials.
|
|
164
178
|
if (!evalMode) {
|
|
@@ -172,11 +186,14 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
172
186
|
// hide it forever from a session that switched TO Advanced. One state handle
|
|
173
187
|
// per session, shared with /advise so they draw on the same cap.
|
|
174
188
|
const advisorState = makeAdvisorState();
|
|
175
|
-
const askAdvisorTool = makeAskAdvisorTool({ state: advisorState });
|
|
189
|
+
const askAdvisorTool = makeAskAdvisorTool({ state: advisorState, workingLine });
|
|
176
190
|
pi.registerTool(askAdvisorTool);
|
|
177
191
|
// /advise runs the SAME tool, sharing the state handle, so a manual consult
|
|
178
192
|
// draws on the same cap rather than opening a side channel around it.
|
|
179
193
|
registerAdviseCommand(pi, askAdvisorTool);
|
|
194
|
+
// Structured human questions: the model poses a closed 2-4-option question
|
|
195
|
+
// and gets a clean machine-readable answer via ctx.ui.custom.
|
|
196
|
+
pi.registerTool(makeAskUserQuestionTool());
|
|
180
197
|
// The differentiated business-grounded tools (loop bricks): review a change
|
|
181
198
|
// for business fit, rank the next work by business priority, and record the
|
|
182
199
|
// engineering rationale back onto the work-item.
|
|
@@ -209,7 +226,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
209
226
|
// driver's delegation identity for the diamond directive (see the
|
|
210
227
|
// before_agent_start handler below) and widens the tool's fan-out ceiling.
|
|
211
228
|
const ultraHolder = createUltraHolder();
|
|
212
|
-
registerSubagents(pi, { isUltra: () => ultraHolder.get() });
|
|
229
|
+
registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine });
|
|
213
230
|
registerUltraCommand(pi, ultraHolder);
|
|
214
231
|
// Shared fetch timeout for the small, interactive display-path reads below
|
|
215
232
|
// (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
|
|
@@ -244,6 +261,19 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
244
261
|
// M6 eval (report-only): /go-compare runs a ticket grounded vs blind and reports
|
|
245
262
|
// the business-fit delta. Never wired to routing.
|
|
246
263
|
registerGoCompareCommand(pi);
|
|
264
|
+
// YAG-580: /feedback (alias /bug) + /diagnostics. The capture/upload is the
|
|
265
|
+
// whole point, so it is gated to non-eval mode like every external side
|
|
266
|
+
// effect; the command's deps (doctor report, git state, child transcripts)
|
|
267
|
+
// are injected so the handler stays unit-testable without spawning git or
|
|
268
|
+
// reading the real session dir.
|
|
269
|
+
if (!evalMode) {
|
|
270
|
+
registerFeedbackCommands(pi, {
|
|
271
|
+
baseUrl,
|
|
272
|
+
getToken: getTokenFn,
|
|
273
|
+
fetchImpl: deps.fetchImpl,
|
|
274
|
+
env: deps.env,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
247
277
|
// W4 judgment loop: the decisions surface (/decide + /decisions) and the
|
|
248
278
|
// bless-with-remember capture are the same product-intent write as the record
|
|
249
279
|
// tools, so both are gated together (skipped in eval mode).
|
|
@@ -288,18 +318,19 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
288
318
|
...(guardianMaxAttemptsAdvertised !== undefined ? { maxAttempts: guardianMaxAttemptsAdvertised } : {}),
|
|
289
319
|
});
|
|
290
320
|
guardianLimits.timeoutMs = guardianTimeoutMs;
|
|
291
|
-
// YAG-510:
|
|
292
|
-
// never the command). The remote guardian-events stream below is
|
|
293
|
-
// separate, opt-in, per-workspace analytics sink; the two are independent.
|
|
321
|
+
// YAG-510: Guardian events go to the unified sink under source:"guardian"
|
|
322
|
+
// (hash-only, never the command). The remote guardian-events stream below is
|
|
323
|
+
// the separate, opt-in, per-workspace analytics sink; the two are independent.
|
|
294
324
|
const guardianLogSink = (payload) => {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
325
|
+
const event = typeof payload.event === "string" ? payload.event : "guardian_event";
|
|
326
|
+
const { event: _ignored, ...fields } = payload;
|
|
327
|
+
logEvent({
|
|
328
|
+
source: "guardian",
|
|
329
|
+
level: event === "guardian_event_post_failed" ? "error" : "info",
|
|
330
|
+
event,
|
|
331
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
332
|
+
fields,
|
|
333
|
+
});
|
|
303
334
|
};
|
|
304
335
|
// YAG-510: persisted "don't ask again" grants, per-repo keyed. Loaded once
|
|
305
336
|
// at startup (grants added by other concurrent sessions appear next launch —
|
|
@@ -331,7 +362,8 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
331
362
|
// "off" → nothing (not even sent); "hash" → sha256 + family prefix +
|
|
332
363
|
// metadata, no command content; "raw" → adds client-REDACTED command and
|
|
333
364
|
// rationale. Fire-and-forget: one attempt, short timeout, failures logged
|
|
334
|
-
// fail-soft to guardian
|
|
365
|
+
// fail-soft to the unified sink (source:"guardian") — a storage outage
|
|
366
|
+
// never touches the session.
|
|
335
367
|
onGuardianEvent: guardianStorageTier === "off" || evalMode
|
|
336
368
|
? undefined
|
|
337
369
|
: (ev) => {
|
|
@@ -369,10 +401,10 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
369
401
|
guardianLogSink({ event: "guardian_event_post_failed", status: res.status });
|
|
370
402
|
}
|
|
371
403
|
}
|
|
372
|
-
catch
|
|
404
|
+
catch {
|
|
373
405
|
guardianLogSink({
|
|
374
406
|
event: "guardian_event_post_failed",
|
|
375
|
-
|
|
407
|
+
kind: "network",
|
|
376
408
|
});
|
|
377
409
|
}
|
|
378
410
|
})();
|
|
@@ -454,15 +486,13 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
454
486
|
onDivergence: (driverServerUsd, localUsd) => {
|
|
455
487
|
if (!isDebug(env))
|
|
456
488
|
return;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
/* a diagnostic must never break /cost */
|
|
465
|
-
}
|
|
489
|
+
logEvent({
|
|
490
|
+
source: "cost",
|
|
491
|
+
level: "debug",
|
|
492
|
+
event: "cost_divergence",
|
|
493
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
494
|
+
fields: { driverServerUsd, localUsd },
|
|
495
|
+
});
|
|
466
496
|
},
|
|
467
497
|
// Carry-over (/cost re-review): surfaces sessionRuns.ts's dropped-run-id
|
|
468
498
|
// count as costHud's "Excludes N earlier /go runs." note.
|
|
@@ -572,6 +602,51 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
572
602
|
// pointers. Computed once per activation (rules are launch-time state, like
|
|
573
603
|
// pi's own skill discovery); fail-soft to null.
|
|
574
604
|
const rulesSection = claudeRulesSection(deps.env ?? process.env);
|
|
605
|
+
// YAG-575: session scratchpad — a permission-free dir the agent writes its
|
|
606
|
+
// working state to. Computed + ensured once at activation (like rulesSection,
|
|
607
|
+
// which must exist before the first before_agent_start), not on session_start,
|
|
608
|
+
// so the very first turn already carries the section. Fails closed: no
|
|
609
|
+
// sessionId (a bare pi run) or a failed mkdir means no section, and the mkdir
|
|
610
|
+
// failure is logged so a missing scratchpad is not silent.
|
|
611
|
+
const scratchpadDirPath = scratchpadDirFor({
|
|
612
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
613
|
+
cwd: process.cwd(),
|
|
614
|
+
tmp: env[SCRATCHPAD_TMPDIR_ENV] || undefined,
|
|
615
|
+
});
|
|
616
|
+
let scratchpadSectionText;
|
|
617
|
+
if (scratchpadDirPath) {
|
|
618
|
+
const ensured = ensureScratchpadDir(scratchpadDirPath);
|
|
619
|
+
if (ensured) {
|
|
620
|
+
scratchpadSectionText = scratchpadSection(ensured);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
logEvent({
|
|
624
|
+
source: "scratchpad",
|
|
625
|
+
level: "error",
|
|
626
|
+
event: "scratchpad_mkdir_failed",
|
|
627
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// The condensed Claude Code-style transcript: the seven built-ins re-render
|
|
632
|
+
// as flat "● Tool(arg)" rows, quiet runs collapse into one "Read 2 files,
|
|
633
|
+
// ran 2 shell commands" summary line, and project writes/edits keep visible
|
|
634
|
+
// previews. Execution delegates to the same factory-made built-ins pi would
|
|
635
|
+
// have used (settings-honoring), so ONLY presentation changes — which is why
|
|
636
|
+
// eval mode keeps pi's stock registration (measured behavior stays
|
|
637
|
+
// byte-identical) and the desktop surface keeps the structured pipeline.
|
|
638
|
+
// YAGNI_CLASSIC_TOOL_ROWS=1 is the debugging escape hatch back to pi's
|
|
639
|
+
// boxed renderers.
|
|
640
|
+
if (!evalMode && !isDesktopSurface() && env.YAGNI_CLASSIC_TOOL_ROWS !== "1") {
|
|
641
|
+
try {
|
|
642
|
+
registerCondensedTools(pi, {
|
|
643
|
+
...(scratchpadDirPath ? { scratchpadDir: scratchpadDirPath } : {}),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// Rendering must never break activation; pi's built-ins remain.
|
|
648
|
+
}
|
|
649
|
+
}
|
|
575
650
|
// Own the identity + inject live company context (and repo rules) on every
|
|
576
651
|
// turn. The extension loads identically in every pi process this app spawns —
|
|
577
652
|
// the interactive driver AND every `/go` stage child, subagent, and advisor
|
|
@@ -596,6 +671,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
596
671
|
: YAGNI_IDENTITY_DRIVER
|
|
597
672
|
: undefined,
|
|
598
673
|
rulesSection,
|
|
674
|
+
scratchpadSection: scratchpadSectionText,
|
|
599
675
|
}),
|
|
600
676
|
});
|
|
601
677
|
// Turn-lifecycle WAL: a `turn_start` with no matching `turn_end` is the
|
|
@@ -691,21 +767,16 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
691
767
|
// session token is expired and refresh failed — the most critical
|
|
692
768
|
// failure signal is no longer silently dropped.
|
|
693
769
|
void authReporter(new Error(`auth_401 on model path; refresh=${rotated ? "succeeded" : "failed"}`), "auth-failure").catch(() => { });
|
|
694
|
-
// YAG-500 Fix F:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
catch {
|
|
706
|
-
// A diagnostic must never break the session.
|
|
707
|
-
}
|
|
708
|
-
}
|
|
770
|
+
// YAG-500 Fix F: auth-401 signal is content-free (status + refresh
|
|
771
|
+
// boolean), so it is always-on and upload-safe by the default-on
|
|
772
|
+
// invariant. No message content, no tokens, no headers.
|
|
773
|
+
logEvent({
|
|
774
|
+
source: "auth",
|
|
775
|
+
level: "info",
|
|
776
|
+
event: "auth_401",
|
|
777
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
778
|
+
fields: { status: 401, refresh: rotated ? "succeeded" : "failed" },
|
|
779
|
+
});
|
|
709
780
|
return { message: { ...msg, errorMessage: explanation } };
|
|
710
781
|
}
|
|
711
782
|
// YAG-460: the backend proxy answers an oversized conversation with an
|
|
@@ -759,6 +830,16 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
759
830
|
// and a "Pi can explain its own features…" line) with a YAGNI Code masthead,
|
|
760
831
|
// set the terminal title, and add a footer brand mark. TUI only.
|
|
761
832
|
pi.on("session_start", async (event, ctx) => {
|
|
833
|
+
// Surface Pi's native find/grep/ls on the driver. These are Pi's
|
|
834
|
+
// equivalents of Claude Code's Glob/Grep/LS; the /go stages and subagents
|
|
835
|
+
// already pass them explicitly, but the driver defaults to read/bash/edit/write
|
|
836
|
+
// and otherwise reaches for `bash` find/grep/ls.
|
|
837
|
+
try {
|
|
838
|
+
pi.setActiveTools([...pi.getActiveTools(), "grep", "find", "ls"]);
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
// Tool enrichment must never break session start.
|
|
842
|
+
}
|
|
762
843
|
ctx.ui?.setTitle(BRAND_NAME);
|
|
763
844
|
if (ctx.mode === "tui") {
|
|
764
845
|
ctx.ui?.setStatus?.("brand", BRAND_NAME);
|
|
@@ -793,6 +874,14 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
793
874
|
catch {
|
|
794
875
|
// A label must never disrupt session start.
|
|
795
876
|
}
|
|
877
|
+
// The pulsing star indicator that pairs with the composed working line.
|
|
878
|
+
// Fails closed to pi's default spinner.
|
|
879
|
+
try {
|
|
880
|
+
ctx.ui?.setWorkingIndicator?.({ frames: [...WORKING_INDICATOR_FRAMES], intervalMs: 140 });
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
// An indicator must never disrupt session start.
|
|
884
|
+
}
|
|
796
885
|
// One-time hint naming what changed now that reasoning is collapsed by
|
|
797
886
|
// default (option 2). Gated on the launcher's YAGNI_HIDE_THINKING_SEEDED
|
|
798
887
|
// marker so it fires only on the launch that actually seeded the key — it
|
|
@@ -889,6 +978,30 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
889
978
|
footerInvalidateHandle.invalidateGit();
|
|
890
979
|
}
|
|
891
980
|
});
|
|
981
|
+
// Seed the unified error trail from tool-exec failures. A tool's SUCCESS
|
|
982
|
+
// is content (it lives in the transcript); its FAILURE is an error and belongs
|
|
983
|
+
// in the sink. We log only the tool name + error class — never args, partial
|
|
984
|
+
// results, or result bodies (those are content and stay out of the upload-safe
|
|
985
|
+
// tier). This is the tool-failure half of the error trail the ticket asks for.
|
|
986
|
+
pi.on("tool_execution_end", (event) => {
|
|
987
|
+
if (!event.isError)
|
|
988
|
+
return;
|
|
989
|
+
// `event.result` is any; its `.error`/message can echo a path or secret
|
|
990
|
+
// (a tool's own failure text). Keep the always-on trail content-free: use
|
|
991
|
+
// ONLY the Error subclass name, never the message or a String() of the
|
|
992
|
+
// result's error payload.
|
|
993
|
+
const errorClass = event.result instanceof Error
|
|
994
|
+
? event.result.name || "Error"
|
|
995
|
+
: "tool_error";
|
|
996
|
+
logEvent({
|
|
997
|
+
source: "tool",
|
|
998
|
+
level: "error",
|
|
999
|
+
event: "tool_failed",
|
|
1000
|
+
sessionId: sessionIdForLog(),
|
|
1001
|
+
flush: "sync",
|
|
1002
|
+
fields: { toolName: event.toolName, errorClass },
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
892
1005
|
}
|
|
893
1006
|
export default async function (pi) {
|
|
894
1007
|
await registerYagni(pi);
|
|
@@ -902,6 +1015,7 @@ export default async function (pi) {
|
|
|
902
1015
|
export { makeAskYagniTool } from "./askYagniTool.js";
|
|
903
1016
|
export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
904
1017
|
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
1018
|
+
export { makeAskUserQuestionTool } from "./askUserQuestionTool.js";
|
|
905
1019
|
export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
|
|
906
1020
|
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs, } from "./permission/guardian.js";
|
|
907
1021
|
export { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
@@ -964,5 +1078,10 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
|
|
|
964
1078
|
// W4 trust plumbing: the durable write-spool for the judgment-capture tools
|
|
965
1079
|
// (idempotencyKey-replayed; server-side dedup makes flush safe).
|
|
966
1080
|
export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
|
|
1081
|
+
// The condensed Claude Code-style transcript: flat tool rows, run summaries,
|
|
1082
|
+
// and the composed streaming status line.
|
|
1083
|
+
export { registerCondensedTools, displayPath, isScratchpadPath, primaryArg, formatRowTitle, formatWriteBody, formatEditBody, formatBashErrorBody, formatBashPartialBody, formatExpandedOutput, splitBashError, countPatchAdditions, WRITE_PREVIEW_LINES, DIFF_PREVIEW_LINES, } from "./condensedTools.js";
|
|
1084
|
+
export { ToolRunTracker, summarizeRun, isQuiet, kindForTool } from "./toolRuns.js";
|
|
1085
|
+
export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
|
|
967
1086
|
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
968
1087
|
//# sourceMappingURL=index.js.map
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { execFileSync } from "node:child_process";
|
|
21
21
|
import * as fs from "node:fs";
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
|
+
import { logEvent } from "./errorSink.js";
|
|
23
24
|
import { METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
24
25
|
/**
|
|
25
26
|
* Client-side bounds on the mining corpus — mirror of the backend's
|
|
@@ -173,6 +174,12 @@ export async function maybeOfferMiningBeat(ctx, opts) {
|
|
|
173
174
|
}, { fetchImpl: opts.fetchImpl, policy: METERED_POST_FETCH_POLICY });
|
|
174
175
|
if (!res.ok) {
|
|
175
176
|
// No marker: the offer stays available next session.
|
|
177
|
+
logEvent({
|
|
178
|
+
source: "mine-beat",
|
|
179
|
+
level: "error",
|
|
180
|
+
event: "mine_failed",
|
|
181
|
+
fields: { status: res.status },
|
|
182
|
+
});
|
|
176
183
|
ctx.ui.notify("Seeding the decision ledger failed; YAGNI Code will offer again next session.", "error");
|
|
177
184
|
return { offered: true, accepted: true };
|
|
178
185
|
}
|
|
@@ -186,6 +193,12 @@ export async function maybeOfferMiningBeat(ctx, opts) {
|
|
|
186
193
|
return { offered: true, accepted: true, banked };
|
|
187
194
|
}
|
|
188
195
|
catch {
|
|
196
|
+
logEvent({
|
|
197
|
+
source: "mine-beat",
|
|
198
|
+
level: "error",
|
|
199
|
+
event: "mine_failed",
|
|
200
|
+
fields: { kind: "network" },
|
|
201
|
+
});
|
|
189
202
|
ctx.ui.notify("Seeding the decision ledger failed; YAGNI Code will offer again next session.", "error");
|
|
190
203
|
return { offered: true, accepted: true };
|
|
191
204
|
}
|
|
@@ -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",
|
|
@@ -71,6 +71,7 @@ import { formatRunCostTable } from "./runCostTable.js";
|
|
|
71
71
|
import { makeCombinedCheckpointStore, makeFileCheckpointStore, makePiJournalCheckpointStore, } from "./checkpoint.js";
|
|
72
72
|
import { getToken as defaultGetToken, resolveBaseUrl } from "../config.js";
|
|
73
73
|
import { makeCrashReporter } from "../crashReport.js";
|
|
74
|
+
import { logEvent } from "../errorSink.js";
|
|
74
75
|
import { scrubSecrets } from "./scrubSecrets.js";
|
|
75
76
|
import { isDesktopSurface } from "../surface.js";
|
|
76
77
|
import { runFinish as defaultRunFinish, verifyTrailerValue, } from "./finish.js";
|
|
@@ -1104,6 +1105,7 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
1104
1105
|
// return results instead) — report it, fire-and-forget. The default
|
|
1105
1106
|
// reporter never rejects; the catch guards an injected one.
|
|
1106
1107
|
void reportCrash(err, "go", runCwd).catch(() => { });
|
|
1108
|
+
logEvent({ source: "go", level: "error", event: "go_failed", fields: { runId } });
|
|
1107
1109
|
// The stopReason travels to the backend run record; scrub it like
|
|
1108
1110
|
// every other captured text (a raw error can echo a connection
|
|
1109
1111
|
// string or key).
|
|
@@ -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
|
];
|