@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
@@ -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
- line1Parts.push(theme.fg("border", truncateEnd(input.git.branch, BRANCH_MAX_WIDTH, dim("…"))));
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
- const unsubscribeBranch = footerData.onBranchChange?.(() => {
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
- gitCache = detectGitInfo(ctx.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
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 = () => { gitCache = undefined; };
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
- gitCache = undefined;
313
+ clearCaches();
301
314
  },
302
315
  dispose() {
303
316
  unsubscribeBranch?.();
@@ -186,6 +186,12 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
186
186
  export type { TokenProvider, TokenProviderDeps, ScheduleFn } from "./tokenProvider.js";
187
187
  export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
188
188
  export type { SpoolEntry, SpoolClientOpts, FlushOutcome, JudgmentWriteOutcome } from "./spool.js";
189
+ export { registerCondensedTools, displayPath, isScratchpadPath, primaryArg, formatRowTitle, formatWriteBody, formatEditBody, formatBashErrorBody, formatBashPartialBody, formatExpandedOutput, splitBashError, countPatchAdditions, WRITE_PREVIEW_LINES, DIFF_PREVIEW_LINES, } from "./condensedTools.js";
190
+ export type { RegisterCondensedToolsDeps, RowStatus } from "./condensedTools.js";
191
+ export { ToolRunTracker, summarizeRun, isQuiet, kindForTool } from "./toolRuns.js";
192
+ export type { RowKind, ToolRow, ToolRowPatch } from "./toolRuns.js";
193
+ export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
194
+ export type { WorkingLineHandle, RegisterWorkingLineDeps } from "./workingLine.js";
189
195
  export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
190
196
  export type { CrashReporter, CrashReporterOpts, FatalCrashOpts, SanitizedCrash } from "./crashReport.js";
191
197
  //# sourceMappingURL=index.d.ts.map
@@ -16,6 +16,7 @@ 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";
21
22
  import { logEvent } from "./errorSink.js";
@@ -48,6 +49,9 @@ import { registerChipEditor } from "./chipEditor.js";
48
49
  import { defaultMineBeatGit, fileMineBeatMarkers, maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, } from "./mineBeat.js";
49
50
  import { makeFlywheelState } from "./flywheel.js";
50
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";
51
55
  function isEvalMode(env = process.env) {
52
56
  return env.YAGNI_CODE_EVAL_MODE === "1";
53
57
  }
@@ -160,6 +164,11 @@ export async function registerYagni(pi, deps = {}) {
160
164
  // no-position record suggestions reach the model) and record_decision
161
165
  // (flywheel-attributed records send dedupe: true). Run 7.
162
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);
163
172
  pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
164
173
  // WebFetch (YAG-578): read an arbitrary URL as clean markdown + a
165
174
  // standard-tier extraction, replacing the bash + curl + python dance.
@@ -177,7 +186,7 @@ export async function registerYagni(pi, deps = {}) {
177
186
  // hide it forever from a session that switched TO Advanced. One state handle
178
187
  // per session, shared with /advise so they draw on the same cap.
179
188
  const advisorState = makeAdvisorState();
180
- const askAdvisorTool = makeAskAdvisorTool({ state: advisorState });
189
+ const askAdvisorTool = makeAskAdvisorTool({ state: advisorState, workingLine });
181
190
  pi.registerTool(askAdvisorTool);
182
191
  // /advise runs the SAME tool, sharing the state handle, so a manual consult
183
192
  // draws on the same cap rather than opening a side channel around it.
@@ -217,7 +226,7 @@ export async function registerYagni(pi, deps = {}) {
217
226
  // driver's delegation identity for the diamond directive (see the
218
227
  // before_agent_start handler below) and widens the tool's fan-out ceiling.
219
228
  const ultraHolder = createUltraHolder();
220
- registerSubagents(pi, { isUltra: () => ultraHolder.get() });
229
+ registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine });
221
230
  registerUltraCommand(pi, ultraHolder);
222
231
  // Shared fetch timeout for the small, interactive display-path reads below
223
232
  // (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
@@ -593,6 +602,51 @@ export async function registerYagni(pi, deps = {}) {
593
602
  // pointers. Computed once per activation (rules are launch-time state, like
594
603
  // pi's own skill discovery); fail-soft to null.
595
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
+ }
596
650
  // Own the identity + inject live company context (and repo rules) on every
597
651
  // turn. The extension loads identically in every pi process this app spawns —
598
652
  // the interactive driver AND every `/go` stage child, subagent, and advisor
@@ -617,6 +671,7 @@ export async function registerYagni(pi, deps = {}) {
617
671
  : YAGNI_IDENTITY_DRIVER
618
672
  : undefined,
619
673
  rulesSection,
674
+ scratchpadSection: scratchpadSectionText,
620
675
  }),
621
676
  });
622
677
  // Turn-lifecycle WAL: a `turn_start` with no matching `turn_end` is the
@@ -819,6 +874,14 @@ export async function registerYagni(pi, deps = {}) {
819
874
  catch {
820
875
  // A label must never disrupt session start.
821
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
+ }
822
885
  // One-time hint naming what changed now that reasoning is collapsed by
823
886
  // default (option 2). Gated on the launcher's YAGNI_HIDE_THINKING_SEEDED
824
887
  // marker so it fires only on the launch that actually seeded the key — it
@@ -1015,5 +1078,10 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
1015
1078
  // W4 trust plumbing: the durable write-spool for the judgment-capture tools
1016
1079
  // (idempotencyKey-replayed; server-side dedup makes flush safe).
1017
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";
1018
1086
  export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
1019
1087
  //# sourceMappingURL=index.js.map
@@ -727,12 +727,59 @@ function isPipeToShell(command) {
727
727
  * backticks, including inside double quotes). Matches FORBIDDEN rules only —
728
728
  * can upgrade the classification, never relax it.
729
729
  */
730
+ const QUOTED_HEREDOC_OPEN = /^cat[ \t]+<<[ \t]*(?:'([^']+)'|"([^"]+)"|\\([A-Za-z_][A-Za-z0-9_]*))[ \t]*\r?\n/;
731
+ /**
732
+ * Recognize the `cat <<'QUOTED' … QUOTED` idiom inside a command substitution.
733
+ *
734
+ * This is the one way to pass multi-line body content to `gh`/`git` as a
735
+ * literal string: the quoted delimiter (`<<'EOF'`, `<<"EOF"`, or `<<\EOF`)
736
+ * makes the heredoc body literal — no `$`/backtick expansion — and `cat` merely
737
+ * prints it. A body like `rm -rf /tmp/test-db` inside such a heredoc is PROSE,
738
+ * not a command; sweeping it for forbidden rules is a false positive.
739
+ *
740
+ * Returns true ONLY for the exact safe shape: the whole substitution is `cat`
741
+ * with no arguments, followed by a heredoc whose delimiter is quoted and whose
742
+ * terminator is the delimiter on its own final line. Any deviation (unquoted
743
+ * delimiter, extra args to `cat`, a second command after the terminator)
744
+ * returns false and falls through to the ordinary danger scan. Safety does not
745
+ * hinge on this exemption: the outer command (the `gh pr create`, the `rm`) is
746
+ * still classified on its own merits, heredoc or not.
747
+ */
748
+ function isLiteralQuotedCatHeredoc(inner) {
749
+ const text = inner.trim();
750
+ const m = QUOTED_HEREDOC_OPEN.exec(text);
751
+ if (!m)
752
+ return false;
753
+ const delim = m[1] ?? m[2] ?? m[3];
754
+ const bodyStart = m[0].length;
755
+ const lines = text.slice(bodyStart).split(/\r?\n/);
756
+ if (lines.length === 0)
757
+ return false;
758
+ // The final line must be the bare delimiter (the closing `)` is already
759
+ // stripped by extractSubstitutions).
760
+ if (lines[lines.length - 1].trim() !== delim)
761
+ return false;
762
+ // No line BEFORE the terminator may equal the delimiter: in real bash, a
763
+ // delimiter line terminates the heredoc there, turning everything after it
764
+ // into an executed command (e.g. a body with a bare `EOF` line followed by
765
+ // `rm -rf /x` would run the `rm`). Reject that shape and fall to the scan.
766
+ for (const line of lines.slice(0, -1)) {
767
+ if (line.trim() === delim)
768
+ return false;
769
+ }
770
+ return true;
771
+ }
730
772
  function dangerScanSubstitutions(command, policy, depth) {
731
773
  if (depth > MAX_SCAN_DEPTH)
732
774
  return null;
733
775
  for (const inner of extractSubstitutions(command)) {
734
776
  if (inner.trim().length === 0)
735
777
  continue;
778
+ // Quoted-heredoc cat: the body is literal data, not an executable
779
+ // command — skipping it avoids false "forbidden" hits on prose that happens
780
+ // to contain a forbidden-looking token. The outer command still classifies.
781
+ if (isLiteralQuotedCatHeredoc(inner))
782
+ continue;
736
783
  if (isPipeToShell(inner)) {
737
784
  return {
738
785
  decision: "forbidden",
@@ -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
  ];
@@ -56,7 +56,7 @@ Numbered, small, actionable steps — each names the file/function to touch.
56
56
  ## Risks
57
57
  What to watch for, including any decision the worker will be forced to make.
58
58
 
59
- Finish the job in ONE turn: do not end your turn on an interstitial like "now let me check X". Your FINAL message MUST be the complete plan in the format above (## Goal / ## Plan / ## Files to modify or create / ## Risks). Keep exploring with your tools until you can write the whole plan, then write it as your last message.
59
+ Complete the task fully — do not gold-plate, but do not leave it half-done. Finish the job in ONE turn: do not end your turn on an interstitial like "now let me check X". Your FINAL message MUST be the complete plan in the format above (## Goal / ## Plan / ## Files to modify or create / ## Risks). Keep exploring with your tools until you can write the whole plan, then write it as your last message.
60
60
 
61
61
  Budget discipline: you have a hard output budget, and a plan that gets cut off mid-thought is worth less than a short plan that ships. Explore only until you can name the files and the steps — do not read broadly for completeness, and do not re-verify what you have already established. Aim for 5-10 short steps; the worker fills small gaps from the ticket. When in doubt, write the plan NOW.
62
62
 
@@ -65,7 +65,7 @@ const WORKER_BODY = `You are a worker with full capabilities, operating in an is
65
65
 
66
66
  You are grounded. Call ask_yagni before guessing about anything organization- or codebase-specific. Treat a confirmed answer as settled; when an answer is an unverified assumption or an inference and your change leans on it, say so in your Notes so the reviewer knows what to check. Critically: for ANY product-intent call you are forced to make that the plan did not settle — a behavior choice, a tradeoff, an interpretation of intent — call record_decision so the company's decision corpus captures it and the next agent inherits the call instead of re-litigating it. When ask_yagni reports no recorded position, follow its instruction and record the assumption you proceed on.
67
67
 
68
- You MUST make the change. If the plan is missing, partial, or appears cut off, do not stop at exploring: implement the ticket directly from the ticket text and the code, calling record_decision for any intent you infer. Ending your turn with no write/edit is a failure.
68
+ Complete the task fully — do not gold-plate, but do not leave it half-done. You MUST make the change. If the plan is missing, partial, or appears cut off, do not stop at exploring: implement the ticket directly from the ticket text and the code, calling record_decision for any intent you infer. Ending your turn with no write/edit is a failure.
69
69
 
70
70
  Output:
71
71
  ## Completed
@@ -242,14 +242,14 @@ Numbered, small, actionable steps — each names the file/function to touch.
242
242
  ## Risks
243
243
  What to watch for, including any decision the worker will be forced to make.
244
244
 
245
- Finish the job in ONE turn: do not end your turn on an interstitial like "now let me check X". Your FINAL message MUST be the complete plan in the format above (## Goal / ## Plan / ## Files to modify or create / ## Risks). Keep exploring with your tools until you can write the whole plan, then write it as your last message.
245
+ Complete the task fully — do not gold-plate, but do not leave it half-done. Finish the job in ONE turn: do not end your turn on an interstitial like "now let me check X". Your FINAL message MUST be the complete plan in the format above (## Goal / ## Plan / ## Files to modify or create / ## Risks). Keep exploring with your tools until you can write the whole plan, then write it as your last message.
246
246
 
247
247
  Budget discipline: you have a hard output budget, and a plan that gets cut off mid-thought is worth less than a short plan that ships. Explore only until you can name the files and the steps — do not read broadly for completeness, and do not re-verify what you have already established. Aim for 5-10 short steps; the worker fills small gaps from the ticket. When in doubt, write the plan NOW.
248
248
 
249
249
  Keep it concrete; the worker executes it verbatim.`;
250
250
  const WORKER_BLIND = `You are a worker with full capabilities, operating in an isolated context to implement a plan. Work autonomously and use the tools as needed.
251
251
 
252
- You MUST make the change. If the plan is missing, partial, or appears cut off, do not stop at exploring: implement the ticket directly from the ticket text and the code. Ending your turn with no write/edit is a failure.
252
+ Complete the task fully — do not gold-plate, but do not leave it half-done. You MUST make the change. If the plan is missing, partial, or appears cut off, do not stop at exploring: implement the ticket directly from the ticket text and the code. Ending your turn with no write/edit is a failure.
253
253
 
254
254
  Output:
255
255
  ## Completed
@@ -32,6 +32,7 @@ export interface RunStageDeps {
32
32
  resolveChild?: () => {
33
33
  piCli: string;
34
34
  extensionPath: string;
35
+ otelExtensionPath?: string;
35
36
  };
36
37
  /** Optional environment for spawned child stages. Defaults to Node's inherited env. */
37
38
  env?: NodeJS.ProcessEnv;
@@ -61,11 +61,19 @@ async function defaultWritePrompt(body) {
61
61
  /**
62
62
  * Default child resolution: the parent process IS pi (so `process.argv[1]` is
63
63
  * pi's cli), and our compiled extension entry sits one dir up from this module.
64
+ *
65
+ * `otelExtensionPath` comes from `YAGNI_OTEL_EXTENSION_PATH`, set by the
66
+ * launcher only when an OTLP endpoint is configured (see the CLI's otel.ts) —
67
+ * children then load pi-otel so their LLM spend is traced. Guarded with
68
+ * existsSync so a stale env value degrades to an untraced child, never a
69
+ * child that fails to boot.
64
70
  */
65
71
  function defaultResolveChild() {
66
72
  const piCli = process.argv[1] ?? "pi";
67
73
  const extensionPath = fileURLToPath(new URL("../index.js", import.meta.url));
68
- return { piCli, extensionPath };
74
+ const otelPath = process.env.YAGNI_OTEL_EXTENSION_PATH;
75
+ const otelExtensionPath = otelPath && fs.existsSync(otelPath) ? otelPath : undefined;
76
+ return { piCli, extensionPath, ...(otelExtensionPath ? { otelExtensionPath } : {}) };
69
77
  }
70
78
  const EMPTY_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
71
79
  /** Truncate to PER_TASK_OUTPUT_CAP bytes, never exceeding the cap. */
@@ -116,8 +124,12 @@ export async function runStage(stage, ctx, deps) {
116
124
  lens: ctx.lens,
117
125
  ...(tierCap ? { tierCap } : {}),
118
126
  });
119
- const { piCli, extensionPath } = resolveChild();
120
- const argv = groundedChildArgv(passthrough, { piCli, extensionPath });
127
+ const { piCli, extensionPath, otelExtensionPath } = resolveChild();
128
+ const argv = groundedChildArgv(passthrough, {
129
+ piCli,
130
+ extensionPath,
131
+ ...(otelExtensionPath ? { otelExtensionPath } : {}),
132
+ });
121
133
  // Fold events into a fixed-size accumulator as they stream — never retain
122
134
  // the full array (that OOMs on a verbose stage; YAG-317 follow-up).
123
135
  const acc = newEventAccumulator();
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `-w / --worktree` session-worktree plumbing.
3
+ *
4
+ * The launcher (`yagni-code-cli`) reaches this module by file path (the same
5
+ * seam as `headlessGo.ts` → `runHeadlessGo`) so it can create/resume a named
6
+ * YAGNI worktree BEFORE spawning pi into it. The launcher owns the process
7
+ * lifecycle (spawn + `cwd` + exit summary); this module owns the git behavior.
8
+ *
9
+ * Design invariants (see the YAG-594 plan):
10
+ * - **Add-only creation.** Every git op is `worktree add`, `fetch`, `show-ref`,
11
+ * `symbolic-ref`, or `rev-parse`. Nothing deletes, force-resets, or
12
+ * `branch -D`s — the worktree is DURABLE by default and never auto-removed.
13
+ * - **Get-or-resume.** An existing worktree dir is resumed, never recreated.
14
+ * - **Lazy fetch.** Base `origin/<default>` is read from the local ref when
15
+ * present; `git fetch` only runs when that ref is absent, and always with
16
+ * credential prompts disabled.
17
+ * - **Validate before any side effect.** The slug is checked (again, defense
18
+ * in depth against the launcher) before the first git subprocess.
19
+ * - **Canonical root.** `-w` invoked from inside an existing worktree lands in
20
+ * the main repo, never nested.
21
+ *
22
+ * Convention reused from `/wt-new`: branch `agent/<slug>`, dir `.worktrees/<slug>`
23
+ * (both gitignored in-repo). PR refs (`#N`, GitHub PR URLs) map to `pr-<N>` and
24
+ * base on `FETCH_HEAD`.
25
+ */
26
+ export interface SessionWorktreeResult {
27
+ /** Absolute destination (under `<mainRepo>/.worktrees/<slug>`). */
28
+ worktreePath: string;
29
+ /** The `agent/<slug>` branch. */
30
+ branch: string;
31
+ /** True when the worktree already existed (resumed, not created). */
32
+ existed: boolean;
33
+ }
34
+ export type SessionGit = (argv: string[], cwd: string, env?: NodeJS.ProcessEnv) => Promise<string>;
35
+ export interface CreateOrResumeDeps {
36
+ /** Repo the user ran `yagni -w` from (any path inside it works for git). */
37
+ repoCwd: string;
38
+ /** Injectable git seam (defaults to a real `git` exec). */
39
+ gitImpl?: SessionGit;
40
+ /** Injectable fs seam for existence checks (defaults to node:fs). */
41
+ pathExists?: (p: string) => boolean;
42
+ /** Injectable randomness (defaults to Math.random). */
43
+ random?: () => number;
44
+ }
45
+ /** Turn arbitrary name text into a git-ref-safe slug. Empty input → "worktree". */
46
+ export declare function slugify(name: string): string;
47
+ /**
48
+ * Validate a worktree slug before any side effect. Mirrors Claude's guard:
49
+ * length cap, per-segment allowlist, `.`/`..` rejection. Throws synchronously.
50
+ */
51
+ export declare function validateWorktreeSlug(slug: string): void;
52
+ /**
53
+ * Parse a PR reference: `#N` or a GitHub-style PR URL. Returns the number or null.
54
+ */
55
+ export declare function parsePRReference(input: string): number | null;
56
+ /**
57
+ * Create or resume the session worktree for `name`.
58
+ *
59
+ * Throws with a user-surfaced message on any failure; the caller (launcher)
60
+ * catches and prints it to stderr + the diagnostic sink. Never leaves a partial
61
+ * branch/worktree: validation happens first, and `git worktree add` is atomic.
62
+ */
63
+ export declare function createOrResume(name: string | undefined, deps: CreateOrResumeDeps): Promise<SessionWorktreeResult>;
64
+ //# sourceMappingURL=sessionWorktree.d.ts.map