@yagni-app/code-staging 1.2.2-staging.1528.1 → 1.2.2-staging.1537.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 +36 -3
- package/dist/extension/pipeline/budget.js +45 -3
- package/dist/extension/pipeline/headlessGo.d.ts +6 -1
- package/dist/extension/pipeline/headlessGo.js +17 -1
- package/dist/extension/pipeline/orchestrator.d.ts +2 -0
- package/dist/extension/pipeline/orchestrator.js +27 -2
- package/package.json +2 -2
|
@@ -29,6 +29,14 @@ export interface RunBudget {
|
|
|
29
29
|
maxCost: number;
|
|
30
30
|
/** Max aggregate input+output tokens. */
|
|
31
31
|
maxTokens: number;
|
|
32
|
+
/**
|
|
33
|
+
* Absolute wall-clock deadline (epoch ms) the run must be done by. Absent on
|
|
34
|
+
* every interactive run, where only the usage ceilings apply. The sandbox
|
|
35
|
+
* harness sets it from `YAGNI_PIPELINE_TIME_LEFT_MS`, anchored to this
|
|
36
|
+
* process's own clock, so the run stops before a step it cannot finish
|
|
37
|
+
* instead of being killed partway through one.
|
|
38
|
+
*/
|
|
39
|
+
deadlineMs?: number;
|
|
32
40
|
}
|
|
33
41
|
/**
|
|
34
42
|
* Generous default ceilings: a 3-round /go fans out review across 3 lenses on top
|
|
@@ -41,8 +49,33 @@ export declare function addRunUsage(a: RunUsage, b: RunUsage): RunUsage;
|
|
|
41
49
|
/** Sum usage across the build/fix stages AND every review lens result. */
|
|
42
50
|
export declare function aggregateRunUsage(stages: StageResult[], rounds: ReviewRound[]): RunUsage;
|
|
43
51
|
/**
|
|
44
|
-
* Return an honest reason string when an aggregate usage crosses any ceiling
|
|
45
|
-
* null. Checked at each stage boundary so
|
|
52
|
+
* Return an honest reason string when an aggregate usage crosses any ceiling (or
|
|
53
|
+
* the run's deadline has passed), else null. Checked at each stage boundary so
|
|
54
|
+
* the run stops at the first crossing.
|
|
46
55
|
*/
|
|
47
|
-
export declare function exceedsBudget(usage: RunUsage, budget: RunBudget): string | null;
|
|
56
|
+
export declare function exceedsBudget(usage: RunUsage, budget: RunBudget, nowMs?: number): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Env var the sandbox host hands the run's time left (ms, relative) on. Relative,
|
|
59
|
+
* not an epoch: the host's clock and the sandbox VM's clock can disagree, and a
|
|
60
|
+
* skew would shift an absolute deadline one-for-one. The run anchors it to its
|
|
61
|
+
* own clock at start.
|
|
62
|
+
*/
|
|
63
|
+
export declare const PIPELINE_TIME_LEFT_ENV = "YAGNI_PIPELINE_TIME_LEFT_MS";
|
|
64
|
+
/** A positive integer millisecond count, or undefined for anything else (absent, blank, garbage). */
|
|
65
|
+
export declare function parsePipelineTimeLeft(raw: string | undefined): number | undefined;
|
|
66
|
+
/** The stage kinds whose START is gated on the time left. */
|
|
67
|
+
export type TimedStageKind = "review" | "fix";
|
|
68
|
+
/**
|
|
69
|
+
* What a stage is assumed to take before one has been observed this run. Set
|
|
70
|
+
* from a production run (2026-09-29): a review round took 2.5 to 3 minutes and
|
|
71
|
+
* a fix 4.5 to more than 5, so each default sits just above what was seen.
|
|
72
|
+
*/
|
|
73
|
+
export declare const DEFAULT_STAGE_ESTIMATE_MS: Record<TimedStageKind, number>;
|
|
74
|
+
/**
|
|
75
|
+
* Null when a stage expected to take `expectedMs` fits before the deadline (or
|
|
76
|
+
* there is no deadline); else an honest reason. The check is at the START of a
|
|
77
|
+
* stage: a stage already running cannot be stopped at a clean edit boundary,
|
|
78
|
+
* so the only safe place to stop is before it begins.
|
|
79
|
+
*/
|
|
80
|
+
export declare function stageWontFit(budget: RunBudget, kind: TimedStageKind, expectedMs: number, nowMs?: number): string | null;
|
|
48
81
|
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -49,10 +49,14 @@ export function aggregateRunUsage(stages, rounds) {
|
|
|
49
49
|
return acc;
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* Return an honest reason string when an aggregate usage crosses any ceiling
|
|
53
|
-
* null. Checked at each stage boundary so
|
|
52
|
+
* Return an honest reason string when an aggregate usage crosses any ceiling (or
|
|
53
|
+
* the run's deadline has passed), else null. Checked at each stage boundary so
|
|
54
|
+
* the run stops at the first crossing.
|
|
54
55
|
*/
|
|
55
|
-
export function exceedsBudget(usage, budget) {
|
|
56
|
+
export function exceedsBudget(usage, budget, nowMs = Date.now()) {
|
|
57
|
+
if (budget.deadlineMs !== undefined && nowMs >= budget.deadlineMs) {
|
|
58
|
+
return `time budget reached (deadline passed ${Math.round((nowMs - budget.deadlineMs) / 1000)}s ago)`;
|
|
59
|
+
}
|
|
56
60
|
if (usage.turns > budget.maxTurns) {
|
|
57
61
|
return `turn ceiling reached (${usage.turns} of ${budget.maxTurns} model turns)`;
|
|
58
62
|
}
|
|
@@ -65,4 +69,42 @@ export function exceedsBudget(usage, budget) {
|
|
|
65
69
|
}
|
|
66
70
|
return null;
|
|
67
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Env var the sandbox host hands the run's time left (ms, relative) on. Relative,
|
|
74
|
+
* not an epoch: the host's clock and the sandbox VM's clock can disagree, and a
|
|
75
|
+
* skew would shift an absolute deadline one-for-one. The run anchors it to its
|
|
76
|
+
* own clock at start.
|
|
77
|
+
*/
|
|
78
|
+
export const PIPELINE_TIME_LEFT_ENV = "YAGNI_PIPELINE_TIME_LEFT_MS";
|
|
79
|
+
/** A positive integer millisecond count, or undefined for anything else (absent, blank, garbage). */
|
|
80
|
+
export function parsePipelineTimeLeft(raw) {
|
|
81
|
+
const text = raw?.trim();
|
|
82
|
+
if (!text || !/^\d+$/.test(text))
|
|
83
|
+
return undefined;
|
|
84
|
+
const value = Number(text);
|
|
85
|
+
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* What a stage is assumed to take before one has been observed this run. Set
|
|
89
|
+
* from a production run (2026-09-29): a review round took 2.5 to 3 minutes and
|
|
90
|
+
* a fix 4.5 to more than 5, so each default sits just above what was seen.
|
|
91
|
+
*/
|
|
92
|
+
export const DEFAULT_STAGE_ESTIMATE_MS = {
|
|
93
|
+
review: 4 * 60_000,
|
|
94
|
+
fix: 6 * 60_000,
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Null when a stage expected to take `expectedMs` fits before the deadline (or
|
|
98
|
+
* there is no deadline); else an honest reason. The check is at the START of a
|
|
99
|
+
* stage: a stage already running cannot be stopped at a clean edit boundary,
|
|
100
|
+
* so the only safe place to stop is before it begins.
|
|
101
|
+
*/
|
|
102
|
+
export function stageWontFit(budget, kind, expectedMs, nowMs = Date.now()) {
|
|
103
|
+
if (budget.deadlineMs === undefined)
|
|
104
|
+
return null;
|
|
105
|
+
const leftMs = budget.deadlineMs - nowMs;
|
|
106
|
+
if (leftMs >= expectedMs)
|
|
107
|
+
return null;
|
|
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
|
+
}
|
|
68
110
|
//# sourceMappingURL=budget.js.map
|
|
@@ -46,9 +46,12 @@
|
|
|
46
46
|
* result object and forwarded on the child env; the clamp itself is applied
|
|
47
47
|
* centrally in `runStage` (see tierCap.ts). `YAGNI_GO_FANOUT` is read the same
|
|
48
48
|
* way and pins the implement diamond for a benchmark lane (see fanout.ts).
|
|
49
|
+
* `YAGNI_PIPELINE_TIME_LEFT_MS` (the sandbox host) is the run's time left,
|
|
50
|
+
* anchored to this process's clock at start: the run then stops `budget_exceeded` before a review round or fix it
|
|
51
|
+
* cannot finish, rather than being killed partway through one (see budget.ts).
|
|
49
52
|
*/
|
|
50
53
|
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
51
|
-
import type
|
|
54
|
+
import { type RunBudget } from "./budget.js";
|
|
52
55
|
import type { Finding, JsonEvent, ModelTier, PipelineStage, StageId, StageTag, StageUsage, StopReason } from "./types.js";
|
|
53
56
|
/** One-line usage copy, shared by every argument error. */
|
|
54
57
|
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]";
|
|
@@ -130,6 +133,8 @@ export interface HeadlessGoDeps {
|
|
|
130
133
|
env?: NodeJS.ProcessEnv;
|
|
131
134
|
/** Environment for spawned stage children. Defaults to `env`. */
|
|
132
135
|
childEnv?: NodeJS.ProcessEnv;
|
|
136
|
+
/** Wall clock (epoch ms) the host's time left is anchored to. Defaults to `Date.now`; injected in tests. */
|
|
137
|
+
now?: () => number;
|
|
133
138
|
runPipeline?: typeof defaultRunPipeline;
|
|
134
139
|
readFile?: (path: string) => string;
|
|
135
140
|
/** stdout sink; one call per NDJSON line, newline added by the caller. */
|
|
@@ -46,6 +46,9 @@
|
|
|
46
46
|
* result object and forwarded on the child env; the clamp itself is applied
|
|
47
47
|
* centrally in `runStage` (see tierCap.ts). `YAGNI_GO_FANOUT` is read the same
|
|
48
48
|
* way and pins the implement diamond for a benchmark lane (see fanout.ts).
|
|
49
|
+
* `YAGNI_PIPELINE_TIME_LEFT_MS` (the sandbox host) is the run's time left,
|
|
50
|
+
* anchored to this process's clock at start: the run then stops `budget_exceeded` before a review round or fix it
|
|
51
|
+
* cannot finish, rather than being killed partway through one (see budget.ts).
|
|
49
52
|
*/
|
|
50
53
|
import { readFileSync } from "node:fs";
|
|
51
54
|
import { resolveFanoutMode } from "./fanout.js";
|
|
@@ -55,6 +58,7 @@ import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
|
55
58
|
import { loadGroundingEnabled } from "../grounding.js";
|
|
56
59
|
import { codeStateHome } from "../stateHome.js";
|
|
57
60
|
import { parseTierCap, TIER_CAP_ENV } from "./tierCap.js";
|
|
61
|
+
import { DEFAULT_RUN_BUDGET, parsePipelineTimeLeft, PIPELINE_TIME_LEFT_ENV } from "./budget.js";
|
|
58
62
|
/** One-line usage copy, shared by every argument error. */
|
|
59
63
|
export const HEADLESS_GO_USAGE = "Usage: yagni go --headless --ticket-file <path> [--plan-file <path>] [--memo-file <path>] [--run-id <id>] [--cwd <path>] [--json]";
|
|
60
64
|
/**
|
|
@@ -257,6 +261,18 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
257
261
|
if (rawCap?.trim() && !tierCap) {
|
|
258
262
|
writeErr(`Ignoring ${TIER_CAP_ENV}="${rawCap}": not a tier (peak, advanced, standard, efficient). Running uncapped.`);
|
|
259
263
|
}
|
|
264
|
+
// The sandbox host's time left for this run, anchored to THIS process's clock
|
|
265
|
+
// (the host's may be skewed from the VM's). Absent (every local run, and a
|
|
266
|
+
// host that predates it) leaves the budget exactly as it was.
|
|
267
|
+
const rawTimeLeft = env[PIPELINE_TIME_LEFT_ENV];
|
|
268
|
+
const timeLeftMs = parsePipelineTimeLeft(rawTimeLeft);
|
|
269
|
+
if (rawTimeLeft?.trim() && timeLeftMs === undefined) {
|
|
270
|
+
writeErr(`Ignoring ${PIPELINE_TIME_LEFT_ENV}="${rawTimeLeft}": not a millisecond count. Running without a deadline.`);
|
|
271
|
+
}
|
|
272
|
+
const deadlineMs = timeLeftMs === undefined ? undefined : (deps.now ?? Date.now)() + timeLeftMs;
|
|
273
|
+
const budget = deadlineMs === undefined
|
|
274
|
+
? deps.budget
|
|
275
|
+
: { ...(deps.budget ?? DEFAULT_RUN_BUDGET), deadlineMs: deps.budget?.deadlineMs ?? deadlineMs };
|
|
260
276
|
const cwd = args.cwd ?? deps.cwd ?? process.cwd();
|
|
261
277
|
const emit = (obj) => write(JSON.stringify(obj));
|
|
262
278
|
const stream = args.json;
|
|
@@ -284,7 +300,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
284
300
|
...(args.runId ? { runId: args.runId } : {}),
|
|
285
301
|
},
|
|
286
302
|
...(deps.signal ? { signal: deps.signal } : {}),
|
|
287
|
-
...(
|
|
303
|
+
...(budget ? { budget } : {}),
|
|
288
304
|
...(deps.stages ? { stages: deps.stages } : {}),
|
|
289
305
|
grounded: deps.grounded ?? grounding,
|
|
290
306
|
...(mission ? { mission } : {}),
|
|
@@ -65,6 +65,8 @@ export interface RunPipelineDeps {
|
|
|
65
65
|
* with `budget_exceeded` rather than spending on. Tunable per lane / per caller.
|
|
66
66
|
*/
|
|
67
67
|
budget?: RunBudget;
|
|
68
|
+
/** Wall clock for the budget's deadline checks (epoch ms). Defaults to `Date.now`; injected in tests. */
|
|
69
|
+
now?: () => number;
|
|
68
70
|
/**
|
|
69
71
|
* M6 eval: the stage list to run. Defaults to selectStages("full"); the blind
|
|
70
72
|
* lane passes a grounding-tool-stripped copy (see eval.ts#blindStages).
|
|
@@ -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, EMPTY_RUN_USAGE, exceedsBudget } from "./budget.js";
|
|
27
|
+
import { addRunUsage, aggregateRunUsage, DEFAULT_RUN_BUDGET, DEFAULT_STAGE_ESTIMATE_MS, EMPTY_RUN_USAGE, exceedsBudget, 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";
|
|
@@ -329,7 +329,14 @@ export async function runPipeline(ticket, deps) {
|
|
|
329
329
|
let baselinePaths;
|
|
330
330
|
const priorUsage = resume?.priorUsage ?? EMPTY_RUN_USAGE;
|
|
331
331
|
const currentUsage = () => addRunUsage(priorUsage, aggregateRunUsage(stages, rounds));
|
|
332
|
-
const
|
|
332
|
+
const clock = deps.now ?? Date.now;
|
|
333
|
+
const overBudget = () => exceedsBudget(currentUsage(), budget, clock());
|
|
334
|
+
// How long each timed stage kind last took THIS run. With a deadline, a review
|
|
335
|
+
// round or fix is only started when its last observed duration (or the
|
|
336
|
+
// conservative default, before one has run) still fits: a stage killed partway
|
|
337
|
+
// through by the harness leaves a half-edited tree and no verdict at all.
|
|
338
|
+
const observedMs = {};
|
|
339
|
+
const wontFit = (kind) => stageWontFit(budget, kind, observedMs[kind] ?? DEFAULT_STAGE_ESTIMATE_MS[kind], clock());
|
|
333
340
|
// The map stage's brief, kept for the partitioner's repo context. Absent on a
|
|
334
341
|
// mission run (map is skipped there — the injected memo stands in) and on any
|
|
335
342
|
// run whose map stage did not produce one; the partition contract is explicit
|
|
@@ -848,6 +855,13 @@ export async function runPipeline(ticket, deps) {
|
|
|
848
855
|
writeCheckpoint(mkRecord("build_done", { snapshot: afterBuild, reviewInput, usage: currentUsage() }));
|
|
849
856
|
}
|
|
850
857
|
while (true) {
|
|
858
|
+
const reviewWontFit = wontFit("review");
|
|
859
|
+
if (reviewWontFit) {
|
|
860
|
+
stopReason = "budget_exceeded";
|
|
861
|
+
log("pipeline_stop", { reason: "budget_exceeded", phase: "review", round, detail: reviewWontFit });
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
const reviewStartedAt = clock();
|
|
851
865
|
progress({ kind: "stage_start", stageId: "review", round });
|
|
852
866
|
stageEvent("review", "start", { round });
|
|
853
867
|
// P4 verify runs CONCURRENTLY with the lens fan-out: it is deterministic,
|
|
@@ -1003,6 +1017,7 @@ export async function runPipeline(ticket, deps) {
|
|
|
1003
1017
|
if (degradedLenses)
|
|
1004
1018
|
log("review_degraded", { round, degradedLenses });
|
|
1005
1019
|
progress({ kind: "stage_done", stageId: "review", round });
|
|
1020
|
+
observedMs.review = clock() - reviewStartedAt;
|
|
1006
1021
|
// R3-b: stop honestly if the review fan-out pushed the run over budget, before
|
|
1007
1022
|
// spending another fix + round. The round's findings are already recorded.
|
|
1008
1023
|
const overAfterReview = overBudget();
|
|
@@ -1016,9 +1031,19 @@ export async function runPipeline(ticket, deps) {
|
|
|
1016
1031
|
stopReason = decision.reason;
|
|
1017
1032
|
break;
|
|
1018
1033
|
}
|
|
1034
|
+
// The review's findings are recorded; a fix that cannot finish in time would
|
|
1035
|
+
// only leave them half-applied.
|
|
1036
|
+
const fixWontFit = wontFit("fix");
|
|
1037
|
+
if (fixWontFit) {
|
|
1038
|
+
stopReason = "budget_exceeded";
|
|
1039
|
+
log("pipeline_stop", { reason: "budget_exceeded", phase: "fix", round, detail: fixWontFit });
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
const fixStartedAt = clock();
|
|
1019
1043
|
progress({ kind: "stage_start", stageId: "fix", round });
|
|
1020
1044
|
stageEvent("fix", "start", { round });
|
|
1021
1045
|
const fixResult = await runStageFn(fix, { ticket: promptTicket, previous: serializeBlocking(findings), grounded }, stageDepsFor({ stageId: "fix", round }));
|
|
1046
|
+
observedMs.fix = clock() - fixStartedAt;
|
|
1022
1047
|
stages.push(fixResult);
|
|
1023
1048
|
log("stage_done", { stageId: "fix", exitCode: fixResult.exitCode });
|
|
1024
1049
|
if (deps.signal?.aborted) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.2.2-staging.
|
|
3
|
+
"version": "1.2.2-staging.1537.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": "4213ac8b3bfc84b5ba97f3163242f943fb9bde9a"
|
|
62
62
|
}
|