@yagni-app/code-staging 1.1.5-staging.1430.1 → 1.1.5-staging.1434.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.
@@ -8,10 +8,15 @@
8
8
  * TUI sees is a complete picture of all tasks.
9
9
  *
10
10
  * The renderer half implements pi's per-tool rendering seam (`renderCall` /
11
- * `renderResult`). Collapsed-while-running is the two-line pattern: a stable
11
+ * `renderResult`). pi paints the call component above the result component
12
+ * for the life of the row, so the two split the work: the call header lists
13
+ * the tasks only while the arguments stream in, and hands off to the result
14
+ * rows the moment execution starts (the tool's first partial update fires at
15
+ * spawn). Collapsed-while-running is the two-line pattern: a stable
12
16
  * `agent — task` title over a churning `↳ current tool` line. Completion is a
13
- * one-line receipt (`✓ agent · N tool uses · Xk tokens · Ys`); expanded shows
14
- * the curated action log and the full report as markdown. String assembly is
17
+ * receipt (`✓ agent · N tool uses · Xk tokens · Ys`) over a dim task line;
18
+ * expanded shows the curated action log and the full report as markdown.
19
+ * String assembly is
15
20
  * kept in pure helpers over a minimal {@link RenderTheme} so tests run against
16
21
  * plain text — renderer exceptions are swallowed by pi (silently degrading to
17
22
  * the bare title bar), so everything here must stay boringly total.
@@ -116,8 +121,28 @@ interface RenderContextSlice {
116
121
  state?: LiveRenderState;
117
122
  invalidate: () => void;
118
123
  }
119
- /** Title painted the moment the call streams in (before any execution output). */
120
- export declare function renderSubagentCall(args: SubagentCallArgs | undefined, theme: RenderTheme, _context: unknown): Component;
124
+ /** The slice of pi's ToolRenderContext the call renderer reads. */
125
+ interface CallContextSlice {
126
+ /** True from the moment pi invokes `execute` (tool-execution.ts re-renders the call on the flip). */
127
+ executionStarted?: boolean;
128
+ /**
129
+ * True from construction until a FINAL result lands. A row rebuilt from
130
+ * session history on resume never has `executionStarted` flipped (pi only
131
+ * marks it on the live tool_execution_start event) but does carry its final
132
+ * result, so this is the signal that keeps a replayed row from listing twice.
133
+ */
134
+ isPartial?: boolean;
135
+ }
136
+ /**
137
+ * Title painted the moment the call streams in. While the model is still
138
+ * writing the arguments this is the only preview of what is being delegated,
139
+ * so it lists every task. Once execution starts (or, on a replayed row, once
140
+ * the final result is present) the result renderer paints a row per task
141
+ * (live line, then receipt) directly beneath, so the header collapses to its
142
+ * title line — otherwise every agent was listed twice, once here and once in
143
+ * the result, for the whole life of the call.
144
+ */
145
+ export declare function renderSubagentCall(args: SubagentCallArgs | undefined, theme: RenderTheme, context: CallContextSlice | undefined): Component;
121
146
  /**
122
147
  * A prose body as markdown when the TUI's markdown theme is available.
123
148
  * `getMarkdownTheme()` hands back a lazy proxy that only throws when a style is
@@ -8,10 +8,15 @@
8
8
  * TUI sees is a complete picture of all tasks.
9
9
  *
10
10
  * The renderer half implements pi's per-tool rendering seam (`renderCall` /
11
- * `renderResult`). Collapsed-while-running is the two-line pattern: a stable
11
+ * `renderResult`). pi paints the call component above the result component
12
+ * for the life of the row, so the two split the work: the call header lists
13
+ * the tasks only while the arguments stream in, and hands off to the result
14
+ * rows the moment execution starts (the tool's first partial update fires at
15
+ * spawn). Collapsed-while-running is the two-line pattern: a stable
12
16
  * `agent — task` title over a churning `↳ current tool` line. Completion is a
13
- * one-line receipt (`✓ agent · N tool uses · Xk tokens · Ys`); expanded shows
14
- * the curated action log and the full report as markdown. String assembly is
17
+ * receipt (`✓ agent · N tool uses · Xk tokens · Ys`) over a dim task line;
18
+ * expanded shows the curated action log and the full report as markdown.
19
+ * String assembly is
15
20
  * kept in pure helpers over a minimal {@link RenderTheme} so tests run against
16
21
  * plain text — renderer exceptions are swallowed by pi (silently degrading to
17
22
  * the bare title bar), so everything here must stay boringly total.
@@ -226,6 +231,14 @@ export function receiptLine(p, theme) {
226
231
  }
227
232
  return `${theme.fg("success", "✓")} ${name} ${theme.fg("dim", `· ${parts.join(" · ")}`)}`;
228
233
  }
234
+ /**
235
+ * The dim task line under a receipt. Once execution starts the call header
236
+ * no longer lists the tasks (see {@link renderSubagentCall}), so the receipt
237
+ * is the only place the reader can tell two same-named agents apart.
238
+ */
239
+ function taskLine(p, theme) {
240
+ return theme.fg("dim", ` ${clip(p.task, 2 * TASK_PREVIEW_MAX)}`);
241
+ }
229
242
  /** Aggregate receipt across parallel tasks. */
230
243
  function totalLine(tasks, theme) {
231
244
  const toolCalls = tasks.reduce((n, p) => n + p.toolCalls, 0);
@@ -262,19 +275,31 @@ export function formatWorkingMessage(tasks, now) {
262
275
  const label = running > 0 && running < tasks.length ? `${running}/${tasks.length} subagents` : `${tasks.length} subagents`;
263
276
  return `${label} · ${countToolUses(toolCalls)} · ${elapsed}`;
264
277
  }
265
- /** Title painted the moment the call streams in (before any execution output). */
266
- export function renderSubagentCall(args, theme, _context) {
278
+ /**
279
+ * Title painted the moment the call streams in. While the model is still
280
+ * writing the arguments this is the only preview of what is being delegated,
281
+ * so it lists every task. Once execution starts (or, on a replayed row, once
282
+ * the final result is present) the result renderer paints a row per task
283
+ * (live line, then receipt) directly beneath, so the header collapses to its
284
+ * title line — otherwise every agent was listed twice, once here and once in
285
+ * the result, for the whole life of the call.
286
+ */
287
+ export function renderSubagentCall(args, theme, context) {
267
288
  const title = theme.fg("toolTitle", theme.bold("subagent"));
268
289
  const a = args ?? {};
290
+ const resultOwnsRows = context?.executionStarted === true || context?.isPartial === false;
291
+ const listTasks = !resultOwnsRows;
269
292
  if (a.tasks && a.tasks.length > 0) {
270
293
  let text = `${title} ${theme.fg("accent", `${a.tasks.length} parallel tasks`)}`;
271
- for (const t of a.tasks) {
272
- text += `\n ${theme.fg("accent", t.agent ?? "general")} ${theme.fg("dim", clip(t.task ?? "…", TASK_PREVIEW_MAX))}`;
294
+ if (listTasks) {
295
+ for (const t of a.tasks) {
296
+ text += `\n ${theme.fg("accent", t.agent ?? "general")} ${theme.fg("dim", clip(t.task ?? "…", TASK_PREVIEW_MAX))}`;
297
+ }
273
298
  }
274
299
  return new Text(text, 0, 0);
275
300
  }
276
301
  let text = `${title} ${theme.fg("accent", a.agent ?? "general")}`;
277
- if (a.task)
302
+ if (listTasks && a.task)
278
303
  text += `\n ${theme.fg("dim", clip(a.task, 2 * TASK_PREVIEW_MAX))}`;
279
304
  return new Text(text, 0, 0);
280
305
  }
@@ -393,7 +418,7 @@ export function renderSubagentResult(result, options, theme, context) {
393
418
  if (p.status === "running")
394
419
  lines.push(...runningLines(p, theme, now, frame));
395
420
  else
396
- lines.push(receiptLine(p, theme));
421
+ lines.push(receiptLine(p, theme), taskLine(p, theme));
397
422
  if (options.expanded)
398
423
  lines.push(...actionLogLines(p, theme, EXPANDED_ACTIONS_SHOWN));
399
424
  }
@@ -405,7 +430,7 @@ export function renderSubagentResult(result, options, theme, context) {
405
430
  if (i > 0)
406
431
  container.addChild(new Spacer(1));
407
432
  container.addChild(new Text(receiptLine(p, theme), 0, 0));
408
- container.addChild(new Text(theme.fg("dim", ` ${clip(p.task, 2 * TASK_PREVIEW_MAX)}`), 0, 0));
433
+ container.addChild(new Text(taskLine(p, theme), 0, 0));
409
434
  if (p.errorMessage)
410
435
  container.addChild(new Text(theme.fg("error", ` ${clip(p.errorMessage, 200)}`), 0, 0));
411
436
  const log = actionLogLines(p, theme, EXPANDED_ACTIONS_SHOWN);
@@ -423,13 +448,13 @@ export function renderSubagentResult(result, options, theme, context) {
423
448
  }
424
449
  return container;
425
450
  }
426
- // Collapsed, final: receipts + a short report preview per task.
451
+ // Collapsed, final: receipt + task + a short report preview per task.
427
452
  const lines = [];
428
453
  let truncated = false;
429
454
  for (const p of tasks) {
430
455
  if (lines.length > 0)
431
456
  lines.push("");
432
- lines.push(receiptLine(p, theme));
457
+ lines.push(receiptLine(p, theme), taskLine(p, theme));
433
458
  if (p.errorMessage)
434
459
  lines.push(theme.fg("error", ` ${clip(p.errorMessage, 160)}`));
435
460
  const report = (p.report ?? "").trim();
@@ -132,11 +132,26 @@ export interface MakeSubagentToolDeps {
132
132
  childUsage?: ChildUsageHandle;
133
133
  }
134
134
  /**
135
- * The subagent lane's resilience policy: the /go defaults, except a wall-
136
- * clock exhaustion is NOT retried (a 20-minute burn means the task is too
137
- * big — fail honestly instead of re-burning it per attempt; idle stalls stay
138
- * retryable). Exported so a test can pin the composed policy the production
139
- * path actually runs (every unit test injects runStageImpl instead).
135
+ * The subagent lane's wall-clock ceiling: two hours per child.
136
+ *
137
+ * The /go stage default (20 minutes) is sized for one pipeline step; a chat
138
+ * subagent is routinely handed a whole review, and at 20 minutes the runner
139
+ * killed three parallel reviewers mid-report and threw their findings away
140
+ * as "PARTIAL output". The wall clock is not the stall guard — the idle timer
141
+ * (4 minutes, deferred while a tool is in flight) is what catches a hung
142
+ * child — it only bounds a runaway that keeps emitting events, so it can be
143
+ * generous. Two hours is above what a child's context window sustains in
144
+ * practice, so it is effectively "let the task finish" while still bounding
145
+ * spend at roughly $20 per child on the mix that burned $2 in 20 minutes.
146
+ */
147
+ export declare const SUBAGENT_WALL_TIMEOUT_MS: number;
148
+ /**
149
+ * The subagent lane's resilience policy: the /go defaults, except the wall
150
+ * clock is the lane's own ({@link SUBAGENT_WALL_TIMEOUT_MS}) and a wall-clock
151
+ * exhaustion is NOT retried (a two-hour burn means the task is too big — fail
152
+ * honestly instead of re-burning it per attempt; idle stalls stay retryable).
153
+ * Exported so a test can pin the composed policy the production path actually
154
+ * runs (every unit test injects runStageImpl instead).
140
155
  */
141
156
  export declare function subagentRunPolicy(): ResiliencePolicy;
142
157
  /**
@@ -463,14 +463,29 @@ const parameters = Type.Object({
463
463
  })),
464
464
  });
465
465
  /**
466
- * The subagent lane's resilience policy: the /go defaults, except a wall-
467
- * clock exhaustion is NOT retried (a 20-minute burn means the task is too
468
- * big — fail honestly instead of re-burning it per attempt; idle stalls stay
469
- * retryable). Exported so a test can pin the composed policy the production
470
- * path actually runs (every unit test injects runStageImpl instead).
466
+ * The subagent lane's wall-clock ceiling: two hours per child.
467
+ *
468
+ * The /go stage default (20 minutes) is sized for one pipeline step; a chat
469
+ * subagent is routinely handed a whole review, and at 20 minutes the runner
470
+ * killed three parallel reviewers mid-report and threw their findings away
471
+ * as "PARTIAL output". The wall clock is not the stall guard — the idle timer
472
+ * (4 minutes, deferred while a tool is in flight) is what catches a hung
473
+ * child — it only bounds a runaway that keeps emitting events, so it can be
474
+ * generous. Two hours is above what a child's context window sustains in
475
+ * practice, so it is effectively "let the task finish" while still bounding
476
+ * spend at roughly $20 per child on the mix that burned $2 in 20 minutes.
477
+ */
478
+ export const SUBAGENT_WALL_TIMEOUT_MS = 2 * 60 * 60_000;
479
+ /**
480
+ * The subagent lane's resilience policy: the /go defaults, except the wall
481
+ * clock is the lane's own ({@link SUBAGENT_WALL_TIMEOUT_MS}) and a wall-clock
482
+ * exhaustion is NOT retried (a two-hour burn means the task is too big — fail
483
+ * honestly instead of re-burning it per attempt; idle stalls stay retryable).
484
+ * Exported so a test can pin the composed policy the production path actually
485
+ * runs (every unit test injects runStageImpl instead).
471
486
  */
472
487
  export function subagentRunPolicy() {
473
- return { ...DEFAULT_RESILIENCE_POLICY, retryWallTimeout: false };
488
+ return { ...DEFAULT_RESILIENCE_POLICY, wallTimeoutMs: SUBAGENT_WALL_TIMEOUT_MS, retryWallTimeout: false };
474
489
  }
475
490
  /**
476
491
  * The per-attempt telemetry adapter: one content-free error-sink row per
@@ -499,13 +514,14 @@ export function subagentTelemetry(env) {
499
514
  }
500
515
  export function makeSubagentTool(deps = {}) {
501
516
  // The default runner rides the /go pipeline's resilience wrapper, so a chat
502
- // subagent gets the same idle + wall-clock ceilings and transient-only retry
503
- // as a /go stage child (previously a hung subagent hung the tool call until
504
- // the user pressed Esc). Two subagent-specific knobs ride the wrapper:
505
- // retryWallTimeout=false (a 20-minute burn means the task is too big — fail
506
- // honestly instead of re-burning it per attempt) and per-attempt telemetry
507
- // into the error sink (a subagent failure previously left NO trace outside
508
- // the tool result text). The write-gate keys on the resolved agent's tools,
517
+ // subagent gets the same idle ceiling and transient-only retry as a /go
518
+ // stage child (previously a hung subagent hung the tool call until the user
519
+ // pressed Esc). Three subagent-specific knobs ride the wrapper: the lane's
520
+ // own two-hour wall clock (SUBAGENT_WALL_TIMEOUT_MS), retryWallTimeout=false
521
+ // (a wall burn means the task is too big — fail honestly instead of
522
+ // re-burning it per attempt) and per-attempt telemetry into the error sink
523
+ // (a subagent failure previously left NO trace outside the tool result
524
+ // text). The write-gate keys on the resolved agent's tools,
509
525
  // passed per task below — the synthetic stage id is "implement" and would
510
526
  // otherwise block retry of a read-only verifier forever.
511
527
  const run = deps.runStageImpl ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.1.5-staging.1430.1",
3
+ "version": "1.1.5-staging.1434.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -58,5 +58,5 @@
58
58
  "turndown": "^7.2.4",
59
59
  "typebox": "^1.3.15"
60
60
  },
61
- "yagniSourceSha": "7bf0c4f8abf9d498530dbe1eace8dfe901a3ad11"
61
+ "yagniSourceSha": "04f866e8c724b482892ee608ca484ae2779c55ba"
62
62
  }