@yagni-app/code-staging 1.2.3-staging.1570.1 → 1.2.3-staging.1575.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.
@@ -32,13 +32,18 @@ import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
32
32
  import type { ChildUsageHandle } from "./childUsage.js";
33
33
  import type { WorkingLineHandle } from "./workingLine.js";
34
34
  import { runStage as defaultRunStage } from "./pipeline/runner.js";
35
- import { type PipelineStage } from "./pipeline/types.js";
35
+ import { type PipelineStage, type ResiliencePolicy } from "./pipeline/types.js";
36
36
  import { type RenderTheme, type SubagentTaskProgress } from "./subagentRender.js";
37
37
  /**
38
38
  * Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
39
39
  * reason documented there: enough tools to navigate instead of flailing on a
40
40
  * single `read`. No `bash`.
41
41
  */
42
+ /**
43
+ * A consult is one read-only question, so it keeps the 20-minute wall clock
44
+ * the /go stages had before their default grew to an hour.
45
+ */
46
+ export declare const ADVISOR_RESILIENCE_POLICY: ResiliencePolicy;
42
47
  export declare const ADVISOR_TOOLS: string[];
43
48
  /** The consult's tool list under `grounding: false` — grounding read
44
49
  * stripped via the canonical filter (grounding.ts). */
@@ -40,6 +40,11 @@ import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, p
40
40
  * reason documented there: enough tools to navigate instead of flailing on a
41
41
  * single `read`. No `bash`.
42
42
  */
43
+ /**
44
+ * A consult is one read-only question, so it keeps the 20-minute wall clock
45
+ * the /go stages had before their default grew to an hour.
46
+ */
47
+ export const ADVISOR_RESILIENCE_POLICY = { ...DEFAULT_RESILIENCE_POLICY, wallTimeoutMs: 20 * 60_000 };
43
48
  export const ADVISOR_TOOLS = ["read", "grep", "find", "ls", "ask_yagni"];
44
49
  /** The consult's tool list under `grounding: false` — grounding read
45
50
  * stripped via the canonical filter (grounding.ts). */
@@ -144,7 +149,7 @@ export function makeAskAdvisorTool(opts) {
144
149
  // driver's tool call until the user presses Esc. The synthetic stage id is
145
150
  // "plan" (read-only tools, no bash), so the wrapper's write-gate never
146
151
  // blocks a retry — re-running a consult cannot double-apply anything.
147
- const runStage = opts.runStage ?? withResilience(defaultRunStage, DEFAULT_RESILIENCE_POLICY);
152
+ const runStage = opts.runStage ?? withResilience(defaultRunStage, ADVISOR_RESILIENCE_POLICY);
148
153
  return {
149
154
  name: "ask_advisor",
150
155
  label: "Ask the advisor",
@@ -78,4 +78,36 @@ export declare const DEFAULT_STAGE_ESTIMATE_MS: Record<TimedStageKind, number>;
78
78
  * so the only safe place to stop is before it begins.
79
79
  */
80
80
  export declare function stageWontFit(budget: RunBudget, kind: TimedStageKind, expectedMs: number, nowMs?: number): string | null;
81
+ /**
82
+ * The shortest wall clock any /go stage gets: the old fixed limit. A run close
83
+ * to its deadline keeps this much rather than a stage being cut off sooner than
84
+ * it ever was.
85
+ */
86
+ export declare const STAGE_WALL_FLOOR_MS: number;
87
+ /** Time the verify gate is allowed after the last write, on top of the stage estimates. */
88
+ export declare const VERIFY_ALLOWANCE_MS: number;
89
+ /**
90
+ * What must still run after a stage stops: a review round, one fix, and the
91
+ * verify gate. Held back from the stage's wall clock so a long stage still
92
+ * leaves the run time to review what it wrote.
93
+ */
94
+ export declare const STAGE_WALL_RESERVE_MS: number;
95
+ /**
96
+ * Where a stage's wall clock came from: the build's remaining time, the
97
+ * 20-minute floor because the deadline was close, or the no-deadline default.
98
+ */
99
+ export type StageWallSource = "deadline" | "floor" | "default";
100
+ /**
101
+ * The wall clock for a stage spawning at `nowMs`. With a deadline (sandbox and
102
+ * mission runs) a working stage gets the build's remaining time less
103
+ * {@link STAGE_WALL_RESERVE_MS}, never under {@link STAGE_WALL_FLOOR_MS}: the
104
+ * idle timer is the check for a stuck model, and stopping a live stage at a
105
+ * fixed limit throws away work the run had time for. Without a deadline
106
+ * (interactive /go, where a person can abort) `fallbackMs` applies. The
107
+ * source rides the stage's attempt record.
108
+ */
109
+ export declare function stageWall(budget: RunBudget, fallbackMs: number, nowMs?: number): {
110
+ ms: number;
111
+ source: StageWallSource;
112
+ };
81
113
  //# sourceMappingURL=budget.d.ts.map
@@ -107,4 +107,33 @@ export function stageWontFit(budget, kind, expectedMs, nowMs = Date.now()) {
107
107
  return null;
108
108
  return `not enough time left for a ${kind} stage (${Math.max(0, Math.round(leftMs / 1000))}s left, expected ${Math.round(expectedMs / 1000)}s)`;
109
109
  }
110
+ /**
111
+ * The shortest wall clock any /go stage gets: the old fixed limit. A run close
112
+ * to its deadline keeps this much rather than a stage being cut off sooner than
113
+ * it ever was.
114
+ */
115
+ export const STAGE_WALL_FLOOR_MS = 20 * 60_000;
116
+ /** Time the verify gate is allowed after the last write, on top of the stage estimates. */
117
+ export const VERIFY_ALLOWANCE_MS = 5 * 60_000;
118
+ /**
119
+ * What must still run after a stage stops: a review round, one fix, and the
120
+ * verify gate. Held back from the stage's wall clock so a long stage still
121
+ * leaves the run time to review what it wrote.
122
+ */
123
+ export const STAGE_WALL_RESERVE_MS = DEFAULT_STAGE_ESTIMATE_MS.review + DEFAULT_STAGE_ESTIMATE_MS.fix + VERIFY_ALLOWANCE_MS;
124
+ /**
125
+ * The wall clock for a stage spawning at `nowMs`. With a deadline (sandbox and
126
+ * mission runs) a working stage gets the build's remaining time less
127
+ * {@link STAGE_WALL_RESERVE_MS}, never under {@link STAGE_WALL_FLOOR_MS}: the
128
+ * idle timer is the check for a stuck model, and stopping a live stage at a
129
+ * fixed limit throws away work the run had time for. Without a deadline
130
+ * (interactive /go, where a person can abort) `fallbackMs` applies. The
131
+ * source rides the stage's attempt record.
132
+ */
133
+ export function stageWall(budget, fallbackMs, nowMs = Date.now()) {
134
+ if (budget.deadlineMs === undefined)
135
+ return { ms: fallbackMs, source: "default" };
136
+ const leftMs = budget.deadlineMs - nowMs - STAGE_WALL_RESERVE_MS;
137
+ return leftMs > STAGE_WALL_FLOOR_MS ? { ms: leftMs, source: "deadline" } : { ms: STAGE_WALL_FLOOR_MS, source: "floor" };
138
+ }
110
139
  //# sourceMappingURL=budget.js.map
@@ -26,10 +26,11 @@
26
26
  */
27
27
  import { type RunBudget } from "./budget.js";
28
28
  import { type MissionInputs } from "./mission.js";
29
+ import { type ResilienceAttemptRecord, type RunStageFn } from "./resilience.js";
29
30
  import { runStage as defaultRunStage } from "./runner.js";
30
31
  import { type VerifyOutcome, type WorkstreamCheckResult, type WorkstreamCheckTarget } from "./verify.js";
31
32
  import { snapshotWorkspace as defaultSnapshotWorkspace } from "./workspace.js";
32
- import { type CheckpointStore, type FanoutBudgetVerdict, type FanoutMode, type PipelineStage, type JsonEvent, type PipelineProgress, type PipelineResult, type ResumePlan, type StageId, type StageResult, type StageTag } from "./types.js";
33
+ import { type CheckpointStore, type FanoutBudgetVerdict, type FanoutMode, type PipelineStage, type JsonEvent, type PipelineProgress, type PipelineResult, type ResiliencePolicy, type ResumePlan, type StageId, type StageResult, type StageTag } from "./types.js";
33
34
  export interface RunPipelineDeps {
34
35
  cwd: string;
35
36
  signal?: AbortSignal;
@@ -174,5 +175,19 @@ export declare class PipelineStageError extends Error {
174
175
  readonly partial: StageResult[];
175
176
  constructor(stageId: string, message: string, partial: StageResult[]);
176
177
  }
178
+ /**
179
+ * The resilience policy for a run's stages. With a deadline, a stage that
180
+ * spent its wall clock spent the build's time: a retry would only get the
181
+ * floor and run past the deadline, so it fails instead of retrying.
182
+ */
183
+ export declare function stageRunnerPolicy(budget: RunBudget): ResiliencePolicy;
184
+ /**
185
+ * The pipeline's default stage runner: `base` under the R1 resilience wrapper
186
+ * with {@link stageRunnerPolicy}, each stage's wall clock taken from the run's
187
+ * deadline when it spawns (budget.ts#stageWall). A run with no deadline keeps
188
+ * the policy's fixed wall clock. Exported so a test can pin the wall a stage
189
+ * actually gets.
190
+ */
191
+ export declare function resilientStageRunner(base: RunStageFn, budget: RunBudget, clock: () => number, telemetry: (rec: ResilienceAttemptRecord) => void): RunStageFn;
177
192
  export declare function runPipeline(ticket: string, deps: RunPipelineDeps): Promise<PipelineResult>;
178
193
  //# sourceMappingURL=orchestrator.d.ts.map
@@ -24,7 +24,7 @@
24
24
  * (an aborted/crashed child has empty output, which otherwise parses to zero
25
25
  * findings — i.e. a false "reviewed and clean").
26
26
  */
27
- import { addRunUsage, aggregateRunUsage, DEFAULT_RUN_BUDGET, DEFAULT_STAGE_ESTIMATE_MS, EMPTY_RUN_USAGE, exceedsBudget, stageWontFit, } from "./budget.js";
27
+ import { addRunUsage, aggregateRunUsage, DEFAULT_RUN_BUDGET, DEFAULT_STAGE_ESTIMATE_MS, EMPTY_RUN_USAGE, exceedsBudget, stageWall, stageWontFit, } from "./budget.js";
28
28
  import { attributeFindings, checkerFindings, composeFixNote, composeResidue, composeSynthesizerInput, mergeFindings, parseOpenFindings, renderFindings, stripOpenFindings, } from "./checker.js";
29
29
  import { auditClaims, parsePartition } from "./fanout.js";
30
30
  import { extractFindingsBlock, hasBlockingFindings, parseFindings, shouldStop, unionFindings } from "./findings.js";
@@ -188,6 +188,27 @@ async function mapWithConcurrencyLimit(items, concurrency, fn) {
188
188
  await Promise.all(workers);
189
189
  return results;
190
190
  }
191
+ /**
192
+ * The resilience policy for a run's stages. With a deadline, a stage that
193
+ * spent its wall clock spent the build's time: a retry would only get the
194
+ * floor and run past the deadline, so it fails instead of retrying.
195
+ */
196
+ export function stageRunnerPolicy(budget) {
197
+ return budget.deadlineMs === undefined ? DEFAULT_RESILIENCE_POLICY : { ...DEFAULT_RESILIENCE_POLICY, retryWallTimeout: false };
198
+ }
199
+ /**
200
+ * The pipeline's default stage runner: `base` under the R1 resilience wrapper
201
+ * with {@link stageRunnerPolicy}, each stage's wall clock taken from the run's
202
+ * deadline when it spawns (budget.ts#stageWall). A run with no deadline keeps
203
+ * the policy's fixed wall clock. Exported so a test can pin the wall a stage
204
+ * actually gets.
205
+ */
206
+ export function resilientStageRunner(base, budget, clock, telemetry) {
207
+ return withResilience(base, stageRunnerPolicy(budget), {
208
+ telemetry,
209
+ wallFor: () => stageWall(budget, DEFAULT_RESILIENCE_POLICY.wallTimeoutMs, clock()),
210
+ });
211
+ }
191
212
  export async function runPipeline(ticket, deps) {
192
213
  const log = (event, data) => deps.logger?.(event, data);
193
214
  // R1: wrap the real child runner with in-loop resilience (idle/wall timeout +
@@ -195,10 +216,11 @@ export async function runPipeline(ticket, deps) {
195
216
  // across its up-to-8 spawns. A caller that injects its own `runStage` (the unit
196
217
  // tests, a custom lane) bypasses the wrapper untouched. Per-attempt telemetry
197
218
  // rides the existing logger seam, so resume + the checkpoint journal are unaffected.
219
+ // With a deadline (sandbox / mission runs) each stage's wall clock is the
220
+ // time left less a reserve for review, a fix and verify, so a live stage is
221
+ // not killed at a fixed limit the run had room to exceed.
198
222
  const runStageFn = deps.runStage ??
199
- withResilience(defaultRunStage, DEFAULT_RESILIENCE_POLICY, {
200
- telemetry: (rec) => log("resilience_attempt", rec),
201
- });
223
+ resilientStageRunner(defaultRunStage, deps.budget ?? DEFAULT_RUN_BUDGET, deps.now ?? Date.now, (rec) => log("resilience_attempt", rec));
202
224
  const snapshotFn = deps.snapshotWorkspace ?? defaultSnapshotWorkspace;
203
225
  const runVerifyFn = deps.runVerify ?? makeRunVerify();
204
226
  const runWorkstreamCheckFn = deps.runWorkstreamCheck ?? makeWorkstreamCheck();
@@ -27,6 +27,7 @@
27
27
  * Pure-ish: all impurity (timers, sleep, clock, randomness) sits behind injectable
28
28
  * seams so the wrapper is unit-tested without real waits.
29
29
  */
30
+ import type { StageWallSource } from "./budget.js";
30
31
  import type { RunStageDeps } from "./runner.js";
31
32
  import { type PipelineStage, type ResiliencePolicy, type ReviewLens, type StageResult } from "./types.js";
32
33
  export type { ResiliencePolicy } from "./types.js";
@@ -54,6 +55,13 @@ export interface ResilienceAttemptRecord {
54
55
  /** 1-based attempt number. */
55
56
  attempt: number;
56
57
  outcome: "ok" | "transient" | "fatal" | "timeout" | "aborted";
58
+ /** The wall clock this attempt was given (ms). */
59
+ wallTimeoutMs?: number;
60
+ /**
61
+ * Where that wall clock came from, when the caller said: the build's time
62
+ * left, the floor because the deadline was close, or the no-deadline default.
63
+ */
64
+ wallSource?: StageWallSource;
57
65
  /** Which timer fired, when the attempt timed out. */
58
66
  timeoutKind?: "idle" | "wall";
59
67
  /** On a timeout: ms the attempt spent with a tool running (the rest went to the model). */
@@ -76,6 +84,16 @@ export interface ResilienceSeams {
76
84
  }
77
85
  export interface WithResilienceOpts {
78
86
  telemetry?: (rec: ResilienceAttemptRecord) => void;
87
+ /**
88
+ * The wall clock for one attempt, asked when the attempt spawns so a later
89
+ * stage (or a retry) gets the time actually left. Absent, every attempt gets
90
+ * `policy.wallTimeoutMs`. The idle window always comes from the policy.
91
+ * `source` rides the attempt record as `wallSource`.
92
+ */
93
+ wallFor?: (stage: PipelineStage) => {
94
+ ms: number;
95
+ source?: StageWallSource;
96
+ };
79
97
  seams?: ResilienceSeams;
80
98
  }
81
99
  /**
@@ -163,7 +163,9 @@ export function withResilience(base, policy, opts = {}) {
163
163
  }
164
164
  fireTimeout("idle");
165
165
  };
166
- const wallTimer = setTimeout(() => fireTimeout("wall"), policy.wallTimeoutMs);
166
+ const wall = opts.wallFor?.(stage);
167
+ const wallTimeoutMs = wall?.ms ?? policy.wallTimeoutMs;
168
+ const wallTimer = setTimeout(() => fireTimeout("wall"), wallTimeoutMs);
167
169
  wallTimer.unref?.();
168
170
  armIdle();
169
171
  const originalOnEvent = deps.onEvent;
@@ -207,6 +209,8 @@ export function withResilience(base, policy, opts = {}) {
207
209
  ...(deps.taskIndex !== undefined ? { taskIndex: deps.taskIndex } : {}),
208
210
  attempt,
209
211
  outcome,
212
+ wallTimeoutMs,
213
+ ...(wall?.source ? { wallSource: wall.source } : {}),
210
214
  ...(timedOut && timeoutKind ? { timeoutKind } : {}),
211
215
  ...(timedOut ? { toolMs, modelBound } : {}),
212
216
  exitCode: last.exitCode,
@@ -229,7 +233,7 @@ export function withResilience(base, policy, opts = {}) {
229
233
  const transient = classifyTransient(last, timedOut);
230
234
  // A wall-clock exhaustion is a "task too big" signal, not a stall: a
231
235
  // lane that opts out (subagents) fails honestly instead of re-burning
232
- // the same 20 minutes per attempt. Idle stalls stay retryable.
236
+ // the same wall clock per attempt. Idle stalls stay retryable.
233
237
  const wallNotRetryable = timedOut && timeoutKind === "wall" && policy.retryWallTimeout === false;
234
238
  // No double-apply: a write stage that already started working is never re-run.
235
239
  const blockedByWriteGate = isWriteStage && sawAnyEvent;
@@ -527,18 +527,21 @@ export interface ResiliencePolicy {
527
527
  /**
528
528
  * Whether a WALL-clock exhaustion is retryable. Default true (the /go
529
529
  * doctrine: a wedged stage is worth another attempt). A lane whose children
530
- * run read-only, fast work (subagents) sets false: a 20-minute burn means
531
- * the task is too big, and re-running it just multiplies the spend before
530
+ * run read-only, fast work (subagents) sets false: a full wall-clock burn
531
+ * means the task is too big, and re-running it just multiplies the spend before
532
532
  * the same honest fail. Idle-stall timeouts stay retryable either way.
533
533
  */
534
534
  retryWallTimeout?: boolean;
535
535
  }
536
536
  /**
537
537
  * Default resilience policy. Generous timeouts so a legitimately long but live
538
- * stage is never killed (the idle window sits well under the 5-min run heartbeat,
539
- * and the wall window bounds a truly wedged child); three attempts with short,
540
- * jittered backoff so a single transient blip across the up-to-8 child spawns no
541
- * longer terminates the whole run.
538
+ * stage is never killed: the idle window (well under the 5-min run heartbeat) is
539
+ * the check for a stuck model, and the wall window only bounds a runaway that
540
+ * keeps emitting events. The wall is 60 minutes for an interactive /go, where a
541
+ * person can abort; a run with a deadline replaces it per stage with the time
542
+ * left (budget.ts#stageWall). Three attempts with short, jittered
543
+ * backoff so a single transient blip across the up-to-8 child spawns no longer
544
+ * terminates the whole run.
542
545
  */
543
546
  export declare const DEFAULT_RESILIENCE_POLICY: ResiliencePolicy;
544
547
  /**
@@ -17,14 +17,17 @@ export const MAX_REVIEW_ROUNDS = 3;
17
17
  export const MAX_FIX_TURNS = 3;
18
18
  /**
19
19
  * Default resilience policy. Generous timeouts so a legitimately long but live
20
- * stage is never killed (the idle window sits well under the 5-min run heartbeat,
21
- * and the wall window bounds a truly wedged child); three attempts with short,
22
- * jittered backoff so a single transient blip across the up-to-8 child spawns no
23
- * longer terminates the whole run.
20
+ * stage is never killed: the idle window (well under the 5-min run heartbeat) is
21
+ * the check for a stuck model, and the wall window only bounds a runaway that
22
+ * keeps emitting events. The wall is 60 minutes for an interactive /go, where a
23
+ * person can abort; a run with a deadline replaces it per stage with the time
24
+ * left (budget.ts#stageWall). Three attempts with short, jittered
25
+ * backoff so a single transient blip across the up-to-8 child spawns no longer
26
+ * terminates the whole run.
24
27
  */
25
28
  export const DEFAULT_RESILIENCE_POLICY = {
26
29
  idleTimeoutMs: 4 * 60_000,
27
- wallTimeoutMs: 20 * 60_000,
30
+ wallTimeoutMs: 60 * 60_000,
28
31
  maxAttempts: 3,
29
32
  backoffBaseMs: 1_000,
30
33
  backoffMaxMs: 30_000,
@@ -134,8 +134,9 @@ export interface MakeSubagentToolDeps {
134
134
  /**
135
135
  * The subagent lane's wall-clock ceiling: two hours per child.
136
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
137
+ * The /go stage default (20 minutes when this was set, an hour since) is
138
+ * sized for one pipeline step; a chat subagent is routinely handed a whole
139
+ * review, and at 20 minutes the runner
139
140
  * killed three parallel reviewers mid-report and threw their findings away
140
141
  * as "PARTIAL output". The wall clock is not the stall guard — the idle timer
141
142
  * (4 minutes, deferred while a tool is in flight) is what catches a hung
@@ -465,8 +465,9 @@ const parameters = Type.Object({
465
465
  /**
466
466
  * The subagent lane's wall-clock ceiling: two hours per child.
467
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
468
+ * The /go stage default (20 minutes when this was set, an hour since) is
469
+ * sized for one pipeline step; a chat subagent is routinely handed a whole
470
+ * review, and at 20 minutes the runner
470
471
  * killed three parallel reviewers mid-report and threw their findings away
471
472
  * as "PARTIAL output". The wall clock is not the stall guard — the idle timer
472
473
  * (4 minutes, deferred while a tool is in flight) is what catches a hung
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.2.3-staging.1570.1",
3
+ "version": "1.2.3-staging.1575.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": "cf9eadbc3cdaaa5eb222270223c092ce900fd9cc"
61
+ "yagniSourceSha": "ce56b0f74cd9190953e1fc22901d340ff765793a"
62
62
  }