@yagni-app/code-staging 1.2.14-staging.2204.1 → 1.2.14-staging.2213.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.
@@ -238,6 +238,18 @@ export declare class PipelineStageError extends Error {
238
238
  readonly partial: StageResult[];
239
239
  constructor(stageId: string, message: string, partial: StageResult[]);
240
240
  }
241
+ /**
242
+ * The silent-402 landmine: when the workspace runs out of credits mid-run, the
243
+ * proxy 402s every model request, the child pi exits 0 with an EMPTY transcript,
244
+ * and the run otherwise dies as a confusing `no_changes` / empty-handoff / false
245
+ * "clean" stop with the real cause buried in stderr. Detect it and stop with an
246
+ * actionable message instead. Gated on the stage having produced no real output
247
+ * (or having failed outright) so a healthy stage whose stderr happens to contain
248
+ * a matching token can never trip it. A Run that spent its own budget stops the
249
+ * same way, with its own message. The session lane's turn reads it too
250
+ * (`sessionLane.ts` `turnStop`).
251
+ */
252
+ export declare function creditExhaustionReason(r: StageResult): string | undefined;
241
253
  /**
242
254
  * The resilience policy for a run's stages. With a deadline, a stage that
243
255
  * spent its wall clock spent the build's time: a retry would only get the
@@ -155,9 +155,10 @@ const RUN_REQUEST_LIMIT_RE = /run_request_limit_exhausted|This Run has used its
155
155
  * actionable message instead. Gated on the stage having produced no real output
156
156
  * (or having failed outright) so a healthy stage whose stderr happens to contain
157
157
  * a matching token can never trip it. A Run that spent its own budget stops the
158
- * same way, with its own message.
158
+ * same way, with its own message. The session lane's turn reads it too
159
+ * (`sessionLane.ts` `turnStop`).
159
160
  */
160
- function creditExhaustionReason(r) {
161
+ export function creditExhaustionReason(r) {
161
162
  if (r.finalOutput.trim().length > 0 && !isFailed(r))
162
163
  return undefined;
163
164
  const hay = `${r.errorMessage ?? ""}\n${r.stderr}`;
@@ -118,9 +118,16 @@ export declare function parseSessionReport(text: string | null, known?: Readonly
118
118
  /** The report's checks and its words on `result.json`: the model's own account, never the host's proof. */
119
119
  export declare function withReport(attempt: AttemptResult, report: SessionReport | null): AttemptResult;
120
120
  /**
121
- * The turn's stop as the result names it: aborted, its clock, a child that
122
- * failed, a writing turn whose tools mostly errored (the cloud lane's health
123
- * gate), else clean.
121
+ * The end of a turn's child's stderr as a note carries it: its last lines
122
+ * without the CLI's own `Skills:` line, secrets scrubbed, clipped; empty when
123
+ * nothing else is left. The child's stderr is the parent's alone, so without
124
+ * this a turn that died said nothing on the CLI's stderr either.
125
+ */
126
+ export declare function childStderrTail(stderr: string): string;
127
+ /**
128
+ * The turn's stop as the result names it: aborted, its clock, a model proxy
129
+ * that refused it for what the run may spend, a child that failed, a writing
130
+ * turn whose tools mostly errored (the cloud lane's health gate), else clean.
124
131
  */
125
132
  export declare function turnStop(run: StageResult, writes: boolean, aborted: boolean): {
126
133
  stop: StopReason;
@@ -44,9 +44,11 @@ import { scanChange } from "./changeScan.js";
44
44
  import { completeCloudSession, RESULT_FILE_NAME, } from "./cloud.js";
45
45
  import { finishExec } from "./finish.js";
46
46
  import { MISSION_CHECKS_FILE_NAME, readMissionChecksFile } from "./missionChecks.js";
47
+ import { creditExhaustionReason } from "./orchestrator.js";
47
48
  import { WORK_A_TICKET_SKILL } from "./personas.js";
48
49
  import { extractPlanJson } from "./planOutput.js";
49
50
  import { runStage as defaultRunStage } from "./runner.js";
51
+ import { scrubSecrets } from "./scrubSecrets.js";
50
52
  import { DECISIONS_READ_TOOL, PLAN_JSON_CONTRACT } from "./stages.js";
51
53
  import { STAGE_WRAP_UP_GRACE_MS } from "./stageClock.js";
52
54
  import { MIN_TOOL_CALLS_FOR_HEALTH, TOOL_ERROR_FAIL_RATE, } from "./types.js";
@@ -340,10 +342,28 @@ export function withReport(attempt, report) {
340
342
  notes,
341
343
  };
342
344
  }
345
+ /** The most of a child's stderr a failed turn's note carries. */
346
+ const STDERR_NOTE_LINES = 8;
347
+ const STDERR_NOTE_CHARS = 600;
343
348
  /**
344
- * The turn's stop as the result names it: aborted, its clock, a child that
345
- * failed, a writing turn whose tools mostly errored (the cloud lane's health
346
- * gate), else clean.
349
+ * The end of a turn's child's stderr as a note carries it: its last lines
350
+ * without the CLI's own `Skills:` line, secrets scrubbed, clipped; empty when
351
+ * nothing else is left. The child's stderr is the parent's alone, so without
352
+ * this a turn that died said nothing on the CLI's stderr either.
353
+ */
354
+ export function childStderrTail(stderr) {
355
+ const lines = scrubSecrets(stderr)
356
+ .split(/\r?\n/)
357
+ .map((line) => line.trim())
358
+ .filter((line) => line && !line.startsWith("Skills: "))
359
+ .slice(-STDERR_NOTE_LINES)
360
+ .join(" / ");
361
+ return lines.length > STDERR_NOTE_CHARS ? `...${lines.slice(-(STDERR_NOTE_CHARS - 3))}` : lines;
362
+ }
363
+ /**
364
+ * The turn's stop as the result names it: aborted, its clock, a model proxy
365
+ * that refused it for what the run may spend, a child that failed, a writing
366
+ * turn whose tools mostly errored (the cloud lane's health gate), else clean.
347
367
  */
348
368
  export function turnStop(run, writes, aborted) {
349
369
  if (aborted || run.stopReason === "aborted")
@@ -351,8 +371,19 @@ export function turnStop(run, writes, aborted) {
351
371
  if (run.timedOut) {
352
372
  return { stop: "budget_exceeded", note: "The turn ran out of the run's time and was stopped." };
353
373
  }
374
+ // The proxy's 402 or 429 for the workspace's credits or the run's own
375
+ // budget, in the pipeline's words, which the host reads as a spend stop
376
+ // (`spendStop.ts`) and hands to a person at once. Named only as an error,
377
+ // the host retried it twice into the same refusal and published the half
378
+ // build for review (staging NEARM-627, 2026-10-30).
379
+ const spent = creditExhaustionReason(run);
380
+ if (spent)
381
+ return { stop: "failed", note: `The session stopped: ${spent}` };
354
382
  if (run.exitCode !== 0 || run.stopReason === "error") {
355
- const why = run.errorMessage ? `: ${run.errorMessage.slice(0, 300)}` : "";
383
+ const said = scrubSecrets(run.errorMessage?.trim() ?? "") || childStderrTail(run.stderr);
384
+ const why = said
385
+ ? `: ${said.slice(0, STDERR_NOTE_CHARS)}`
386
+ : ` (exit ${run.exitCode}, nothing on its stderr)`;
356
387
  return { stop: "failed", note: `The session's turn ended on an error${why}.` };
357
388
  }
358
389
  if (writes &&
@@ -467,6 +498,10 @@ export async function runSessionTurn(input, deps = {}) {
467
498
  const stop = turnStop(run, writes, input.signal?.aborted === true);
468
499
  if (stop.note)
469
500
  notes.push(stop.note);
501
+ // On the CLI's own stderr too, whose tail the host logs with a session
502
+ // that ended without its work.
503
+ if (stop.stop === "failed" && stop.note)
504
+ input.writeErr(stop.note);
470
505
  const planned = input.stage !== "plan" || planText.trim().length > 0;
471
506
  ran = {
472
507
  result: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.2.14-staging.2204.1",
3
+ "version": "1.2.14-staging.2213.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": "481a9f9ced04d8756c322d0aa3edfe7420f6b957"
61
+ "yagniSourceSha": "4cc9f6388171d4967fbf78689819f238baa1f5f8"
62
62
  }