@yagni-app/code 1.0.0 → 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 (55) 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 +11 -3
  9. package/dist/extension/askYagniTool.js +2 -0
  10. package/dist/extension/branding.d.ts +15 -0
  11. package/dist/extension/branding.js +76 -0
  12. package/dist/extension/chipEditor.d.ts +22 -1
  13. package/dist/extension/chipEditor.js +58 -5
  14. package/dist/extension/condensedTools.d.ts +93 -0
  15. package/dist/extension/condensedTools.js +392 -0
  16. package/dist/extension/diffStat.d.ts +62 -0
  17. package/dist/extension/diffStat.js +158 -0
  18. package/dist/extension/footer.d.ts +2 -0
  19. package/dist/extension/footer.js +21 -8
  20. package/dist/extension/index.d.ts +6 -0
  21. package/dist/extension/index.js +70 -2
  22. package/dist/extension/permission/execPolicy.js +47 -0
  23. package/dist/extension/pipeline/invocation.d.ts +7 -0
  24. package/dist/extension/pipeline/invocation.js +7 -0
  25. package/dist/extension/pipeline/personas.js +4 -4
  26. package/dist/extension/pipeline/runner.d.ts +1 -0
  27. package/dist/extension/pipeline/runner.js +15 -3
  28. package/dist/extension/pipeline/sessionWorktree.d.ts +64 -0
  29. package/dist/extension/pipeline/sessionWorktree.js +225 -0
  30. package/dist/extension/scratchpad.d.ts +66 -0
  31. package/dist/extension/scratchpad.js +93 -0
  32. package/dist/extension/subagents.d.ts +10 -0
  33. package/dist/extension/subagents.js +18 -4
  34. package/dist/extension/todos.d.ts +1 -0
  35. package/dist/extension/todos.js +15 -0
  36. package/dist/extension/toolRuns.d.ts +92 -0
  37. package/dist/extension/toolRuns.js +201 -0
  38. package/dist/extension/webFetchTool.js +2 -0
  39. package/dist/extension/workingLine.d.ts +49 -0
  40. package/dist/extension/workingLine.js +116 -0
  41. package/dist/feedback.d.ts +77 -0
  42. package/dist/feedback.js +500 -0
  43. package/dist/goHeadless.d.ts +3 -0
  44. package/dist/goHeadless.js +13 -0
  45. package/dist/launch.d.ts +8 -0
  46. package/dist/launch.js +6 -0
  47. package/dist/otel.d.ts +150 -0
  48. package/dist/otel.js +291 -0
  49. package/dist/outputFormat.d.ts +83 -0
  50. package/dist/outputFormat.js +207 -0
  51. package/dist/paths.d.ts +10 -0
  52. package/dist/paths.js +13 -0
  53. package/dist/worktreeArgs.d.ts +43 -0
  54. package/dist/worktreeArgs.js +96 -0
  55. package/package.json +3 -2
package/README.md CHANGED
@@ -171,6 +171,43 @@ Credentials live in `~/.yagni-code/profiles/<name>.json` (mode `0600`); the acti
171
171
  environment is recorded in `~/.yagni-code/config.json`. A pre-profiles
172
172
  `~/.yagni-code/credentials.json` is migrated automatically on first run.
173
173
 
174
+ ### OTel export (opt-in)
175
+
176
+ Point sessions at **your own** OpenTelemetry collector (Datadog Agent, Grafana
177
+ Alloy, an OTLP-native backend) and every session — including `/go` stage
178
+ children and subagents — emits a per-prompt span tree: interaction → LLM
179
+ request → tool calls, following the OTel GenAI semantic conventions. Nothing is
180
+ exported unless you configure an endpoint.
181
+
182
+ Enable it one of three ways (first match wins):
183
+
184
+ - `OTEL_EXPORTER_OTLP_ENDPOINT=http://<collector>:4317` in the environment
185
+ (a personal override — handy for pointing one session at a scratch
186
+ collector), or
187
+ - **workspace settings** (the zero-setup path): a workspace admin sets the
188
+ endpoint, protocol, and any collector headers (e.g. a Datadog API key) once
189
+ in the web app under Settings → YAGNI Code → Trace export. Every session in
190
+ the workspace picks it up at launch — nothing to install or configure on
191
+ developer machines. Header values are encrypted at rest server-side and the
192
+ launch-time copy is cached at mode `0600`, the same posture as your device
193
+ token. Or,
194
+ - commit `{ "otel": { "endpoint": "http://<collector>:4317" } }` to the repo's
195
+ `.pi/settings.json` so one repo's sessions export without per-machine setup.
196
+
197
+ Standard OTel env vars are honored (`OTEL_EXPORTER_OTLP_PROTOCOL`,
198
+ `OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME` — defaults to `yagni-code`),
199
+ and `PI_OTEL_DISABLED=1` is the kill switch. `yagni doctor` shows the current
200
+ export state.
201
+
202
+ Two things are enforced and not configurable:
203
+
204
+ - **Metadata only.** Token counts, cost, tier, finish reasons, and tool-call
205
+ ids export; prompt and response text never do — a settings file or env var
206
+ asking for content capture is overridden.
207
+ - **Cost is your contracted rate.** `pi.cost.usd` is computed from your
208
+ workspace's tier rate card, and the exported model name is the opaque tier
209
+ id (`advanced`, `peak`, …), so traces never fingerprint the backing model.
210
+
174
211
  Device tokens are revocable from both ends: `yagni logout` revokes the current
175
212
  one, and a workspace admin can list every connected device and revoke any token
176
213
  from the web app (Settings, YAGNI Code, Connected devices). Tokens are stored
@@ -196,6 +233,11 @@ precisely:
196
233
  third-party crash service, so we can fix the crash before you have to report
197
234
  it. Disable with `YAGNI_DISABLE_CRASH_REPORTS=1`.
198
235
 
236
+ A third flow exists only when you turn it on: **OTel export** (above) sends
237
+ session *metadata* — never prompt or response text — to a collector **you**
238
+ configure and operate. It is off unless an OTLP endpoint is set, and YAGNI
239
+ never receives these traces.
240
+
199
241
  ## Troubleshooting
200
242
 
201
243
  - **`Not logged in to environment "<name>" … Run \`yagni login\` first.`** — no
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ import { spawn } from "node:child_process";
16
16
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
17
17
  import { join } from "node:path";
18
18
  import { createInterface } from "node:readline/promises";
19
- import { fileURLToPath } from "node:url";
19
+ import { fileURLToPath, pathToFileURL } from "node:url";
20
20
  import { PI_CONFIG_NAME } from "./branding.js";
21
21
  import { claudeCompatArgs } from "./claudeCompat.js";
22
22
  import { agentDir, credentialsDir, piPackageDir } from "./credentials.js";
@@ -27,14 +27,18 @@ import { login } from "./login.js";
27
27
  import { logout } from "./logout.js";
28
28
  import { tokenCommand } from "./token.js";
29
29
  import { buildLaunch } from "./launch.js";
30
+ import { resolveOtelLaunchWithWorkspace } from "./otel.js";
31
+ import { parseOutputFormat, parseJsonEvents, buildResultObject, readGuardianEvents, } from "./outputFormat.js";
32
+ import { feedbackCommand } from "./feedback.js";
30
33
  import { runDoctor } from "./doctor.js";
31
34
  import { installProcessCrashHandlers } from "./crashReport.js";
32
35
  import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
33
36
  import { maybeRefreshAtLaunch } from "./refresh.js";
34
37
  import { exitCodeFor, installSignalForwarding } from "./signalForward.js";
35
38
  import { PAD_X } from "./padding.js";
39
+ import { parseWorktreeFlag, validateWorktreeLaunchArgs } from "./worktreeArgs.js";
36
40
  import { ensureShadowPiPackage } from "./piPackage.js";
37
- import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
41
+ import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir, resolveSessionWorktreePath } from "./paths.js";
38
42
  import { credentialsFromProfile, getActiveProfileName, listProfiles, migrateLegacyCredentials, persistProfileTokenRotation, profilePath, readActiveProfile, useProfile, } from "./profiles.js";
39
43
  // Present as "yagni" in process listings, not "node".
40
44
  process.title = DISTRIBUTION.commandName;
@@ -159,6 +163,18 @@ export function seedHideThinkingBlock(piAgentDir) {
159
163
  return seedSetting(piAgentDir, "hideThinkingBlock", true);
160
164
  }
161
165
  async function runDefault(passthroughArgs) {
166
+ // `-w / --worktree [name]` is its own launch path: create/resume a worktree
167
+ // and enter a session there. Gate strictly on `requested` (not `name`) so a
168
+ // bare `-w` (random slug) is honored too. Returning early keeps the standard
169
+ // path below physically unreachable by any `-w` bug.
170
+ const worktree = parseWorktreeFlag(passthroughArgs);
171
+ if (worktree.requested) {
172
+ return runWorktreeLaunch(worktree.remainingArgs, worktree.name);
173
+ }
174
+ // Parse --output-format out of argv before passing to pi (pi doesn't know
175
+ // about it). The format determines how we handle pi's stdout: text = inherit,
176
+ // stream-json = inherit with --mode json, json = pipe + post-process.
177
+ const { format: outputFormat, remainingArgs } = parseOutputFormat(passthroughArgs);
162
178
  // Cache-backed update nudge (never a network wait), then a background cache
163
179
  // refresh that completes while the session runs. Both fail soft.
164
180
  await maybeNudgeAndRefresh({ current: cliVersion() });
@@ -230,6 +246,16 @@ async function runDefault(passthroughArgs) {
230
246
  catch {
231
247
  compat = { argv: [], env: {} };
232
248
  }
249
+ // OTel export: load pi-otel only when an OTLP endpoint is configured — the
250
+ // user's env, the workspace's admin-set config (fetched fail-soft, cached),
251
+ // or the repo's .pi/settings.json, in that order. undefined keeps the
252
+ // launch untouched. See otel.ts for the policy.
253
+ const otel = await resolveOtelLaunchWithWorkspace({
254
+ env: process.env,
255
+ cwd: process.cwd(),
256
+ creds,
257
+ profileName: profile.name,
258
+ });
233
259
  // buildLaunch runs the token-expiry preflight: it throws (with an actionable
234
260
  // login prompt) on an already-expired token so we never spawn a session that
235
261
  // immediately 401s, and returns non-fatal warnings (e.g. expiry approaching).
@@ -237,7 +263,7 @@ async function runDefault(passthroughArgs) {
237
263
  // re-login in another terminal is picked up here with no stale cached token.
238
264
  let plan;
239
265
  try {
240
- plan = buildLaunch(creds, passthroughArgs, {
266
+ plan = buildLaunch(creds, remainingArgs, {
241
267
  extensionPath: resolveExtensionPath(),
242
268
  agentDir: piAgentDir,
243
269
  piPackageDir: shadowPiDir,
@@ -250,6 +276,7 @@ async function runDefault(passthroughArgs) {
250
276
  cliVersion: cliVersion(),
251
277
  baseEnv: process.env,
252
278
  ...(seededHideThinking ? { hideThinkingSeeded: true } : {}),
279
+ ...(otel ? { otel } : {}),
253
280
  });
254
281
  }
255
282
  catch (err) {
@@ -260,24 +287,210 @@ async function runDefault(passthroughArgs) {
260
287
  process.stderr.write(`${warning}\n`);
261
288
  }
262
289
  const { env, argv } = plan;
290
+ return spawnPiAndAwait({
291
+ argv,
292
+ env,
293
+ remainingArgs,
294
+ outputFormat,
295
+ });
296
+ }
297
+ /**
298
+ * Spawn pi and await its exit, mapping to the launcher's exit code. Extracted
299
+ * so `runDefault` (cwd = current) and `runWorktreeLaunch` (cwd = worktree) share
300
+ * the exact same json/stream-json post-processing, signal forwarding, and exit
301
+ * code mapping — the two paths can never drift on the output contract.
302
+ */
303
+ async function spawnPiAndAwait(opts) {
304
+ const { argv, env, remainingArgs, outputFormat, cwd } = opts;
305
+ // For json/stream-json output, inject --mode json so pi emits NDJSON events.
306
+ const userChoseMode = remainingArgs.some((a) => a === "--mode" || a.startsWith("--mode="));
307
+ const childArgv = outputFormat === "text" || userChoseMode
308
+ ? argv
309
+ : [...argv, "--mode", "json"];
310
+ // text + stream-json: inherit stdout (passthrough). json: pipe stdout so we
311
+ // can post-process the NDJSON into a single result object.
312
+ const stdio = outputFormat === "json"
313
+ ? ["inherit", "pipe", "inherit"]
314
+ : "inherit";
263
315
  const piCli = resolvePiCliPath();
316
+ const startMs = Date.now();
264
317
  return await new Promise((resolve) => {
265
- const child = spawn(process.execPath, [piCli, ...argv], {
266
- stdio: "inherit",
318
+ const child = spawn(process.execPath, [piCli, ...childArgv], {
319
+ stdio: stdio,
267
320
  env,
321
+ ...(cwd ? { cwd } : {}),
268
322
  });
323
+ // Collect pi's stdout when piping for --output-format json.
324
+ let stdoutChunks = "";
325
+ if (outputFormat === "json" && child.stdout) {
326
+ child.stdout.setEncoding("utf8");
327
+ child.stdout.on("data", (chunk) => {
328
+ stdoutChunks += chunk;
329
+ });
330
+ }
269
331
  // Forward termination signals to pi instead of dying around it (see
270
332
  // signalForward.ts for the policy: first signal graceful, second tree-kill).
271
333
  installSignalForwarding(child);
272
334
  // 128+n for a signal death (bash parity), so a cancelled/killed run never
273
335
  // reads as success to scripts or CI.
274
- child.on("exit", (code, signal) => resolve(exitCodeFor(code, signal)));
336
+ child.on("exit", (code, signal) => {
337
+ const exitCode = exitCodeFor(code, signal);
338
+ if (outputFormat === "json") {
339
+ const durationMs = Date.now() - startMs;
340
+ const events = parseJsonEvents(stdoutChunks);
341
+ const yagniSessionId = env.YAGNI_SESSION_ID ?? "";
342
+ const guardianEvents = readGuardianEvents(yagniSessionId);
343
+ const verbose = remainingArgs.includes("--verbose");
344
+ const result = buildResultObject({
345
+ events,
346
+ guardianEvents,
347
+ durationMs,
348
+ verbose,
349
+ });
350
+ process.stdout.write(`${JSON.stringify(result)}\n`);
351
+ }
352
+ resolve(exitCode);
353
+ });
275
354
  child.on("error", (err) => {
276
355
  process.stderr.write(`Failed to start YAGNI Code: ${err.message}\n`);
277
356
  resolve(1);
278
357
  });
279
358
  });
280
359
  }
360
+ async function defaultLoadSessionWorktree() {
361
+ const mod = (await import(pathToFileURL(resolveSessionWorktreePath()).href));
362
+ if (typeof mod?.createOrResume !== "function") {
363
+ throw new Error("The bundled extension is missing its session-worktree entry point (is the CLI up to date?).");
364
+ }
365
+ return mod;
366
+ }
367
+ /**
368
+ * `yagni -w [name]` — create/resume a worktree and enter a session there.
369
+ *
370
+ * Delegates all git behavior to the extension's `sessionWorktree` entry; this
371
+ * launcher path only resolves credentials, builds the plan, and spawns pi with
372
+ * `cwd` = the worktree. The worktree is DURABLE — nothing is ever removed.
373
+ */
374
+ async function runWorktreeLaunch(passthroughArgs, worktreeName, loadSessionWorktree = defaultLoadSessionWorktree) {
375
+ // Validate the name + argv before any side effect (slug guard + `-c`
376
+ // disallow). The extension re-validates the mapped slug for PR refs.
377
+ const launchError = validateWorktreeLaunchArgs(worktreeName, passthroughArgs);
378
+ if (launchError !== undefined) {
379
+ process.stderr.write(`${launchError}\n`);
380
+ return 1;
381
+ }
382
+ const { format: outputFormat, remainingArgs } = parseOutputFormat(passthroughArgs);
383
+ // Load the session-worktree entry first, so a stale bundled extension fails
384
+ // honestly before we mutate anything.
385
+ let sessionWorktree;
386
+ try {
387
+ sessionWorktree = await loadSessionWorktree();
388
+ }
389
+ catch (err) {
390
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
391
+ return 1;
392
+ }
393
+ // Create/resume resolves the main repo root + branch + dir, and performs
394
+ // `git worktree add` / resume. Never throws on a normal path; catch-all maps
395
+ // to a clean stderr + exit 1.
396
+ let result;
397
+ try {
398
+ result = await sessionWorktree.createOrResume(worktreeName, {
399
+ repoCwd: process.cwd(),
400
+ });
401
+ }
402
+ catch (err) {
403
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
404
+ return 1;
405
+ }
406
+ // Reuse the standard credential + env + plan flow (same token refresh, shadow
407
+ // package, compat args, preflight). The worktree becomes the project cwd, so
408
+ // compat assets resolve from there.
409
+ await maybeNudgeAndRefresh({ current: cliVersion() });
410
+ const profile = await readActiveProfile();
411
+ let creds = credentialsFromProfile(profile);
412
+ if (!creds?.token) {
413
+ process.stderr.write(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`yagni login\` first.\n`);
414
+ return 1;
415
+ }
416
+ const refresh = await maybeRefreshAtLaunch(creds, {
417
+ persist: (c) => persistProfileTokenRotation(profile.name, c),
418
+ });
419
+ for (const warning of refresh.warnings) {
420
+ process.stderr.write(`${warning}\n`);
421
+ }
422
+ creds = refresh.creds;
423
+ const piAgentDir = agentDir(profile.name);
424
+ mkdirSync(piAgentDir, { recursive: true, mode: 0o700 });
425
+ seedEditorPadding(piAgentDir);
426
+ seedCollapseChangelog(piAgentDir);
427
+ seedHideThinkingBlock(piAgentDir);
428
+ let shadowPiDir;
429
+ try {
430
+ shadowPiDir = ensureShadowPiPackage({
431
+ realPiDir: resolvePiPackageDir(),
432
+ shadowDir: piPackageDir(),
433
+ name: PI_CONFIG_NAME,
434
+ });
435
+ }
436
+ catch {
437
+ shadowPiDir = undefined;
438
+ }
439
+ let compat = { argv: [], env: {} };
440
+ try {
441
+ compat = await claudeCompatArgs({
442
+ cwd: result.worktreePath,
443
+ agentDir: piAgentDir,
444
+ confirm: confirmOnTty,
445
+ });
446
+ }
447
+ catch {
448
+ compat = { argv: [], env: {} };
449
+ }
450
+ // OTel gate reads the WORKTREE's .pi/settings.json — that is the session's
451
+ // cwd, so a repo-committed otel config applies to its worktrees too.
452
+ const otel = await resolveOtelLaunchWithWorkspace({
453
+ env: process.env,
454
+ cwd: result.worktreePath,
455
+ creds,
456
+ profileName: profile.name,
457
+ });
458
+ let plan;
459
+ try {
460
+ plan = buildLaunch(creds, remainingArgs, {
461
+ extensionPath: resolveExtensionPath(),
462
+ agentDir: piAgentDir,
463
+ piPackageDir: shadowPiDir,
464
+ extraAgentArgs: compat.argv,
465
+ extraEnv: compat.env,
466
+ profilePath: profilePath(profile.name),
467
+ stateDir: credentialsDir(),
468
+ cliVersion: cliVersion(),
469
+ baseEnv: process.env,
470
+ ...(otel ? { otel } : {}),
471
+ });
472
+ }
473
+ catch (err) {
474
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
475
+ return 1;
476
+ }
477
+ for (const warning of plan.warnings) {
478
+ process.stderr.write(`${warning}\n`);
479
+ }
480
+ const exitCode = await spawnPiAndAwait({
481
+ argv: plan.argv,
482
+ env: plan.env,
483
+ remainingArgs,
484
+ outputFormat,
485
+ cwd: result.worktreePath,
486
+ });
487
+ // Durable by default: nothing is removed. Tell the user where their work
488
+ // lives (stderr only — never stdout, to keep json clean).
489
+ process.stderr.write(`[yagni] worktree ${result.existed ? "resumed" : "created"}: ${result.worktreePath}\n` +
490
+ `[yagni] branch: ${result.branch}\n` +
491
+ `[yagni] resume: cd ${result.worktreePath} && yagni\n`);
492
+ return exitCode;
493
+ }
281
494
  export const HELP_TEXT = [
282
495
  "YAGNI Code — a business-context-grounded terminal coding agent.",
283
496
  "",
@@ -285,10 +498,16 @@ export const HELP_TEXT = [
285
498
  " yagni [args…] Launch the agent in the current repo.",
286
499
  " yagni -c Continue the most recent session.",
287
500
  " yagni -r Browse and resume a previous session.",
501
+ " yagni -w [name] Create/resume a worktree and enter a session there.",
288
502
  ' yagni -p "prompt" Print one response and exit (reads piped stdin too).',
503
+ ' yagni -p "prompt" Use --output-format json for a machine-readable',
504
+ ' --output-format json result object with tools, cost, guardian reviews.',
289
505
  " yagni login Authorize the active environment (device-code flow).",
290
506
  " yagni logout Revoke and clear the active environment's token.",
291
507
  " yagni doctor Check that everything is ready (green/red checklist).",
508
+ " yagni feedback [sessionId] File a bug report from the shell. Lists recent",
509
+ " sessions to pick, or pass a session ID directly.",
510
+ " Attaches transcript + error trail, then submits.",
292
511
  " yagni go --headless Run the /go pipeline without a session, for scripts",
293
512
  " and CI: --ticket-file <path> [--plan-file <path>]",
294
513
  " [--memo-file <path>] [--run-id <id>] [--json].",
@@ -307,6 +526,9 @@ export const HELP_TEXT = [
307
526
  " --model <tier> Model tier (fixed to advanced).",
308
527
  " --thinking <level> off | minimal | low | medium | high | xhigh | max",
309
528
  " --session <id> Open a specific session; --fork <id> branches one.",
529
+ " --output-format <fmt> Output format: text (default), json, stream-json.",
530
+ " json emits a single result object; stream-json",
531
+ " emits NDJSON events (same as --mode json).",
310
532
  " --mode json Emit machine-readable events (for scripts and CI).",
311
533
  "",
312
534
  "In a session:",
@@ -437,6 +659,9 @@ export async function main(argv) {
437
659
  if (command === "go") {
438
660
  return goCommand(rest, {}, cliVersion());
439
661
  }
662
+ if (command === "feedback") {
663
+ return feedbackCommand(rest, {}, cliVersion());
664
+ }
440
665
  if (command === "token") {
441
666
  return tokenCommand();
442
667
  }
@@ -41,6 +41,7 @@ export declare function crashReportsDisabled(env?: NodeJS.ProcessEnv): boolean;
41
41
  export declare function runningUnderTest(env?: NodeJS.ProcessEnv): boolean;
42
42
  /** Reporting is off when the user disabled it OR this is a test process. */
43
43
  export declare function crashReportsSuppressed(env?: NodeJS.ProcessEnv): boolean;
44
+ export declare const SECRET_PATTERNS: Array<[RegExp, string]>;
44
45
  export interface SanitizeCrashOptions {
45
46
  /** Environment whose values get redacted (defaults to process.env). */
46
47
  env?: NodeJS.ProcessEnv;
@@ -57,6 +58,13 @@ export interface SanitizeCrashOptions {
57
58
  * `node_modules/` on, so dependency frames stay diagnosable)
58
59
  * Over-redacts rather than under-redacts; pure; never throws.
59
60
  */
61
+ /**
62
+ * Lightweight secret-only scrub — applies SECRET_PATTERNS and nothing else.
63
+ * Matches the extension's `scrubSecrets` exactly (no path collapse, no env
64
+ * redaction). Use this for feedback transcripts where file paths and code
65
+ * context must stay readable; the backend re-normalizes home paths on receipt.
66
+ */
67
+ export declare function scrubSecrets(text: string): string;
60
68
  export declare function sanitizeCrashText(text: string, opts?: SanitizeCrashOptions): string;
61
69
  export interface SanitizedCrash {
62
70
  errorClass: string;
@@ -62,7 +62,7 @@ export function crashReportsSuppressed(env = process.env) {
62
62
  }
63
63
  // Mirrors scrubSecrets (backend yagniCode/scrubSecrets.ts and
64
64
  // pi-extension-yagni pipeline/scrubSecrets.ts) — keep in sync.
65
- const SECRET_PATTERNS = [
65
+ export const SECRET_PATTERNS = [
66
66
  [/\b([a-z][a-z0-9+.\-]*:\/\/[^\s:@/]+):[^\s:@/]+@/gi, "$1:[REDACTED]@"],
67
67
  [/\b(sk-[A-Za-z0-9]{16,}|sk_(?:live|test)_[A-Za-z0-9]{16,}|rk_(?:live|test)_[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_\-]{20,})\b/g, "[REDACTED]"],
68
68
  [/\b([A-Za-z0-9_]*(?:secret|password|passwd|api[_-]?key|token|private[_-]?key|access[_-]?key)[A-Za-z0-9_]*)\b(\s*[:=]\s*)("[^"]+"|'[^']+'|`[^`]+`|[^\s"']+)/gi, "$1$2[REDACTED]"],
@@ -106,6 +106,18 @@ function collapsePathToken(token) {
106
106
  * `node_modules/` on, so dependency frames stay diagnosable)
107
107
  * Over-redacts rather than under-redacts; pure; never throws.
108
108
  */
109
+ /**
110
+ * Lightweight secret-only scrub — applies SECRET_PATTERNS and nothing else.
111
+ * Matches the extension's `scrubSecrets` exactly (no path collapse, no env
112
+ * redaction). Use this for feedback transcripts where file paths and code
113
+ * context must stay readable; the backend re-normalizes home paths on receipt.
114
+ */
115
+ export function scrubSecrets(text) {
116
+ let out = text;
117
+ for (const [re, repl] of SECRET_PATTERNS)
118
+ out = out.replace(re, repl);
119
+ return out;
120
+ }
109
121
  export function sanitizeCrashText(text, opts = {}) {
110
122
  let out = text;
111
123
  const env = opts.env ?? process.env;
package/dist/doctor.d.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  * Advisory checks (loose perms, missing `gh`) never flip the exit code.
14
14
  */
15
15
  import { type TokenExpiryStatus } from "./launch.js";
16
+ import { type OtelLaunchConfig } from "./otel.js";
16
17
  import { type Profile } from "./profiles.js";
17
18
  export type CheckStatus = "ok" | "warn" | "fail";
18
19
  export interface CheckResult {
@@ -60,6 +61,12 @@ export declare function checkCliUpdate(probe: {
60
61
  latest: string | null;
61
62
  }): CheckResult;
62
63
  export declare function checkGh(onPath: boolean): CheckResult;
64
+ /**
65
+ * Advisory OTel-export line: says whether sessions will stream traces to an
66
+ * OTLP collector, and from which config source. Never flips the exit code —
67
+ * most machines have no collector, and that is the healthy default.
68
+ */
69
+ export declare function checkOtelExport(config: OtelLaunchConfig | undefined): CheckResult;
63
70
  /** What the Windows bash probe found (pi needs a bash — Git Bash — on win32). */
64
71
  export interface BashProbe {
65
72
  found: boolean;
package/dist/doctor.js CHANGED
@@ -17,6 +17,7 @@ import { delimiter, join } from "node:path";
17
17
  import { credentialsDir } from "./credentials.js";
18
18
  import { currentCliVersion, fetchLatestVersion, isNewerVersion } from "./upgrade.js";
19
19
  import { classifyTokenExpiry } from "./launch.js";
20
+ import { resolveOtelLaunchWithWorkspace } from "./otel.js";
20
21
  import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
21
22
  import { readActiveProfile } from "./profiles.js";
22
23
  // ── Pure check builders ─────────────────────────────────────────────────────
@@ -198,6 +199,32 @@ export function checkGh(onPath) {
198
199
  required: false,
199
200
  };
200
201
  }
202
+ /**
203
+ * Advisory OTel-export line: says whether sessions will stream traces to an
204
+ * OTLP collector, and from which config source. Never flips the exit code —
205
+ * most machines have no collector, and that is the healthy default.
206
+ */
207
+ export function checkOtelExport(config) {
208
+ if (!config) {
209
+ return {
210
+ name: "otel export (optional)",
211
+ status: "ok",
212
+ detail: "off (no OTLP endpoint configured)",
213
+ required: false,
214
+ };
215
+ }
216
+ const source = config.source === "env"
217
+ ? "OTEL_EXPORTER_OTLP_ENDPOINT"
218
+ : config.source === "workspace"
219
+ ? "workspace settings"
220
+ : ".pi/settings.json";
221
+ return {
222
+ name: "otel export (optional)",
223
+ status: "ok",
224
+ detail: `on → ${config.endpoint} (${source}, metadata-only)`,
225
+ required: false,
226
+ };
227
+ }
201
228
  export function checkBash(probe) {
202
229
  if (!probe.found) {
203
230
  return {
@@ -379,6 +406,12 @@ export async function gatherChecks(deps = {}) {
379
406
  checks.push(checkBackend(backend));
380
407
  checks.push(checkStateDir(probeStateDir()));
381
408
  checks.push(checkGh(ghOnPath()));
409
+ checks.push(checkOtelExport(await resolveOtelLaunchWithWorkspace({
410
+ env: process.env,
411
+ cwd: process.cwd(),
412
+ creds: profile.token ? { baseUrl: profile.baseUrl, token: profile.token } : null,
413
+ profileName: profile.name,
414
+ })));
382
415
  return checks;
383
416
  }
384
417
  /**
@@ -29,6 +29,7 @@ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-age
29
29
  import { type Component } from "@earendil-works/pi-tui";
30
30
  import { Type } from "typebox";
31
31
  import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
32
+ import type { WorkingLineHandle } from "./workingLine.js";
32
33
  import { runStage as defaultRunStage } from "./pipeline/runner.js";
33
34
  import { type PipelineStage } from "./pipeline/types.js";
34
35
  import { type RenderTheme, type SubagentTaskProgress } from "./subagentRender.js";
@@ -49,6 +50,12 @@ export interface MakeAskAdvisorToolOptions {
49
50
  limits?: AdvisorLimits;
50
51
  /** Injectable so tests never spawn a child. */
51
52
  runStage?: typeof defaultRunStage;
53
+ /**
54
+ * The session working-line manager (workingLine.ts). When present, live
55
+ * consult progress goes through it (so the elapsed/token suffix survives);
56
+ * absent, the tool falls back to ui.setWorkingMessage directly.
57
+ */
58
+ workingLine?: WorkingLineHandle;
52
59
  }
53
60
  /**
54
61
  * Assemble the consult brief. The advisor's persona already tells it not to take
@@ -125,6 +125,8 @@ export function makeAskAdvisorTool(opts) {
125
125
  "The advice comes back as plain text: act on it, and call record_decision when it settles a product-intent call so the next agent inherits it.",
126
126
  ],
127
127
  parameters,
128
+ // Self-framed: the condensed transcript look has no tinted tool boxes.
129
+ renderShell: "self",
128
130
  renderCall: renderAdvisorCall,
129
131
  renderResult: renderSubagentResult,
130
132
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
@@ -159,9 +161,12 @@ export function makeAskAdvisorTool(opts) {
159
161
  },
160
162
  });
161
163
  const working = formatWorkingMessage([progress], now);
162
- if (ui && working !== lastWorking) {
164
+ if (working !== lastWorking) {
163
165
  lastWorking = working;
164
- ui.setWorkingMessage?.(working);
166
+ if (opts.workingLine)
167
+ opts.workingLine.setActivity(working);
168
+ else
169
+ ui?.setWorkingMessage?.(working);
165
170
  }
166
171
  };
167
172
  emit();
@@ -181,7 +186,10 @@ export function makeAskAdvisorTool(opts) {
181
186
  }
182
187
  finally {
183
188
  // Restore the default "Working…" text whether we resolved or threw.
184
- ui?.setWorkingMessage?.();
189
+ if (opts.workingLine)
190
+ opts.workingLine.setActivity(undefined);
191
+ else
192
+ ui?.setWorkingMessage?.();
185
193
  }
186
194
  const cost = result.usage?.cost ?? 0;
187
195
  const state = opts.state.record(cost);
@@ -56,6 +56,8 @@ export function makeAskYagniTool(opts) {
56
56
  "Answers carry a standing: treat a confirmed decision as settled; when you lean on an unverified assumption or an inference, say so where the work is reviewed; when there is no recorded position, follow the answer's instruction to record the assumption you proceed on.",
57
57
  ],
58
58
  parameters,
59
+ // Self-framed: the condensed transcript look has no tinted tool boxes.
60
+ renderShell: "self",
59
61
  renderCall(args, theme) {
60
62
  const t = theme;
61
63
  let text = `${t.fg("toolTitle", t.bold("ask_yagni"))} ${t.fg("dim", clipLine(args?.question ?? "…", 100))}`;
@@ -59,6 +59,19 @@ export declare const YAGNI_IDENTITY_DRIVER = "You are YAGNI Code, an autonomous
59
59
  * open with a `- ` bullet line, no emojis.
60
60
  */
61
61
  export declare const TICKET_IMAGE_RULE: string;
62
+ /**
63
+ * GitHub operations recipe. A standing directive that teaches the
64
+ * model the one correct way to reply to inline PR review comments and how to
65
+ * pass multi-line bodies to `gh`/`git` without shell-quoting failures. It is
66
+ * GitHub-mechanics knowledge every session needs — the two failures it fixes
67
+ * (hoisting several inline replies into one top-level comment; a shell-
68
+ * quoting dance that mangles multi-line bodies) are universal, not
69
+ * workflow-specific.
70
+ *
71
+ * Content constraints (parity with {@link TICKET_IMAGE_RULE}): must not contain
72
+ * the standalone word "pi", must not open with a `- ` bullet line, no emojis.
73
+ */
74
+ export declare const GITHUB_OPERATIONS: string;
62
75
  /**
63
76
  * The injected-reminder framing (YAG-574, Change A prerequisite). Claude Code
64
77
  * carries this exact sentence in every system prompt so its whole reminder
@@ -111,6 +124,8 @@ export interface BrandSystemPromptOptions {
111
124
  * never brand-rewritten (same exemption as <project_context>).
112
125
  */
113
126
  rulesSection?: string | null;
127
+ /** Scratchpad-directory prompt section, supplied when a scratchpad is configured. */
128
+ scratchpadSection?: string;
114
129
  }
115
130
  /**
116
131
  * Rebrand pi's assembled system prompt as YAGNI Code's, and optionally inject a