@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.
Files changed (75) hide show
  1. package/README.md +42 -0
  2. package/dist/cli.js +231 -6
  3. package/dist/crashReport.d.ts +8 -0
  4. package/dist/crashReport.js +13 -1
  5. package/dist/doctor.d.ts +7 -0
  6. package/dist/doctor.js +33 -0
  7. package/dist/extension/askAdvisorTool.d.ts +7 -0
  8. package/dist/extension/askAdvisorTool.js +13 -3
  9. package/dist/extension/askUserQuestionTool.d.ts +54 -0
  10. package/dist/extension/askUserQuestionTool.js +621 -0
  11. package/dist/extension/askYagniTool.js +2 -0
  12. package/dist/extension/branding.d.ts +15 -0
  13. package/dist/extension/branding.js +76 -0
  14. package/dist/extension/chipEditor.d.ts +22 -1
  15. package/dist/extension/chipEditor.js +58 -5
  16. package/dist/extension/cmux/state.js +9 -16
  17. package/dist/extension/condensedTools.d.ts +93 -0
  18. package/dist/extension/condensedTools.js +392 -0
  19. package/dist/extension/crashReport.js +12 -0
  20. package/dist/extension/decisionCapture.js +3 -0
  21. package/dist/extension/decisions.js +4 -0
  22. package/dist/extension/diagnostics.d.ts +31 -0
  23. package/dist/extension/diagnostics.js +53 -55
  24. package/dist/extension/diffStat.d.ts +62 -0
  25. package/dist/extension/diffStat.js +158 -0
  26. package/dist/extension/errorSink.d.ts +64 -0
  27. package/dist/extension/errorSink.js +180 -0
  28. package/dist/extension/feedbackCommand.d.ts +38 -0
  29. package/dist/extension/feedbackCommand.js +151 -0
  30. package/dist/extension/footer.d.ts +2 -0
  31. package/dist/extension/footer.js +21 -8
  32. package/dist/extension/hooks.js +12 -12
  33. package/dist/extension/index.d.ts +7 -0
  34. package/dist/extension/index.js +161 -42
  35. package/dist/extension/mineBeat.js +13 -0
  36. package/dist/extension/permission/execPolicy.js +47 -0
  37. package/dist/extension/pipeline/goCommand.js +2 -0
  38. package/dist/extension/pipeline/invocation.d.ts +7 -0
  39. package/dist/extension/pipeline/invocation.js +7 -0
  40. package/dist/extension/pipeline/personas.js +4 -4
  41. package/dist/extension/pipeline/runner.d.ts +1 -0
  42. package/dist/extension/pipeline/runner.js +24 -3
  43. package/dist/extension/pipeline/sessionWorktree.d.ts +64 -0
  44. package/dist/extension/pipeline/sessionWorktree.js +225 -0
  45. package/dist/extension/scratchpad.d.ts +66 -0
  46. package/dist/extension/scratchpad.js +93 -0
  47. package/dist/extension/silentTurnReminder.js +18 -14
  48. package/dist/extension/subagents.d.ts +10 -0
  49. package/dist/extension/subagents.js +18 -4
  50. package/dist/extension/todos.d.ts +1 -0
  51. package/dist/extension/todos.js +15 -0
  52. package/dist/extension/toolRuns.d.ts +92 -0
  53. package/dist/extension/toolRuns.js +201 -0
  54. package/dist/extension/turnLog.js +17 -46
  55. package/dist/extension/webFetch.d.ts +85 -0
  56. package/dist/extension/webFetch.js +192 -0
  57. package/dist/extension/webFetchTool.d.ts +34 -0
  58. package/dist/extension/webFetchTool.js +106 -0
  59. package/dist/extension/workingLine.d.ts +49 -0
  60. package/dist/extension/workingLine.js +116 -0
  61. package/dist/feedback.d.ts +77 -0
  62. package/dist/feedback.js +500 -0
  63. package/dist/goHeadless.d.ts +3 -0
  64. package/dist/goHeadless.js +13 -0
  65. package/dist/launch.d.ts +8 -0
  66. package/dist/launch.js +6 -0
  67. package/dist/otel.d.ts +150 -0
  68. package/dist/otel.js +291 -0
  69. package/dist/outputFormat.d.ts +83 -0
  70. package/dist/outputFormat.js +207 -0
  71. package/dist/paths.d.ts +10 -0
  72. package/dist/paths.js +13 -0
  73. package/dist/worktreeArgs.d.ts +43 -0
  74. package/dist/worktreeArgs.js +96 -0
  75. package/package.json +4 -2
@@ -0,0 +1,500 @@
1
+ /**
2
+ * `yagni feedback [sessionId]` — file a bug report from the shell (YAG-592).
3
+ *
4
+ * A shortcut to trigger what `/feedback` does inside a session, but from
5
+ * outside the TUI. Two modes:
6
+ *
7
+ * Case A (no session arg): list the 10 most recent sessions for the current
8
+ * cwd, let the user pick, prompt for a description, confirm, submit.
9
+ * Case B (session ID provided): skip the list, go straight to description
10
+ * prompt → confirm → submit.
11
+ *
12
+ * Self-contained: no cross-package imports. The scrub (`scrubSecrets`) and the
13
+ * error-trail reader (`readSessionTrail`) are local copies of the extension's
14
+ * logic — keep in sync with `pi-extension-yagni/src/pipeline/scrubSecrets.ts`
15
+ * and `pi-extension-yagni/src/errorSink.ts`. The backend re-scrubs server-side
16
+ * (`backend/src/yagniCode/feedback.ts`), so the client-side scrub is the first
17
+ * line of defense, not the only one.
18
+ */
19
+ import { readFileSync, readdirSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { createInterface } from "node:readline/promises";
22
+ import { stdin as input, stdout as output } from "node:process";
23
+ import { isatty } from "node:tty";
24
+ import { agentDir, credentialsDir } from "./credentials.js";
25
+ import { scrubSecrets } from "./crashReport.js";
26
+ import { credentialsFromProfile, readActiveProfile } from "./profiles.js";
27
+ const MAX_DESCRIPTION = 512;
28
+ const MAX_TRANSCRIPT_READ_BYTES = 512 * 1024;
29
+ const MAX_TRAIL_BYTES = 64 * 1024;
30
+ const FIRST_MESSAGE_PREVIEW_FALLBACK = 60;
31
+ /** Max chars for the first-message preview, capped by terminal width. */
32
+ function previewMaxWidth(termCols) {
33
+ const cols = termCols ?? process.stdout.columns;
34
+ if (!cols || cols < 40)
35
+ return FIRST_MESSAGE_PREVIEW_FALLBACK;
36
+ // Account for: " " + " N. " (5) + time (18) + dur (6) = ~29 chars of prefix
37
+ const available = cols - 32;
38
+ return Math.max(available, 20);
39
+ }
40
+ const LIST_LIMIT = 10;
41
+ // --- session discovery ---
42
+ /**
43
+ * Encode a cwd into pi's session directory name format:
44
+ * `/Users/foo/bar` → `--Users-foo-bar--`
45
+ * Mirrors pi's `migrations.js`: `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`
46
+ */
47
+ export function encodeCwd(cwd) {
48
+ return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
49
+ }
50
+ /**
51
+ * Parse the first few lines of a session JSONL to extract metadata.
52
+ * Returns null on any failure (corrupt/empty file).
53
+ */
54
+ function parseSessionHeader(filePath) {
55
+ try {
56
+ const data = readFileSync(filePath, "utf8");
57
+ const lines = data.split("\n").filter((l) => l.length > 0);
58
+ for (const line of lines) {
59
+ try {
60
+ const obj = JSON.parse(line);
61
+ if (obj.type === "session" && typeof obj.id === "string" && typeof obj.timestamp === "string") {
62
+ return { id: obj.id, timestamp: obj.timestamp };
63
+ }
64
+ }
65
+ catch {
66
+ // skip unparseable lines
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ /**
76
+ * Extract the first user message text from a session JSONL (skips
77
+ * `custom_message` entries — only real `type: "message"` with `role: "user"`).
78
+ */
79
+ function extractFirstUserMessage(filePath, maxChars) {
80
+ try {
81
+ const data = readFileSync(filePath, "utf8");
82
+ const lines = data.split("\n").filter((l) => l.length > 0);
83
+ for (const line of lines) {
84
+ try {
85
+ const obj = JSON.parse(line);
86
+ if (obj.type === "message" &&
87
+ obj.message?.role === "user" &&
88
+ Array.isArray(obj.message?.content)) {
89
+ const textPart = obj.message.content.find((c) => c.type === "text");
90
+ if (textPart?.text) {
91
+ // Strip skill invocation XML tags (e.g. `<skill name="…" …>`)
92
+ // — they're machinery, not the user's message.
93
+ let text = textPart.text.trim()
94
+ .replace(/<skill\s[^>]*>\s*/g, "")
95
+ .replace(/<\/skill>/g, "")
96
+ .replace(/\n+/g, " ")
97
+ .trim();
98
+ const cap = maxChars ?? FIRST_MESSAGE_PREVIEW_FALLBACK;
99
+ if (text.length > 0) {
100
+ return text.length > cap
101
+ ? `${text.slice(0, cap)}…`
102
+ : text;
103
+ }
104
+ }
105
+ }
106
+ }
107
+ catch {
108
+ // skip unparseable lines
109
+ }
110
+ }
111
+ return "";
112
+ }
113
+ catch {
114
+ return "";
115
+ }
116
+ }
117
+ /**
118
+ * Get the timestamp of the last non-empty line in a session JSONL.
119
+ * Used to compute session duration.
120
+ */
121
+ function extractLastTimestamp(filePath) {
122
+ try {
123
+ const data = readFileSync(filePath, "utf8");
124
+ const lines = data.split("\n").filter((l) => l.length > 0);
125
+ for (let i = lines.length - 1; i >= 0; i--) {
126
+ try {
127
+ const obj = JSON.parse(lines[i]);
128
+ if (typeof obj.timestamp === "string")
129
+ return obj.timestamp;
130
+ }
131
+ catch {
132
+ // skip
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
141
+ /**
142
+ * List the most recent sessions for the current cwd.
143
+ * Scans `<agentDir>/sessions/<encoded-cwd>/*.jsonl`, sorted by start time desc.
144
+ */
145
+ export function listRecentSessions(agentDirPath, cwd, limit = LIST_LIMIT, termCols) {
146
+ const sessionsDir = join(agentDirPath, "sessions", encodeCwd(cwd));
147
+ let files;
148
+ try {
149
+ files = readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
150
+ }
151
+ catch {
152
+ return [];
153
+ }
154
+ const sessions = [];
155
+ for (const file of files) {
156
+ const filePath = join(sessionsDir, file);
157
+ const header = parseSessionHeader(filePath);
158
+ if (!header)
159
+ continue;
160
+ const firstMessage = extractFirstUserMessage(filePath, previewMaxWidth(termCols));
161
+ const lastTs = extractLastTimestamp(filePath);
162
+ let durationMs = null;
163
+ if (lastTs) {
164
+ const start = Date.parse(header.timestamp);
165
+ const end = Date.parse(lastTs);
166
+ if (!isNaN(start) && !isNaN(end))
167
+ durationMs = end - start;
168
+ }
169
+ sessions.push({
170
+ id: header.id,
171
+ filePath,
172
+ startTime: header.timestamp,
173
+ durationMs,
174
+ firstMessage,
175
+ });
176
+ }
177
+ sessions.sort((a, b) => b.startTime.localeCompare(a.startTime));
178
+ return sessions.slice(0, limit);
179
+ }
180
+ /**
181
+ * Find a session file by UUID across all project dirs.
182
+ */
183
+ export function findSessionById(agentDirPath, sessionId) {
184
+ const sessionsRoot = join(agentDirPath, "sessions");
185
+ let projectDirs;
186
+ try {
187
+ projectDirs = readdirSync(sessionsRoot, { withFileTypes: true })
188
+ .filter((d) => d.isDirectory())
189
+ .map((d) => join(sessionsRoot, d.name));
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ for (const dir of projectDirs) {
195
+ let files;
196
+ try {
197
+ files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
198
+ }
199
+ catch {
200
+ continue;
201
+ }
202
+ for (const file of files) {
203
+ if (file.includes(sessionId)) {
204
+ const filePath = join(dir, file);
205
+ const header = parseSessionHeader(filePath);
206
+ if (header)
207
+ return { filePath, startTime: header.timestamp };
208
+ }
209
+ }
210
+ }
211
+ return null;
212
+ }
213
+ // --- transcript + error trail ---
214
+ /**
215
+ * Read the durable transcript, clamped by byte size. Returns empty on any
216
+ * failure or when too large. Mirrors the extension's `readTranscript`.
217
+ */
218
+ export function readTranscript(sessionFile) {
219
+ if (!sessionFile)
220
+ return "";
221
+ try {
222
+ const data = readFileSync(sessionFile, "utf8");
223
+ if (Buffer.byteLength(data, "utf8") > MAX_TRANSCRIPT_READ_BYTES)
224
+ return "";
225
+ return data;
226
+ }
227
+ catch {
228
+ return "";
229
+ }
230
+ }
231
+ /**
232
+ * Read the session-scoped error trail from today's error sink file.
233
+ * Filters by sessionId and excludes debug-level entries. Scrubs each line.
234
+ * Mirrors the extension's `readSessionTrail` — keep in sync.
235
+ */
236
+ export function readSessionTrail(sessionId, stateDir, maxBytes = MAX_TRAIL_BYTES) {
237
+ try {
238
+ const dayStamp = new Date().toISOString().slice(0, 10);
239
+ const sinkPath = join(stateDir, "logs", `errors-${dayStamp}.jsonl`);
240
+ const data = readFileSync(sinkPath, "utf8");
241
+ const lines = data
242
+ .split("\n")
243
+ .filter((l) => l.length > 0)
244
+ .filter((l) => {
245
+ try {
246
+ const obj = JSON.parse(l);
247
+ return obj.sessionId === sessionId && obj.level !== "debug";
248
+ }
249
+ catch {
250
+ return false;
251
+ }
252
+ })
253
+ .map((l) => scrubSecrets(l))
254
+ .join("\n");
255
+ return lines.length > maxBytes ? lines.slice(lines.length - maxBytes) : lines;
256
+ }
257
+ catch {
258
+ return "";
259
+ }
260
+ }
261
+ function buildFeedbackPayload(opts) {
262
+ const transcript = readTranscript(opts.sessionFile);
263
+ const trail = readSessionTrail(opts.sessionId, opts.stateDir);
264
+ return {
265
+ client: "cli",
266
+ clientVersion: opts.env.YAGNI_CODE_VERSION?.trim() || opts.clientVersion || "unknown",
267
+ platform: `${process.platform} ${process.arch}`,
268
+ description: scrubSecrets(opts.description).slice(0, MAX_DESCRIPTION),
269
+ sessionId: opts.sessionId,
270
+ ...(transcript ? { transcriptJsonl: scrubSecrets(transcript) } : {}),
271
+ ...(trail ? { errorTrailJsonl: trail } : {}),
272
+ };
273
+ }
274
+ async function submitFeedback(payload, baseUrl, token, fetchImpl) {
275
+ try {
276
+ const res = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/api/yagni-code/feedback`, {
277
+ method: "POST",
278
+ headers: {
279
+ "content-type": "application/json",
280
+ authorization: `Bearer ${token}`,
281
+ },
282
+ body: JSON.stringify(payload),
283
+ signal: AbortSignal.timeout(30_000),
284
+ });
285
+ if (res.ok) {
286
+ const body = (await res.json().catch(() => ({})));
287
+ return { ok: true, id: body.id };
288
+ }
289
+ return { ok: false, error: `Server returned ${res.status}` };
290
+ }
291
+ catch (err) {
292
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
293
+ }
294
+ }
295
+ /**
296
+ * Create a readline-like interface that survives piped stdin (where the
297
+ * real readline's `question` hangs after EOF). For TTY stdin it delegates to
298
+ * the real `readline/promises`; for piped stdin it reads all lines upfront
299
+ * and serves them one-by-one.
300
+ */
301
+ function makeReadline() {
302
+ if (isatty(0)) {
303
+ const rl = createInterface({ input, output });
304
+ return {
305
+ question: (q) => rl.question(q),
306
+ close: () => rl.close(),
307
+ };
308
+ }
309
+ // Piped stdin: read all available data and serve lines FIFO.
310
+ const lines = [];
311
+ try {
312
+ const data = readFileSync(0, "utf8"); // fd 0 = stdin
313
+ for (const line of data.split("\n")) {
314
+ if (line.length > 0)
315
+ lines.push(line);
316
+ }
317
+ }
318
+ catch {
319
+ // stdin not readable as a file — fall back to empty
320
+ }
321
+ let idx = 0;
322
+ return {
323
+ question: async (q) => {
324
+ process.stdout.write(q);
325
+ const answer = lines[idx++] ?? "";
326
+ // Echo a newline so subsequent prompts start on a new line (in TTY
327
+ // mode, readline echoes the user's Enter; piped mode does not).
328
+ process.stdout.write("\n");
329
+ return answer;
330
+ },
331
+ close: () => { },
332
+ };
333
+ }
334
+ // --- display ---
335
+ function formatDuration(ms) {
336
+ if (ms === null)
337
+ return "";
338
+ if (ms < 1000)
339
+ return "<1s";
340
+ const s = Math.floor(ms / 1000);
341
+ if (s < 60)
342
+ return `${s}s`;
343
+ const m = Math.floor(s / 60);
344
+ return `${m}m`;
345
+ }
346
+ function formatStartTime(iso) {
347
+ try {
348
+ const d = new Date(iso);
349
+ return d.toLocaleString("en-US", {
350
+ month: "short",
351
+ day: "2-digit",
352
+ hour: "2-digit",
353
+ minute: "2-digit",
354
+ hour12: false,
355
+ });
356
+ }
357
+ catch {
358
+ return iso;
359
+ }
360
+ }
361
+ // Minimal ANSI escape sequences for the session list.
362
+ const RESET = "\x1b[0m";
363
+ const BOLD = "\x1b[1m";
364
+ const DIM = "\x1b[2m";
365
+ const GREEN = "\x1b[32m";
366
+ const CYAN = "\x1b[36m";
367
+ function renderSessionList(sessions) {
368
+ const sep = DIM + " " + "─".repeat(70) + RESET;
369
+ const lines = [BOLD + " Recent sessions" + RESET, sep];
370
+ const countWidth = String(sessions.length).length;
371
+ for (let i = 0; i < sessions.length; i++) {
372
+ const s = sessions[i];
373
+ const num = ` ${i + 1}.`.padEnd(countWidth + 2);
374
+ const idx = GREEN + num + RESET + " ";
375
+ const time = DIM + formatStartTime(s.startTime).padEnd(17) + RESET + " ";
376
+ const dur = DIM + formatDuration(s.durationMs).padEnd(5) + RESET + " ";
377
+ const msg = s.firstMessage || "(no text)";
378
+ lines.push(` ${idx}${time}${dur}${msg}`);
379
+ }
380
+ lines.push("");
381
+ return lines.join("\n");
382
+ }
383
+ // --- orchestration ---
384
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
385
+ export async function feedbackCommand(args, deps = {}, cliVersion) {
386
+ const writeOut = deps.writeOut ?? ((line) => void process.stdout.write(`${line}\n`));
387
+ const writeErr = deps.writeErr ?? ((line) => void process.stderr.write(`${line}\n`));
388
+ const cwd = deps.cwd ?? process.cwd();
389
+ const env = deps.env ?? process.env;
390
+ // Load credentials (same pattern as goCommand).
391
+ const profile = await (deps.loadCredentials ?? (async () => {
392
+ const active = await readActiveProfile(env);
393
+ const creds = credentialsFromProfile(active);
394
+ return {
395
+ name: active.name,
396
+ baseUrl: active.baseUrl,
397
+ ...(creds?.token ? { token: creds.token } : {}),
398
+ };
399
+ }))();
400
+ if (!profile.token) {
401
+ writeErr(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`yagni login\` first.`);
402
+ return 1;
403
+ }
404
+ const agentDirPath = deps.agentDirPath ?? agentDir(profile.name);
405
+ const stateDir = deps.stateDir ?? credentialsDir();
406
+ // Parse args: is the first positional a session UUID?
407
+ const sessionIdArg = args.find((a) => !a.startsWith("-") && UUID_RE.test(a));
408
+ let sessionFile;
409
+ let sessionId;
410
+ // One readline interface for all prompts. For piped stdin, makeReadline
411
+ // reads all lines upfront to avoid the readline/promises hang on EOF.
412
+ const rl = deps.readline ?? makeReadline();
413
+ const closeRl = () => {
414
+ if (deps.readline)
415
+ deps.readline.close();
416
+ else
417
+ rl.close();
418
+ };
419
+ try {
420
+ if (sessionIdArg) {
421
+ // Case B: session ID provided directly.
422
+ const found = findSessionById(agentDirPath, sessionIdArg);
423
+ if (!found) {
424
+ writeErr(`Session ${sessionIdArg} not found.`);
425
+ return 1;
426
+ }
427
+ sessionFile = found.filePath;
428
+ sessionId = sessionIdArg;
429
+ }
430
+ else {
431
+ // Case A: list sessions for the current cwd.
432
+ const sessions = listRecentSessions(agentDirPath, cwd);
433
+ if (sessions.length === 0) {
434
+ writeOut("No recent sessions found for this directory.");
435
+ return 1;
436
+ }
437
+ writeOut(renderSessionList(sessions));
438
+ const answer = (await rl.question(CYAN + "Select a session (1-" + sessions.length + "): " + RESET)).trim();
439
+ const idx = parseInt(answer, 10) - 1;
440
+ if (isNaN(idx) || idx < 0 || idx >= sessions.length) {
441
+ writeOut("Feedback cancelled.");
442
+ return 0;
443
+ }
444
+ sessionFile = sessions[idx].filePath;
445
+ sessionId = sessions[idx].id;
446
+ }
447
+ // Prompt for description.
448
+ const descAnswer = await rl.question(DIM + "Describe the issue (one or two lines):" + RESET + "\n> ");
449
+ const description = descAnswer.trim();
450
+ if (!description) {
451
+ writeOut("Feedback cancelled.");
452
+ return 0;
453
+ }
454
+ // Confirm.
455
+ const transcriptBytes = sessionFile
456
+ ? Buffer.byteLength(readTranscript(sessionFile), "utf8")
457
+ : 0;
458
+ const confirmLines = [
459
+ DIM + "Sending:" + RESET,
460
+ ` - Your feedback description`,
461
+ ` - This session's transcript${transcriptBytes > 0 ? "" : " (could not be read)"}`,
462
+ ` - Recent error trail for this session`,
463
+ "",
464
+ CYAN + "Send this report? [Y/n]" + RESET,
465
+ ];
466
+ const confirmed = (await rl.question(confirmLines.join("\n") + "\n")).trim();
467
+ if (/^n/i.test(confirmed)) {
468
+ writeOut("Feedback cancelled.");
469
+ return 0;
470
+ }
471
+ // Build payload + submit.
472
+ const payload = buildFeedbackPayload({
473
+ sessionId,
474
+ sessionFile,
475
+ description,
476
+ stateDir,
477
+ env,
478
+ clientVersion: cliVersion ?? "unknown",
479
+ });
480
+ const fetchImpl = deps.fetchImpl ?? fetch;
481
+ const result = await submitFeedback(payload, profile.baseUrl, profile.token, fetchImpl);
482
+ if (result.ok) {
483
+ writeOut(GREEN + "Feedback submitted. Thank you!" + RESET);
484
+ return 0;
485
+ }
486
+ else {
487
+ writeErr(`Could not submit feedback. ${result.error ?? "Please try again."}`);
488
+ return 1;
489
+ }
490
+ }
491
+ catch {
492
+ // readline closed or EOF — treat as cancel.
493
+ writeOut("Feedback cancelled.");
494
+ return 0;
495
+ }
496
+ finally {
497
+ closeRl();
498
+ }
499
+ }
500
+ //# sourceMappingURL=feedback.js.map
@@ -15,6 +15,7 @@
15
15
  * runs locally and what the fleet executes are the same binary and the same
16
16
  * pipeline.
17
17
  */
18
+ import { type OtelLaunchConfig } from "./otel.js";
18
19
  /** Mirrors HEADLESS_GO_EXIT in the extension; duplicated to keep the packages independent. */
19
20
  export declare const GO_EXIT: {
20
21
  readonly verified: 0;
@@ -64,6 +65,8 @@ export declare function buildHeadlessChildEnv(opts: {
64
65
  workspaceId?: string;
65
66
  cliVersion?: string;
66
67
  sessionId?: string;
68
+ /** Resolved OTel export config; stage children load pi-otel when present. */
69
+ otel?: OtelLaunchConfig;
67
70
  }): NodeJS.ProcessEnv;
68
71
  /**
69
72
  * Run `yagni go`. Returns the process exit code: 0 only on a verified
@@ -20,6 +20,7 @@ import { mkdirSync } from "node:fs";
20
20
  import { pathToFileURL } from "node:url";
21
21
  import { agentDirEnvVar } from "./branding.js";
22
22
  import { agentDir, credentialsDir } from "./credentials.js";
23
+ import { otelChildEnv, resolveOtelLaunchWithWorkspace } from "./otel.js";
23
24
  import { credentialsFromProfile, profilePath, readActiveProfile } from "./profiles.js";
24
25
  import { resolveHeadlessGoPath, resolvePiCliPath } from "./paths.js";
25
26
  /** Mirrors HEADLESS_GO_EXIT in the extension; duplicated to keep the packages independent. */
@@ -54,6 +55,9 @@ export function buildHeadlessChildEnv(opts) {
54
55
  PI_CODING_AGENT_DIR: piAgentDir,
55
56
  PI_SKIP_VERSION_CHECK: "1",
56
57
  PI_TELEMETRY: "0",
58
+ // OTel export: pin metadata-only capture and forward pi-otel's path so
59
+ // every stage child spawns with it (see otel.ts for the policy).
60
+ ...(opts.otel ? otelChildEnv(opts.otel, opts.baseEnv) : {}),
57
61
  };
58
62
  }
59
63
  /**
@@ -88,6 +92,14 @@ export async function goCommand(args, deps = {}, cliVersion) {
88
92
  writeErr(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`yagni login\` first.`);
89
93
  return GO_EXIT.usage;
90
94
  }
95
+ // Same OTel gate as an interactive launch — a headless /go run's stage
96
+ // children are LLM spend too, and the pilot's cost A/B needs to see them.
97
+ const otel = await resolveOtelLaunchWithWorkspace({
98
+ env: baseEnv,
99
+ cwd: deps.cwd ?? process.cwd(),
100
+ creds: { baseUrl, token },
101
+ profileName: profile.name,
102
+ });
91
103
  const childEnv = buildHeadlessChildEnv({
92
104
  baseEnv,
93
105
  token,
@@ -96,6 +108,7 @@ export async function goCommand(args, deps = {}, cliVersion) {
96
108
  ...(profile.expiresAt ? { expiresAt: profile.expiresAt } : {}),
97
109
  ...(profile.workspaceId ? { workspaceId: profile.workspaceId } : {}),
98
110
  ...(cliVersion ? { cliVersion } : {}),
111
+ ...(otel ? { otel } : {}),
99
112
  });
100
113
  // The hermetic agent dir must exist before a stage child tries to read it.
101
114
  try {
package/dist/launch.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * defaults the provider to `yagni`.
7
7
  */
8
8
  import type { Credentials } from "./credentials.js";
9
+ import { type OtelLaunchConfig } from "./otel.js";
9
10
  export interface LaunchPlan {
10
11
  env: NodeJS.ProcessEnv;
11
12
  argv: string[];
@@ -92,6 +93,13 @@ export interface BuildLaunchOptions {
92
93
  * override the token, base URL, or hermetic agent dir.
93
94
  */
94
95
  extraEnv?: Record<string, string>;
96
+ /**
97
+ * Resolved OTel export config (see `otel.ts`). When present, the session
98
+ * loads pi-otel alongside our extension and the env pins metadata-only
99
+ * capture; absent means no OTLP endpoint is configured and the launch is
100
+ * byte-for-byte what it was before OTel support existed.
101
+ */
102
+ otel?: OtelLaunchConfig;
95
103
  }
96
104
  export declare function buildLaunch(creds: Credentials | null, passthroughArgs: string[], opts: BuildLaunchOptions): LaunchPlan;
97
105
  //# sourceMappingURL=launch.d.ts.map
package/dist/launch.js CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { agentDirEnvVar } from "./branding.js";
10
+ import { otelChildEnv } from "./otel.js";
10
11
  import { PAD_X_ENV, resolvePadX } from "./padding.js";
11
12
  import { ENGINEERING_PRACTICE_SECTION, promptEnrichmentDisabled } from "./promptEnrichment.js";
12
13
  /**
@@ -105,6 +106,10 @@ export function buildLaunch(creds, passthroughArgs, opts) {
105
106
  // the editor (which the launcher pads by seeding editorPaddingX). The
106
107
  // footer can't read pi's settings, so the value crosses over env.
107
108
  [PAD_X_ENV]: String(resolvePadX()),
109
+ // OTel export (gated in otel.ts): pin metadata-only capture and forward
110
+ // pi-otel's path so /go stage children and subagents load it too. Placed
111
+ // after baseEnv on purpose — the capture pin must beat a user env override.
112
+ ...(opts.otel ? otelChildEnv(opts.otel, opts.baseEnv ?? {}) : {}),
108
113
  };
109
114
  // Always load our extension. Default the provider to `yagni` unless the user
110
115
  // explicitly chose one (so power users can still point pi elsewhere).
@@ -125,6 +130,7 @@ export function buildLaunch(creds, passthroughArgs, opts) {
125
130
  const argv = [
126
131
  "-e",
127
132
  opts.extensionPath,
133
+ ...(opts.otel ? ["-e", opts.otel.extensionPath] : []),
128
134
  ...(userChoseProvider ? [] : ["--provider", "yagni"]),
129
135
  ...(userChoseModel ? [] : ["--model", "advanced"]),
130
136
  ...(enrichmentOff ? [] : ["--append-system-prompt", ENGINEERING_PRACTICE_SECTION]),