@yagni-app/code-staging 1.2.3-staging.1621.1 → 1.2.3-staging.1632.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.
@@ -79,9 +79,9 @@ export declare const DEFAULT_STAGE_ESTIMATE_MS: Record<TimedStageKind, number>;
79
79
  */
80
80
  export declare function stageWontFit(budget: RunBudget, kind: TimedStageKind, expectedMs: number, nowMs?: number): string | null;
81
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.
82
+ * The shortest wall clock a /go stage gets while the build has time for it: the
83
+ * old fixed limit. A run close to its deadline keeps this much, but never more
84
+ * than the time actually left, since the host stops the run at the deadline.
85
85
  */
86
86
  export declare const STAGE_WALL_FLOOR_MS: number;
87
87
  /** Time the verify gate is allowed after the last write, on top of the stage estimates. */
@@ -94,7 +94,8 @@ export declare const VERIFY_ALLOWANCE_MS: number;
94
94
  export declare const STAGE_WALL_RESERVE_MS: number;
95
95
  /**
96
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.
97
+ * 20-minute floor (or the less time left) because the deadline was close, or
98
+ * the no-deadline default.
98
99
  */
99
100
  export type StageWallSource = "deadline" | "floor" | "default";
100
101
  /**
@@ -102,7 +103,10 @@ export type StageWallSource = "deadline" | "floor" | "default";
102
103
  * mission runs) a working stage gets the build's remaining time less
103
104
  * {@link STAGE_WALL_RESERVE_MS}, never under {@link STAGE_WALL_FLOOR_MS}: the
104
105
  * 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
+ * fixed limit throws away work the run had time for. The floor is capped at the
107
+ * time left before the deadline (zero once it has passed): a wall past the
108
+ * deadline would only let the host kill the stage partway through, with no
109
+ * stop the run can report. Without a deadline
106
110
  * (interactive /go, where a person can abort) `fallbackMs` applies. The
107
111
  * source rides the stage's attempt record.
108
112
  */
@@ -108,9 +108,9 @@ export function stageWontFit(budget, kind, expectedMs, nowMs = Date.now()) {
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
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.
111
+ * The shortest wall clock a /go stage gets while the build has time for it: the
112
+ * old fixed limit. A run close to its deadline keeps this much, but never more
113
+ * than the time actually left, since the host stops the run at the deadline.
114
114
  */
115
115
  export const STAGE_WALL_FLOOR_MS = 20 * 60_000;
116
116
  /** Time the verify gate is allowed after the last write, on top of the stage estimates. */
@@ -126,7 +126,10 @@ export const STAGE_WALL_RESERVE_MS = DEFAULT_STAGE_ESTIMATE_MS.review + DEFAULT_
126
126
  * mission runs) a working stage gets the build's remaining time less
127
127
  * {@link STAGE_WALL_RESERVE_MS}, never under {@link STAGE_WALL_FLOOR_MS}: the
128
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
129
+ * fixed limit throws away work the run had time for. The floor is capped at the
130
+ * time left before the deadline (zero once it has passed): a wall past the
131
+ * deadline would only let the host kill the stage partway through, with no
132
+ * stop the run can report. Without a deadline
130
133
  * (interactive /go, where a person can abort) `fallbackMs` applies. The
131
134
  * source rides the stage's attempt record.
132
135
  */
@@ -134,6 +137,8 @@ export function stageWall(budget, fallbackMs, nowMs = Date.now()) {
134
137
  if (budget.deadlineMs === undefined)
135
138
  return { ms: fallbackMs, source: "default" };
136
139
  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" };
140
+ if (leftMs > STAGE_WALL_FLOOR_MS)
141
+ return { ms: leftMs, source: "deadline" };
142
+ return { ms: Math.min(STAGE_WALL_FLOOR_MS, Math.max(0, budget.deadlineMs - nowMs)), source: "floor" };
138
143
  }
139
144
  //# sourceMappingURL=budget.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * PURE continuation rules for a headless mission run (`--continue-file`).
3
+ *
4
+ * When a cloud build stops before it finishes (a timeout, a crash), the host
5
+ * starts a new build from the earlier attempt's saved change: the sandbox work
6
+ * tree already holds that change as UNCOMMITTED edits on top of HEAD (the base
7
+ * commit), and the host says where the earlier attempt stopped. Two rules:
8
+ *
9
+ * - the build's baseline is the CLEAN base, so the saved change counts as this
10
+ * run's own change (an implement that only confirms finished work is not
11
+ * `no_changes`, and the verify gate scopes to the whole diff against HEAD);
12
+ * - an attempt that stopped in review, fix or verify re-enters the review→fix
13
+ * loop at the next round, through the same `resume_loop` seam a resumed
14
+ * interactive run uses. An attempt that stopped in implement (or before)
15
+ * runs implement again on the approved plan.
16
+ *
17
+ * The file is the host's contract, so anything unreadable is refused here and
18
+ * the caller runs a normal mission instead of crashing.
19
+ */
20
+ import { type MissionInputs } from "./mission.js";
21
+ import { type ResumePlan } from "./types.js";
22
+ /** Where the earlier attempt stopped, as the host reports it. */
23
+ export type ContinuationStage = "implement" | "review" | "fix" | "verify";
24
+ /** The parsed `--continue-file` (contract version 1). */
25
+ export interface ContinuationInputs {
26
+ /** The stage the earlier attempt was in when it stopped; null when unknown. */
27
+ stageAtStop: ContinuationStage | null;
28
+ /** Review rounds the earlier attempt finished. */
29
+ reviewRoundsDone: number;
30
+ /** The host's short code for why the earlier attempt stopped. */
31
+ priorStop: string;
32
+ }
33
+ /** Parse and validate the continue file's text; a reason when it cannot be used. */
34
+ export declare function parseContinuation(raw: string): {
35
+ ok: true;
36
+ value: ContinuationInputs;
37
+ } | {
38
+ ok: false;
39
+ reason: string;
40
+ };
41
+ /** True when the earlier attempt got past implement, so the run re-enters the review loop. */
42
+ export declare function continuesInLoop(continuation: ContinuationInputs): boolean;
43
+ /**
44
+ * The round-1 review handoff for a continuation that skips implement. The live
45
+ * run hands review the implement summary (plus the pinned design context); a
46
+ * continuation has no summary, so it says where the change is instead and pins
47
+ * the same design context.
48
+ */
49
+ export declare function continuationReviewInput(continuation: ContinuationInputs, mission: MissionInputs | undefined): string;
50
+ /**
51
+ * The resume plan that re-enters the review→fix loop for a continuation that
52
+ * stopped in review, fix or verify; undefined when implement must run again.
53
+ * The round continues the earlier attempt's count, capped at the last round so
54
+ * a continuation always gets one review.
55
+ */
56
+ export declare function continuationResumePlan(continuation: ContinuationInputs, ctx: {
57
+ ticket: string;
58
+ runId?: string;
59
+ mission?: MissionInputs;
60
+ }): ResumePlan | undefined;
61
+ //# sourceMappingURL=continuation.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * PURE continuation rules for a headless mission run (`--continue-file`).
3
+ *
4
+ * When a cloud build stops before it finishes (a timeout, a crash), the host
5
+ * starts a new build from the earlier attempt's saved change: the sandbox work
6
+ * tree already holds that change as UNCOMMITTED edits on top of HEAD (the base
7
+ * commit), and the host says where the earlier attempt stopped. Two rules:
8
+ *
9
+ * - the build's baseline is the CLEAN base, so the saved change counts as this
10
+ * run's own change (an implement that only confirms finished work is not
11
+ * `no_changes`, and the verify gate scopes to the whole diff against HEAD);
12
+ * - an attempt that stopped in review, fix or verify re-enters the review→fix
13
+ * loop at the next round, through the same `resume_loop` seam a resumed
14
+ * interactive run uses. An attempt that stopped in implement (or before)
15
+ * runs implement again on the approved plan.
16
+ *
17
+ * The file is the host's contract, so anything unreadable is refused here and
18
+ * the caller runs a normal mission instead of crashing.
19
+ */
20
+ import { missionDesignContext } from "./mission.js";
21
+ import { MAX_REVIEW_ROUNDS } from "./types.js";
22
+ const CONTINUATION_STAGES = ["implement", "review", "fix", "verify"];
23
+ /** Parse and validate the continue file's text; a reason when it cannot be used. */
24
+ export function parseContinuation(raw) {
25
+ let data;
26
+ try {
27
+ data = JSON.parse(raw);
28
+ }
29
+ catch {
30
+ return { ok: false, reason: "it is not JSON" };
31
+ }
32
+ if (!data || typeof data !== "object" || Array.isArray(data))
33
+ return { ok: false, reason: "it is not a JSON object" };
34
+ const obj = data;
35
+ if (obj.version !== 1)
36
+ return { ok: false, reason: `unknown version ${JSON.stringify(obj.version)}` };
37
+ const stage = obj.stageAtStop;
38
+ if (stage !== null && !CONTINUATION_STAGES.includes(stage)) {
39
+ return { ok: false, reason: `unknown stageAtStop ${JSON.stringify(stage)}` };
40
+ }
41
+ const rounds = obj.reviewRoundsDone;
42
+ if (typeof rounds !== "number" || !Number.isInteger(rounds) || rounds < 0) {
43
+ return { ok: false, reason: `reviewRoundsDone is not a whole number (${JSON.stringify(rounds)})` };
44
+ }
45
+ const priorStop = typeof obj.priorStop === "string" ? obj.priorStop.trim() : "";
46
+ return { ok: true, value: { stageAtStop: stage, reviewRoundsDone: rounds, priorStop } };
47
+ }
48
+ /** True when the earlier attempt got past implement, so the run re-enters the review loop. */
49
+ export function continuesInLoop(continuation) {
50
+ const stage = continuation.stageAtStop;
51
+ return stage === "review" || stage === "fix" || stage === "verify";
52
+ }
53
+ /**
54
+ * The round-1 review handoff for a continuation that skips implement. The live
55
+ * run hands review the implement summary (plus the pinned design context); a
56
+ * continuation has no summary, so it says where the change is instead and pins
57
+ * the same design context.
58
+ */
59
+ export function continuationReviewInput(continuation, mission) {
60
+ const why = continuation.priorStop ? ` (${continuation.priorStop})` : "";
61
+ const summary = `No implement summary: this build continues an earlier attempt that stopped during its ${continuation.stageAtStop ?? "unknown"} stage${why}. ` +
62
+ "That attempt's change is already in the working tree as uncommitted edits on top of HEAD. " +
63
+ "Review the whole change against HEAD (`git diff HEAD`, plus any untracked files `git status` lists).";
64
+ const design = missionDesignContext(mission);
65
+ return design ? `${summary}\n\n${design}` : summary;
66
+ }
67
+ /**
68
+ * The resume plan that re-enters the review→fix loop for a continuation that
69
+ * stopped in review, fix or verify; undefined when implement must run again.
70
+ * The round continues the earlier attempt's count, capped at the last round so
71
+ * a continuation always gets one review.
72
+ */
73
+ export function continuationResumePlan(continuation, ctx) {
74
+ if (!continuesInLoop(continuation))
75
+ return undefined;
76
+ return {
77
+ mode: "resume_loop",
78
+ round: Math.min(continuation.reviewRoundsDone + 1, MAX_REVIEW_ROUNDS),
79
+ reviewInput: continuationReviewInput(continuation, ctx.mission),
80
+ priorRounds: [],
81
+ ticket: ctx.ticket,
82
+ ...(ctx.runId ? { runId: ctx.runId } : {}),
83
+ };
84
+ }
85
+ //# sourceMappingURL=continuation.js.map
@@ -13,8 +13,12 @@
13
13
  * after MAX_REVIEW_ROUNDS rounds, always with an explicit reason.
14
14
  */
15
15
  import { MAX_REVIEW_ROUNDS } from "./types.js";
16
- const SEVERITIES = ["critical", "high", "medium", "low"];
16
+ const SEVERITIES = ["critical", "high", "medium", "low", "needs_person"];
17
17
  const DEFAULT_LENS = "correctness";
18
+ /** `NEEDS PERSON`, `needs-person` and `needs_person` all read as `needs_person`. */
19
+ function normalizeSeverity(value) {
20
+ return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
21
+ }
18
22
  function isSeverity(value) {
19
23
  return SEVERITIES.includes(value);
20
24
  }
@@ -33,7 +37,7 @@ function parsePipeLine(line, lens) {
33
37
  const parts = line.split("|").map((p) => p.trim());
34
38
  if (parts.length < 2)
35
39
  return null;
36
- const severity = parts[0].toLowerCase();
40
+ const severity = normalizeSeverity(parts[0]);
37
41
  if (!isSeverity(severity))
38
42
  return null;
39
43
  let finding;
@@ -81,7 +85,7 @@ function parseJsonFindings(raw, lens) {
81
85
  if (!item || typeof item !== "object")
82
86
  continue;
83
87
  const obj = item;
84
- const severity = typeof obj.severity === "string" ? obj.severity.toLowerCase() : "";
88
+ const severity = typeof obj.severity === "string" ? normalizeSeverity(obj.severity) : "";
85
89
  if (!isSeverity(severity))
86
90
  continue;
87
91
  const finding = {
@@ -906,7 +906,7 @@ export function registerGoCommand(pi, deps = {}) {
906
906
  })()
907
907
  : {}),
908
908
  openPr: parsed.flags.pr,
909
- remainingFindings: result.findings.filter((f) => f.severity === "medium" || f.severity === "low"),
909
+ remainingFindings: result.findings.filter((f) => f.severity === "medium" || f.severity === "low" || f.severity === "needs_person"),
910
910
  ...(finishBaselinePaths.length > 0 ? { baselinePaths: finishBaselinePaths } : {}),
911
911
  signal: runSignal,
912
912
  });
@@ -8,7 +8,7 @@
8
8
  * session instead of the product's staged pipeline. This module closes it:
9
9
  *
10
10
  * yagni go --headless --ticket-file <p> [--plan-file <p>] [--memo-file <p>]
11
- * [--run-id <id>] [--cwd <p>] [--json]
11
+ * [--continue-file <p>] [--run-id <id>] [--cwd <p>] [--json]
12
12
  *
13
13
  * It is the SAME `runPipeline` path the interactive command drives (no forked
14
14
  * driver) with the interactive-only machinery left out: no worktree, no run
@@ -24,6 +24,13 @@
24
24
  * run ends at the reviewed candidate and never commits, pushes, opens a PR, or
25
25
  * reports one. The mission's own prHandoff owns all of that.
26
26
  *
27
+ * CONTINUATION (`--continue-file`, mission mode only): the run continues an
28
+ * earlier attempt whose change is already in the tree as uncommitted edits. The
29
+ * build baselines the clean base, and an attempt that stopped in review, fix or
30
+ * verify re-enters the review loop at its next round (see continuation.ts). An
31
+ * unreadable or unknown continue file is reported on stderr and the run goes on
32
+ * as a normal mission.
33
+ *
27
34
  * Output contract:
28
35
  * - `--json`: one NDJSON line per event on stdout. Child events are the pi
29
36
  * `--mode json` objects VERBATIM plus an additive `yagni` attribution key
@@ -55,7 +62,7 @@ import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
55
62
  import { type RunBudget } from "./budget.js";
56
63
  import type { Finding, JsonEvent, ModelTier, PipelineStage, StageId, StageTag, StageUsage, StopReason } from "./types.js";
57
64
  /** One-line usage copy, shared by every argument error. */
58
- export declare const HEADLESS_GO_USAGE = "Usage: yagni go --headless --ticket-file <path> [--plan-file <path>] [--memo-file <path>] [--run-id <id>] [--cwd <path>] [--json]";
65
+ export declare const HEADLESS_GO_USAGE = "Usage: yagni go --headless --ticket-file <path> [--plan-file <path>] [--memo-file <path>] [--continue-file <path>] [--run-id <id>] [--cwd <path>] [--json]";
59
66
  /**
60
67
  * Exit codes. `verified` is deliberately narrow: only a `clean` stop means the
61
68
  * run produced a reviewed candidate, so a round_cap / no_changes / failed run
@@ -73,6 +80,7 @@ export interface HeadlessGoArgs {
73
80
  ticketFile?: string;
74
81
  planFile?: string;
75
82
  memoFile?: string;
83
+ continueFile?: string;
76
84
  runId?: string;
77
85
  cwd?: string;
78
86
  /** Any `--flag`-shaped token we do not know: refused, never folded in. */
@@ -8,7 +8,7 @@
8
8
  * session instead of the product's staged pipeline. This module closes it:
9
9
  *
10
10
  * yagni go --headless --ticket-file <p> [--plan-file <p>] [--memo-file <p>]
11
- * [--run-id <id>] [--cwd <p>] [--json]
11
+ * [--continue-file <p>] [--run-id <id>] [--cwd <p>] [--json]
12
12
  *
13
13
  * It is the SAME `runPipeline` path the interactive command drives (no forked
14
14
  * driver) with the interactive-only machinery left out: no worktree, no run
@@ -24,6 +24,13 @@
24
24
  * run ends at the reviewed candidate and never commits, pushes, opens a PR, or
25
25
  * reports one. The mission's own prHandoff owns all of that.
26
26
  *
27
+ * CONTINUATION (`--continue-file`, mission mode only): the run continues an
28
+ * earlier attempt whose change is already in the tree as uncommitted edits. The
29
+ * build baselines the clean base, and an attempt that stopped in review, fix or
30
+ * verify re-enters the review loop at its next round (see continuation.ts). An
31
+ * unreadable or unknown continue file is reported on stderr and the run goes on
32
+ * as a normal mission.
33
+ *
27
34
  * Output contract:
28
35
  * - `--json`: one NDJSON line per event on stdout. Child events are the pi
29
36
  * `--mode json` objects VERBATIM plus an additive `yagni` attribution key
@@ -51,6 +58,7 @@
51
58
  * cannot finish, rather than being killed partway through one (see budget.ts).
52
59
  */
53
60
  import { readFileSync } from "node:fs";
61
+ import { continuationResumePlan, parseContinuation } from "./continuation.js";
54
62
  import { resolveFanoutMode } from "./fanout.js";
55
63
  import { makeFanoutBeats } from "./fanoutBeats.js";
56
64
  import { normalizeMission } from "./mission.js";
@@ -61,7 +69,7 @@ import { codeStateHome } from "../stateHome.js";
61
69
  import { parseTierCap, TIER_CAP_ENV } from "./tierCap.js";
62
70
  import { DEFAULT_RUN_BUDGET, parsePipelineTimeLeft, PIPELINE_TIME_LEFT_ENV } from "./budget.js";
63
71
  /** One-line usage copy, shared by every argument error. */
64
- export const HEADLESS_GO_USAGE = "Usage: yagni go --headless --ticket-file <path> [--plan-file <path>] [--memo-file <path>] [--run-id <id>] [--cwd <path>] [--json]";
72
+ export const HEADLESS_GO_USAGE = "Usage: yagni go --headless --ticket-file <path> [--plan-file <path>] [--memo-file <path>] [--continue-file <path>] [--run-id <id>] [--cwd <path>] [--json]";
65
73
  /**
66
74
  * Exit codes. `verified` is deliberately narrow: only a `clean` stop means the
67
75
  * run produced a reviewed candidate, so a round_cap / no_changes / failed run
@@ -72,6 +80,7 @@ const VALUE_FLAGS = {
72
80
  "--ticket-file": "ticketFile",
73
81
  "--plan-file": "planFile",
74
82
  "--memo-file": "memoFile",
83
+ "--continue-file": "continueFile",
75
84
  "--run-id": "runId",
76
85
  "--cwd": "cwd",
77
86
  };
@@ -284,6 +293,26 @@ export async function runHeadlessGo(argv, deps = {}) {
284
293
  // A blank plan or memo file is NOT mission context: it must never read as an
285
294
  // approved plan (which would skip the pipeline's planning on nothing at all).
286
295
  const mission = normalizeMission({ plan: files.plan, memo: files.memo });
296
+ // The continue file fails soft on every path: a continuation the run cannot
297
+ // read still has the saved change in its tree, and a normal mission on it beats
298
+ // no run at all.
299
+ let continuation;
300
+ if (args.continueFile && !mission) {
301
+ writeErr(`Ignoring --continue-file ${args.continueFile}: a continuation needs a mission (--plan-file or --memo-file).`);
302
+ }
303
+ else if (args.continueFile) {
304
+ const raw = read(args.continueFile, "continue file");
305
+ const parsed = typeof raw === "string" ? parseContinuation(raw) : undefined;
306
+ if (parsed?.ok)
307
+ continuation = parsed.value;
308
+ else if (typeof raw !== "string")
309
+ writeErr(`${raw.error}. Running as a normal mission.`);
310
+ else if (parsed)
311
+ writeErr(`Ignoring --continue-file ${args.continueFile}: ${parsed.reason}. Running as a normal mission.`);
312
+ }
313
+ const resumeFrom = continuation
314
+ ? continuationResumePlan(continuation, { ticket: files.ticket, ...(args.runId ? { runId: args.runId } : {}), ...(mission ? { mission } : {}) })
315
+ : undefined;
287
316
  // The implement diamond's recording thread, per RUN. The beats ride the
288
317
  // NDJSON stream as `pipeline_fanout` records, and the mission collector
289
318
  // folds them onto the Run.
@@ -305,6 +334,8 @@ export async function runHeadlessGo(argv, deps = {}) {
305
334
  ...(deps.stages ? { stages: deps.stages } : {}),
306
335
  grounded: deps.grounded ?? grounding,
307
336
  ...(mission ? { mission } : {}),
337
+ ...(continuation ? { continuation: { stageAtStop: continuation.stageAtStop } } : {}),
338
+ ...(resumeFrom ? { resumeFrom } : {}),
308
339
  onEvent: (ev, tag) => {
309
340
  deps.onEvent?.(ev, tag);
310
341
  if (stream)
@@ -323,10 +354,12 @@ export async function runHeadlessGo(argv, deps = {}) {
323
354
  },
324
355
  onStage: (s) => {
325
356
  // `output` carries the plan stage's full text; the stream reports the
326
- // boundary, never the body.
357
+ // boundary, never the body. `at` is when the boundary happened, so a
358
+ // host that reads the line later (a reattach replays the log) still
359
+ // dates it right.
327
360
  const { output: _output, ...boundary } = s;
328
361
  if (stream)
329
- emit({ type: "pipeline_stage", ...boundary });
362
+ emit({ type: "pipeline_stage", ...boundary, at: Date.now() });
330
363
  },
331
364
  logger: (event, data) => {
332
365
  deps.logger?.(event, data);
@@ -52,4 +52,11 @@ export declare function missionSkippedStages(mission: MissionInputs | undefined)
52
52
  * still runs and the memo is its repo map.
53
53
  */
54
54
  export declare function missionSeed(mission: MissionInputs | undefined): string | undefined;
55
+ /** Heading of the design context pinned onto every review and fix handoff. */
56
+ export declare const PINNED_DESIGN_MARKER = "## Pinned design context";
57
+ /**
58
+ * The approved plan, pinned onto the review and fix handoffs so a prototype
59
+ * reference survives every round. Undefined when the mission carries no plan.
60
+ */
61
+ export declare function missionDesignContext(mission: MissionInputs | undefined): string | undefined;
55
62
  //# sourceMappingURL=mission.d.ts.map
@@ -67,4 +67,14 @@ export function missionSeed(mission) {
67
67
  return normalized.plan;
68
68
  return `${normalized.plan}\n\n${REPO_CONTEXT_HEADING}\n\n${REPO_CONTEXT_NOTE}\n\n${normalized.memo}`;
69
69
  }
70
+ /** Heading of the design context pinned onto every review and fix handoff. */
71
+ export const PINNED_DESIGN_MARKER = "## Pinned design context";
72
+ /**
73
+ * The approved plan, pinned onto the review and fix handoffs so a prototype
74
+ * reference survives every round. Undefined when the mission carries no plan.
75
+ */
76
+ export function missionDesignContext(mission) {
77
+ const plan = normalizeMission(mission)?.plan;
78
+ return plan ? `${PINNED_DESIGN_MARKER}\nApproved mission plan (preserve exact prototype references):\n${plan}` : undefined;
79
+ }
70
80
  //# sourceMappingURL=mission.js.map
@@ -25,12 +25,23 @@
25
25
  * findings — i.e. a false "reviewed and clean").
26
26
  */
27
27
  import { type RunBudget } from "./budget.js";
28
+ import type { ContinuationStage } from "./continuation.js";
28
29
  import { type MissionInputs } from "./mission.js";
29
30
  import { type ResilienceAttemptRecord, type RunStageFn } from "./resilience.js";
30
31
  import { runStage as defaultRunStage } from "./runner.js";
31
32
  import { type VerifyOutcome, type WorkstreamCheckResult, type WorkstreamCheckTarget } from "./verify.js";
32
33
  import { snapshotWorkspace as defaultSnapshotWorkspace } from "./workspace.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";
34
+ import { type CheckpointStore, type FanoutBudgetVerdict, type FanoutMode, type Finding, type PipelineStage, type JsonEvent, type PipelineProgress, type PipelineResult, type ResiliencePolicy, type ResumePlan, type StageId, type StageResult, type StageTag } from "./types.js";
35
+ /** Each review round's findings as the host stores them: bounded, messages clipped. */
36
+ export declare const MAX_ROUND_ITEMS = 20;
37
+ export interface RoundFindingItem {
38
+ severity: Finding["severity"];
39
+ lens: Finding["lens"];
40
+ message: string;
41
+ file?: string;
42
+ line?: number;
43
+ }
44
+ export declare function roundFindingItems(findings: readonly Finding[]): RoundFindingItem[];
34
45
  export interface RunPipelineDeps {
35
46
  /** Bounded external design preparation; never runs inside the read-only planner or approved missions. */
36
47
  prepareDesign?: (input: {
@@ -112,6 +123,10 @@ export interface RunPipelineDeps {
112
123
  * `output` carries a stage's `finalOutput` ONLY on the plan FINISH boundary, so
113
124
  * /go can record the plan onto the work item without streaming every stage's
114
125
  * full text. It is absent on every other boundary.
126
+ *
127
+ * `items` carries each review round's own findings on the review FINISH
128
+ * boundary (at most {@link MAX_ROUND_ITEMS}, messages clipped), so the host
129
+ * keeps every round and not only the last one.
115
130
  */
116
131
  onStage?: (s: {
117
132
  stage: StageId;
@@ -119,6 +134,7 @@ export interface RunPipelineDeps {
119
134
  round?: number;
120
135
  findings?: number;
121
136
  output?: string;
137
+ items?: RoundFindingItem[];
122
138
  }) => void;
123
139
  /**
124
140
  * Optional, fail-soft durable journal seam (spec: 2026-06-29-go-resilience).
@@ -146,6 +162,19 @@ export interface RunPipelineDeps {
146
162
  * The stage-skipping and seeding rules are pure and live in `mission.ts`.
147
163
  */
148
164
  mission?: MissionInputs;
165
+ /**
166
+ * Set when this run continues an earlier attempt (the headless entry's
167
+ * `--continue-file`): the working tree already holds that attempt's change as
168
+ * uncommitted edits on top of HEAD. The build baseline is then the CLEAN base,
169
+ * so the saved change counts as this run's own: an implement that only
170
+ * confirms finished work is not `no_changes`, and the verify gate and the
171
+ * fan-out's change list keep the saved change's files in scope. Re-entering
172
+ * the review loop is `resumeFrom`'s job (see continuation.ts). Absent on every
173
+ * other run, which is byte-identical to before.
174
+ */
175
+ continuation?: {
176
+ stageAtStop: ContinuationStage | null;
177
+ };
149
178
  /**
150
179
  * `go.fanout` (spec decision 8). `auto` (the default) runs the partitioner and
151
180
  * lets its conservative verdict stand; `always` pins the diamond for benchmark
@@ -28,13 +28,27 @@ import { addRunUsage, aggregateRunUsage, DEFAULT_RUN_BUDGET, DEFAULT_STAGE_ESTIM
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";
31
- import { missionSeed, missionSkippedStages, normalizeMission } from "./mission.js";
31
+ import { missionDesignContext, missionSeed, missionSkippedStages, normalizeMission, PINNED_DESIGN_MARKER, } from "./mission.js";
32
32
  import { withResilience } from "./resilience.js";
33
33
  import { runStage as defaultRunStage } from "./runner.js";
34
34
  import { makeRunVerify, makeWorkstreamCheck, parseChangedPaths, } from "./verify.js";
35
35
  import { builderStage, fixerStage, orchestratorStage, partitionReaskStage, PARTITION_CALLER_LABEL, REQUIRED_LENSES, REVIEW_LENSES, reaskStage, reviewStage, selectStages, synthesizerFixStage, synthesizerStage, SYNTHESIZER_CALLER_LABEL, workstreamCallerLabel, } from "./stages.js";
36
36
  import { snapshotWorkspace as defaultSnapshotWorkspace, workspaceChanged } from "./workspace.js";
37
37
  import { DEFAULT_RESILIENCE_POLICY, MAX_CONCURRENCY, MAX_FANOUT_CONCURRENCY, MAX_FANOUT_CONCURRENCY_ULTRA, MAX_FIX_TURNS, MIN_TOOL_CALLS_FOR_HEALTH, TOOL_ERROR_FAIL_RATE, } from "./types.js";
38
+ /** Each review round's findings as the host stores them: bounded, messages clipped. */
39
+ export const MAX_ROUND_ITEMS = 20;
40
+ const MAX_ROUND_ITEM_MESSAGE = 300;
41
+ export function roundFindingItems(findings) {
42
+ return findings.slice(0, MAX_ROUND_ITEMS).map((finding) => ({
43
+ severity: finding.severity,
44
+ lens: finding.lens,
45
+ message: finding.message.length > MAX_ROUND_ITEM_MESSAGE
46
+ ? `${finding.message.slice(0, MAX_ROUND_ITEM_MESSAGE - 1)}…`
47
+ : finding.message,
48
+ ...(finding.file ? { file: finding.file } : {}),
49
+ ...(finding.line !== undefined ? { line: finding.line } : {}),
50
+ }));
51
+ }
38
52
  /** Error thrown when a build stage fails; carries the partial run for the caller. */
39
53
  export class PipelineStageError extends Error {
40
54
  stageId;
@@ -335,11 +349,9 @@ export async function runPipeline(ticket, deps) {
335
349
  const rounds = resume ? [...resume.priorRounds] : [];
336
350
  let round = resume ? resume.round : 1;
337
351
  let reviewInput = resume ? resume.reviewInput : "";
338
- const designMarker = "## Pinned design context";
352
+ const designMarker = PINNED_DESIGN_MARKER;
339
353
  const resumedDesignOffset = reviewInput.indexOf(designMarker);
340
- let designContext = mission?.plan
341
- ? `${designMarker}\nApproved mission plan (preserve exact prototype references):\n${mission.plan}`
342
- : resumedDesignOffset >= 0 ? reviewInput.slice(resumedDesignOffset) : undefined;
354
+ let designContext = missionDesignContext(mission) ?? (resumedDesignOffset >= 0 ? reviewInput.slice(resumedDesignOffset) : undefined);
343
355
  let stopReason = "clean";
344
356
  // P4: honest note carried out when the verify gate could not give a verdict on
345
357
  // the final round (no command / unrunnable), so /go never claims a build it did
@@ -364,6 +376,9 @@ export async function runPipeline(ticket, deps) {
364
376
  // through by the harness leaves a half-edited tree and no verdict at all.
365
377
  const observedMs = {};
366
378
  const wontFit = (kind) => stageWontFit(budget, kind, observedMs[kind] ?? DEFAULT_STAGE_ESTIMATE_MS[kind], clock());
379
+ if (deps.continuation) {
380
+ log("continuation", { stageAtStop: deps.continuation.stageAtStop, resumedAtRound: resume ? resume.round : null });
381
+ }
367
382
  // The map stage's brief, kept for the partitioner's repo context. Absent on a
368
383
  // mission run (map is skipped there — the injected memo stands in) and on any
369
384
  // run whose map stage did not produce one; the partition contract is explicit
@@ -748,7 +763,10 @@ export async function runPipeline(ticket, deps) {
748
763
  // Baseline the working tree BEFORE the (read-only) recon stages so the no-op
749
764
  // guard below can attribute any change to the build half. A git-less / non-repo
750
765
  // cwd yields an untracked snapshot and the guard fails open (see workspace.ts).
751
- const beforeBuild = await snapshotFn(deps.cwd, deps.signal);
766
+ // A continuation baselines the clean base instead: the earlier attempt's saved
767
+ // edits are this run's change, not pre-run dirt.
768
+ const liveBaseline = await snapshotFn(deps.cwd, deps.signal);
769
+ const beforeBuild = deps.continuation && liveBaseline.tracked ? { ...liveBaseline, status: "" } : liveBaseline;
752
770
  writeCheckpoint(mkRecord("run_start", { snapshot: beforeBuild }));
753
771
  // Record the pre-run dirt so the verify gate scopes to this run's OWN diff.
754
772
  baselinePaths = parseChangedPaths(beforeBuild.status);
@@ -1026,7 +1044,7 @@ export async function runPipeline(ticket, deps) {
1026
1044
  ...(degradedLenses ? { degradedLenses } : {}),
1027
1045
  });
1028
1046
  progress({ kind: "findings", round, total: findings.length, blocking: blockingCount(findings) });
1029
- stageEvent("review", "finish", { round, findings: findings.length });
1047
+ stageEvent("review", "finish", { round, findings: findings.length, items: roundFindingItems(findings) });
1030
1048
  // Honest health check BEFORE trusting the verdict: an aborted or crashed
1031
1049
  // review must NOT be read as a clean review (empty output → [] findings →
1032
1050
  // would otherwise stop 'clean'). Mirrors the build half's isFailed guard.
@@ -7,7 +7,8 @@
7
7
  * - scout / planner: call `ask_yagni` before inferring a convention,
8
8
  * - worker: call `record_decision` only for a consequential product-intent or architecture call,
9
9
  * - reviewer (business-fit lens): call `review_business_match` and treat a
10
- * conflict with a recorded decision as at least High.
10
+ * conflict with a recorded decision as at least High, and a disagreement
11
+ * with the plan the person approved as needs_person.
11
12
  *
12
13
  * The implement diamond adds two more roles: `orchestrator` (the read-only
13
14
  * partitioner, carrying the ```partition output contract `parsePartition` reads)
@@ -7,7 +7,8 @@
7
7
  * - scout / planner: call `ask_yagni` before inferring a convention,
8
8
  * - worker: call `record_decision` only for a consequential product-intent or architecture call,
9
9
  * - reviewer (business-fit lens): call `review_business_match` and treat a
10
- * conflict with a recorded decision as at least High.
10
+ * conflict with a recorded decision as at least High, and a disagreement
11
+ * with the plan the person approved as needs_person.
11
12
  *
12
13
  * The implement diamond adds two more roles: `orchestrator` (the read-only
13
14
  * partitioner, carrying the ```partition output contract `parsePartition` reads)
@@ -331,9 +332,9 @@ export const BLIND_PERSONA_BODIES = {
331
332
  };
332
333
  /** The lens-specific clause appended to the reviewer body, one per review angle. */
333
334
  const LENS_CLAUSES = {
334
- correctness: "Lens: CORRECTNESS. Hunt bugs, broken logic, unhandled edge cases, race conditions, and incorrect error handling. A real defect that can ship is at least High.",
335
- business_fit: "Lens: BUSINESS-FIT. This is the only-YAGNI lens. Call review_business_match and consult the decision corpus: does this change match the recorded decisions, conventions, and current priorities of this company? Right code doing the wrong thing is exactly the failure you exist to catch. A conflict with a recorded decision is at least High.",
336
- does_it_hold: "Lens: DOES-IT-HOLD. Does the change actually accomplish the ticket, and does it build/test as far as read-only bash lets you verify? Missing tests for new behavior, or a change that does not do the task, is at least High.",
335
+ correctness: "Lens: CORRECTNESS. Hunt bugs, broken logic, unhandled edge cases, race conditions, and incorrect error handling. A defect that breaks behavior or data at runtime is at least High. Copy, naming, dead code, and style are Medium or Low.",
336
+ business_fit: "Lens: BUSINESS-FIT. This is the only-YAGNI lens. Call review_business_match and consult the decision corpus: does this change match the recorded decisions, conventions, and current priorities of this company? Right code doing the wrong thing is exactly the failure you exist to catch. A conflict with a recorded decision is at least High. A disagreement with the plan the person approved is needs_person: the person decided it, so only the person can change it.",
337
+ does_it_hold: "Lens: DOES-IT-HOLD. Does the change actually accomplish the ticket, and does it build/test as far as read-only bash lets you verify? A change that does not do the task, or new behavior with no test at all, is at least High. Something only a person can do (a named sign-off, an account, a credential, a step outside the repository) is needs_person, never High.",
337
338
  };
338
339
  /**
339
340
  * The machine-readable findings contract every reviewer must emit so the
@@ -346,7 +347,7 @@ End your review with a fenced block in EXACTLY this form, one line per finding:
346
347
  SEVERITY | file:line | message
347
348
  \`\`\`
348
349
 
349
- SEVERITY is one of: critical, high, medium, low. Use \`file:line\` when you can point to a location; otherwise give a short location or omit it. Put one finding per line and nothing else inside the block. If you found no problems, emit an empty \`\`\`findings block. Only critical and high findings block the change.
350
+ SEVERITY is one of: critical, high, medium, low, needs_person. needs_person means only a person can do it (a named sign-off, an account, a credential, a call outside the code); it does not block, and the person sees it on the pull request. Use \`file:line\` when you can point to a location; otherwise give a short location or omit it. Put one finding per line and nothing else inside the block. If you found no problems, emit an empty \`\`\`findings block. Only critical and high findings block the change.
350
351
 
351
352
  Be terse. Do not narrate your process, restate the diff, or quote code back at length — spend your output on the findings themselves, at most a few short paragraphs before the block. Cap the block at the 12 most important findings, most severe first; a review cut off by its own length limit helps nobody.`;
352
353
  /**
@@ -357,7 +358,7 @@ Be terse. Do not narrate your process, restate the diff, or quote code back at l
357
358
  */
358
359
  const BLIND_LENS_CLAUSES = {
359
360
  correctness: LENS_CLAUSES.correctness,
360
- business_fit: "Lens: BUSINESS-FIT. Does this change match the apparent product intent and the conventions visible in the code? Right code doing the wrong thing is exactly the failure you exist to catch. A clear mismatch is at least High.",
361
+ business_fit: "Lens: BUSINESS-FIT. Does this change match the apparent product intent and the conventions visible in the code? Right code doing the wrong thing is exactly the failure you exist to catch. A clear mismatch is at least High. A disagreement with the plan the person approved is needs_person.",
361
362
  does_it_hold: LENS_CLAUSES.does_it_hold,
362
363
  };
363
364
  /**
@@ -35,8 +35,13 @@ export type StageId = "map" | "plan" | "implement" | "review" | "fix";
35
35
  export type FeedStageId = StageId | "finish";
36
36
  /** The three adversarial review angles (spec §3). */
37
37
  export type ReviewLens = "correctness" | "business_fit" | "does_it_hold";
38
- /** Finding severities; only `critical` + `high` block the review→fix loop. */
39
- export type Severity = "critical" | "high" | "medium" | "low";
38
+ /**
39
+ * Finding severities; only `critical` + `high` block the review→fix loop.
40
+ * `needs_person` is something only a person can do (a named sign-off, an
41
+ * account, a credential, a call outside the code): the reviewer assigns it,
42
+ * the fix stage never sees it, and it rides the pull request for the person.
43
+ */
44
+ export type Severity = "critical" | "high" | "medium" | "low" | "needs_person";
40
45
  /**
41
46
  * One declarative stage. `model` + `tools` are the tuning surface; `taskTemplate`
42
47
  * carries `{ticket}` / `{previous}` placeholders the invocation builder fills.
@@ -496,8 +501,12 @@ export type PipelineProgress = {
496
501
  kind: "done";
497
502
  stopReason: StopReason;
498
503
  };
499
- /** Stop the review→fix loop after at most this many rounds (spec §7.3). */
500
- export declare const MAX_REVIEW_ROUNDS = 3;
504
+ /**
505
+ * Stop the review→fix loop after at most this many rounds (spec §7.3). A third
506
+ * round never helped: every build that reached it ended with as many findings
507
+ * or more, and hit the cap anyway.
508
+ */
509
+ export declare const MAX_REVIEW_ROUNDS = 2;
501
510
  /**
502
511
  * Hard cap on the implement diamond's fix turns (spec decision 7). Verification
503
512
  * may re-engage the builders that broke the tree, but only this many times: past
@@ -6,8 +6,12 @@
6
6
  * scattered string literals. The stage list is a declarative contract (spec
7
7
  * §7.2) so future complexity-lanes select a subset, never a rewrite.
8
8
  */
9
- /** Stop the review→fix loop after at most this many rounds (spec §7.3). */
10
- export const MAX_REVIEW_ROUNDS = 3;
9
+ /**
10
+ * Stop the review→fix loop after at most this many rounds (spec §7.3). A third
11
+ * round never helped: every build that reached it ended with as many findings
12
+ * or more, and hit the cap anyway.
13
+ */
14
+ export const MAX_REVIEW_ROUNDS = 2;
11
15
  /**
12
16
  * Hard cap on the implement diamond's fix turns (spec decision 7). Verification
13
17
  * may re-engage the builders that broke the tree, but only this many times: past
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.2.3-staging.1621.1",
3
+ "version": "1.2.3-staging.1632.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": "7d2396135b54ce5b4461bd57dcdb99292eb52115"
61
+ "yagniSourceSha": "3275eb7ef2a6e502e47bd2e835aa38bd4fb28b37"
62
62
  }