@yagni-app/code-staging 1.2.3-staging.1627.1 → 1.2.3-staging.1659.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.
- package/dist/extension/pipeline/budget.d.ts +9 -5
- package/dist/extension/pipeline/budget.js +10 -5
- package/dist/extension/pipeline/continuation.d.ts +61 -0
- package/dist/extension/pipeline/continuation.js +85 -0
- package/dist/extension/pipeline/headlessGo.d.ts +10 -2
- package/dist/extension/pipeline/headlessGo.js +33 -2
- package/dist/extension/pipeline/mission.d.ts +7 -0
- package/dist/extension/pipeline/mission.js +10 -0
- package/dist/extension/pipeline/orchestrator.d.ts +14 -0
- package/dist/extension/pipeline/orchestrator.js +23 -7
- package/package.json +2 -2
|
@@ -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
|
|
83
|
-
* to its deadline keeps this much
|
|
84
|
-
*
|
|
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
|
|
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.
|
|
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
|
|
112
|
-
* to its deadline keeps this much
|
|
113
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
|
@@ -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)
|
|
@@ -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,6 +25,7 @@
|
|
|
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";
|
|
@@ -161,6 +162,19 @@ export interface RunPipelineDeps {
|
|
|
161
162
|
* The stage-skipping and seeding rules are pure and live in `mission.ts`.
|
|
162
163
|
*/
|
|
163
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
|
+
};
|
|
164
178
|
/**
|
|
165
179
|
* `go.fanout` (spec decision 8). `auto` (the default) runs the partitioner and
|
|
166
180
|
* lets its conservative verdict stand; `always` pins the diamond for benchmark
|
|
@@ -28,7 +28,7 @@ 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";
|
|
@@ -128,6 +128,14 @@ function incompleteHandoff(r) {
|
|
|
128
128
|
* against stderr/errorMessage only — never a stage's real output.
|
|
129
129
|
*/
|
|
130
130
|
const CREDIT_EXHAUSTION_RE = /out of credits|insufficient_quota|payment required|\b402\b/;
|
|
131
|
+
/**
|
|
132
|
+
* The run-scoped budget shapes: a Run's own approved budget or model-request
|
|
133
|
+
* limit, which the proxy refuses with a 429 typed `insufficient_quota`. The
|
|
134
|
+
* workspace can have plenty of credits when this fires, so it is checked first
|
|
135
|
+
* and never reported as the workspace's credits (production 2026-10-08: a $25
|
|
136
|
+
* run budget read as "the workspace ran out of credits").
|
|
137
|
+
*/
|
|
138
|
+
const RUN_BUDGET_EXHAUSTION_RE = /run_budget_exhausted|run_request_limit_exhausted|This Run has reached its model-request or approved packet budget|This Run has used its \d+ model requests/;
|
|
131
139
|
/**
|
|
132
140
|
* The silent-402 landmine: when the workspace runs out of credits mid-run, the
|
|
133
141
|
* proxy 402s every model request, the child pi exits 0 with an EMPTY transcript,
|
|
@@ -135,12 +143,16 @@ const CREDIT_EXHAUSTION_RE = /out of credits|insufficient_quota|payment required
|
|
|
135
143
|
* "clean" stop with the real cause buried in stderr. Detect it and stop with an
|
|
136
144
|
* actionable message instead. Gated on the stage having produced no real output
|
|
137
145
|
* (or having failed outright) so a healthy stage whose stderr happens to contain
|
|
138
|
-
* a matching token can never trip it.
|
|
146
|
+
* a matching token can never trip it. A Run that spent its own budget stops the
|
|
147
|
+
* same way, with its own message.
|
|
139
148
|
*/
|
|
140
149
|
function creditExhaustionReason(r) {
|
|
141
150
|
if (r.finalOutput.trim().length > 0 && !isFailed(r))
|
|
142
151
|
return undefined;
|
|
143
152
|
const hay = `${r.errorMessage ?? ""}\n${r.stderr}`;
|
|
153
|
+
if (RUN_BUDGET_EXHAUSTION_RE.test(hay)) {
|
|
154
|
+
return "this run reached its approved budget (the model proxy refused further calls). Raise the Team's run budget or revise the plan, then re-run /go.";
|
|
155
|
+
}
|
|
144
156
|
if (!CREDIT_EXHAUSTION_RE.test(hay))
|
|
145
157
|
return undefined;
|
|
146
158
|
return "the workspace is out of YAGNI credits (the model proxy returned 402). Top up credits, then re-run /go.";
|
|
@@ -349,11 +361,9 @@ export async function runPipeline(ticket, deps) {
|
|
|
349
361
|
const rounds = resume ? [...resume.priorRounds] : [];
|
|
350
362
|
let round = resume ? resume.round : 1;
|
|
351
363
|
let reviewInput = resume ? resume.reviewInput : "";
|
|
352
|
-
const designMarker =
|
|
364
|
+
const designMarker = PINNED_DESIGN_MARKER;
|
|
353
365
|
const resumedDesignOffset = reviewInput.indexOf(designMarker);
|
|
354
|
-
let designContext = mission
|
|
355
|
-
? `${designMarker}\nApproved mission plan (preserve exact prototype references):\n${mission.plan}`
|
|
356
|
-
: resumedDesignOffset >= 0 ? reviewInput.slice(resumedDesignOffset) : undefined;
|
|
366
|
+
let designContext = missionDesignContext(mission) ?? (resumedDesignOffset >= 0 ? reviewInput.slice(resumedDesignOffset) : undefined);
|
|
357
367
|
let stopReason = "clean";
|
|
358
368
|
// P4: honest note carried out when the verify gate could not give a verdict on
|
|
359
369
|
// the final round (no command / unrunnable), so /go never claims a build it did
|
|
@@ -378,6 +388,9 @@ export async function runPipeline(ticket, deps) {
|
|
|
378
388
|
// through by the harness leaves a half-edited tree and no verdict at all.
|
|
379
389
|
const observedMs = {};
|
|
380
390
|
const wontFit = (kind) => stageWontFit(budget, kind, observedMs[kind] ?? DEFAULT_STAGE_ESTIMATE_MS[kind], clock());
|
|
391
|
+
if (deps.continuation) {
|
|
392
|
+
log("continuation", { stageAtStop: deps.continuation.stageAtStop, resumedAtRound: resume ? resume.round : null });
|
|
393
|
+
}
|
|
381
394
|
// The map stage's brief, kept for the partitioner's repo context. Absent on a
|
|
382
395
|
// mission run (map is skipped there — the injected memo stands in) and on any
|
|
383
396
|
// run whose map stage did not produce one; the partition contract is explicit
|
|
@@ -762,7 +775,10 @@ export async function runPipeline(ticket, deps) {
|
|
|
762
775
|
// Baseline the working tree BEFORE the (read-only) recon stages so the no-op
|
|
763
776
|
// guard below can attribute any change to the build half. A git-less / non-repo
|
|
764
777
|
// cwd yields an untracked snapshot and the guard fails open (see workspace.ts).
|
|
765
|
-
|
|
778
|
+
// A continuation baselines the clean base instead: the earlier attempt's saved
|
|
779
|
+
// edits are this run's change, not pre-run dirt.
|
|
780
|
+
const liveBaseline = await snapshotFn(deps.cwd, deps.signal);
|
|
781
|
+
const beforeBuild = deps.continuation && liveBaseline.tracked ? { ...liveBaseline, status: "" } : liveBaseline;
|
|
766
782
|
writeCheckpoint(mkRecord("run_start", { snapshot: beforeBuild }));
|
|
767
783
|
// Record the pre-run dirt so the verify gate scopes to this run's OWN diff.
|
|
768
784
|
baselinePaths = parseChangedPaths(beforeBuild.status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.2.3-staging.
|
|
3
|
+
"version": "1.2.3-staging.1659.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": "
|
|
61
|
+
"yagniSourceSha": "b384d042526710ecc56fb3c2bbb2dc4bd83925e3"
|
|
62
62
|
}
|