@yagni-app/code-staging 1.2.6-staging.1973.1 → 1.2.6-staging.1982.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/index.js +5 -0
- package/dist/extension/pipeline/orchestrator.js +7 -0
- package/dist/extension/pipeline/personas.js +1 -1
- package/dist/extension/pipeline/runner.d.ts +2 -0
- package/dist/extension/pipeline/runner.js +21 -0
- package/dist/extension/pipeline/stageClock.d.ts +31 -0
- package/dist/extension/pipeline/stageClock.js +43 -0
- package/dist/extension/pipeline/stages.js +22 -7
- package/dist/extension/pipeline/types.d.ts +9 -0
- package/package.json +2 -2
package/dist/extension/index.js
CHANGED
|
@@ -47,6 +47,7 @@ import { createEscapeTally } from "./sandbox/escapeTally.js";
|
|
|
47
47
|
import { createGrantCensus } from "./permission/grantCensus.js";
|
|
48
48
|
import { runsInSandboxHarness, toolchainSpawnHookFromEnv, toolchainUserBashOperations } from "./toolchain/spawnHook.js";
|
|
49
49
|
import { CLOUD_SESSION_ENV } from "./pipeline/cloud.js";
|
|
50
|
+
import { registerStageClock } from "./pipeline/stageClock.js";
|
|
50
51
|
import { registerTelemetry as defaultRegisterTelemetry } from "./telemetry/register.js";
|
|
51
52
|
import { loadHooksConfig, makeHookRunner, registerHooks } from "./hooks.js";
|
|
52
53
|
import { loadPermissionRules } from "./permissionRules/loadConfig.js";
|
|
@@ -159,6 +160,10 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
159
160
|
const fetchCatalog = deps.fetchCatalog ?? defaultFetchCatalog;
|
|
160
161
|
const env = deps.env ?? process.env;
|
|
161
162
|
const evalMode = isEvalMode(env);
|
|
163
|
+
// A timed /go stage child refuses every tool once its time is up. Order does
|
|
164
|
+
// not matter: pi runs every tool_call handler until one blocks, and an allow
|
|
165
|
+
// (a result without `block`) never ends the loop, so this block always lands.
|
|
166
|
+
registerStageClock(pi, env, now);
|
|
162
167
|
// The grounding settings switch (the judgment-loop kill switch): resolved
|
|
163
168
|
// ONCE at boot from the USER-tier config (default on; a malformed config
|
|
164
169
|
// fails soft to on with a sink warn). Eval mode is its own carve-out and
|
|
@@ -1074,6 +1074,13 @@ export async function runPipeline(ticket, deps) {
|
|
|
1074
1074
|
stages.push(r);
|
|
1075
1075
|
log("stage_done", { stageId: stage.id, exitCode: r.exitCode, attempt: 2 });
|
|
1076
1076
|
}
|
|
1077
|
+
// A stage stopped at its own clock (`stageClock.ts`) spent its time
|
|
1078
|
+
// without writing an answer: the run's time budget, not a crash.
|
|
1079
|
+
if (r.timedOut && !deps.signal?.aborted) {
|
|
1080
|
+
log("pipeline_stop", { reason: "budget_exceeded", where: stage.id, detail: "stage_clock" });
|
|
1081
|
+
progress({ kind: "done", stopReason: "budget_exceeded" });
|
|
1082
|
+
return { stages, rounds, stopReason: "budget_exceeded", findings: [], decisionsRecorded };
|
|
1083
|
+
}
|
|
1077
1084
|
const credit = creditExhaustionReason(r);
|
|
1078
1085
|
if (isFailed(r)) {
|
|
1079
1086
|
const why = credit ??
|
|
@@ -67,7 +67,7 @@ Keep it concrete; the worker executes it verbatim.`;
|
|
|
67
67
|
* `plan-persona.md`. Its output contract rides the stage's task, so a host
|
|
68
68
|
* persona that replaces this body keeps it.
|
|
69
69
|
*/
|
|
70
|
-
const CLOUD_PLANNER_ROLE = `You are a planning specialist. You plan one ticket in the repository checked out in your working directory
|
|
70
|
+
const CLOUD_PLANNER_ROLE = `You are a planning specialist. You plan one ticket in the repository checked out in your working directory, by reading it: you run none of its commands. Your plan is what a builder executes, and your questions are what a person answers before anything is built. You do not change files. A fact you can settle by reading code is settled, not asked.`;
|
|
71
71
|
const CLOUD_PLANNER_GROUNDING = `You are grounded. Call ask_yagni before inferring a convention or making any organization- or codebase-specific assumption. To check a fact outside this repository (what is deployed, what the data holds, what a ticket or page says), call team_read with the Team's connected tools before you ask a person.`;
|
|
72
72
|
const CLOUD_PLANNER_FINISH = `Complete the task fully: do not gold-plate, but do not leave it half-done. Finish the job in ONE turn: do not end your turn on an interstitial like "now let me check X". Your FINAL message MUST be the complete plan in the format the task gives. Explore until you can name the files and the steps, then write the plan.`;
|
|
73
73
|
const CLOUD_PLANNER_BODY = [CLOUD_PLANNER_ROLE, CLOUD_PLANNER_GROUNDING, CLOUD_PLANNER_FINISH].join("\n\n");
|
|
@@ -96,6 +96,8 @@ export interface RunStageDeps {
|
|
|
96
96
|
personaBody?: (stage: PipelineStage, opts: {
|
|
97
97
|
grounded?: boolean;
|
|
98
98
|
}) => string;
|
|
99
|
+
/** The clock a timed stage's deadline is read from; tests inject one. */
|
|
100
|
+
now?: () => number;
|
|
99
101
|
}
|
|
100
102
|
/**
|
|
101
103
|
* Default per-line stdout ceiling (16 MiB). A legitimate NDJSON event — even one
|
|
@@ -29,6 +29,7 @@ import { finalOutputFrom, foldEvent, newEventAccumulator } from "./events.js";
|
|
|
29
29
|
import { logEvent } from "../errorSink.js";
|
|
30
30
|
import { buildStageInvocation, groundedChildArgv } from "./invocation.js";
|
|
31
31
|
import { personaBody } from "./personas.js";
|
|
32
|
+
import { STAGE_WRAP_UP_AT_ENV, STAGE_WRAP_UP_GRACE_MS } from "./stageClock.js";
|
|
32
33
|
import { clampTier, resolveTierCap } from "./tierCap.js";
|
|
33
34
|
import { PER_TASK_OUTPUT_CAP, } from "./types.js";
|
|
34
35
|
/**
|
|
@@ -159,10 +160,17 @@ export async function runStage(stage, ctx, deps) {
|
|
|
159
160
|
// supplied one — its absence just means this child's completions attribute
|
|
160
161
|
// to the session id alone.
|
|
161
162
|
const callerLabel = deps.callerLabel ?? (ctx.lens ? `go:${stage.id}:${ctx.lens}` : `go:${stage.id}`);
|
|
163
|
+
// A timed stage (`stageClock.ts`): the child refuses tools from
|
|
164
|
+
// `wrapUpAt`, and is stopped a grace after it.
|
|
165
|
+
const wrapUpAt = stage.timeBudgetMs !== undefined && stage.timeBudgetMs > 0
|
|
166
|
+
? (deps.now ?? Date.now)() + stage.timeBudgetMs
|
|
167
|
+
: undefined;
|
|
168
|
+
let timedOut = false;
|
|
162
169
|
const childEnv = {
|
|
163
170
|
...envForCap,
|
|
164
171
|
YAGNI_CALLER: callerLabel,
|
|
165
172
|
...(deps.attribution?.runId ? { YAGNI_RUN_ID: deps.attribution.runId } : {}),
|
|
173
|
+
...(wrapUpAt !== undefined ? { [STAGE_WRAP_UP_AT_ENV]: String(wrapUpAt) } : {}),
|
|
166
174
|
};
|
|
167
175
|
// The child uses this exact Node binary. Forward only CA trust, never
|
|
168
176
|
// unrelated parent loaders/inspectors or a flag in inherited NODE_OPTIONS.
|
|
@@ -221,10 +229,21 @@ export async function runStage(stage, ctx, deps) {
|
|
|
221
229
|
}, 5000);
|
|
222
230
|
killTimer.unref?.();
|
|
223
231
|
};
|
|
232
|
+
const clock = wrapUpAt !== undefined && stage.timeBudgetMs !== undefined
|
|
233
|
+
? setTimeout(() => {
|
|
234
|
+
if (closed)
|
|
235
|
+
return;
|
|
236
|
+
timedOut = true;
|
|
237
|
+
onAbort();
|
|
238
|
+
}, stage.timeBudgetMs + STAGE_WRAP_UP_GRACE_MS)
|
|
239
|
+
: undefined;
|
|
240
|
+
clock?.unref?.();
|
|
224
241
|
const finish = (code) => {
|
|
225
242
|
closed = true;
|
|
226
243
|
if (killTimer)
|
|
227
244
|
clearTimeout(killTimer);
|
|
245
|
+
if (clock)
|
|
246
|
+
clearTimeout(clock);
|
|
228
247
|
// Detach the abort listener on normal completion so it does not
|
|
229
248
|
// accumulate on the shared, long-lived pipeline signal across stages.
|
|
230
249
|
if (signal)
|
|
@@ -309,6 +328,8 @@ export async function runStage(stage, ctx, deps) {
|
|
|
309
328
|
result.errorMessage = acc.errorMessage;
|
|
310
329
|
if (wasAborted)
|
|
311
330
|
result.stopReason = "aborted";
|
|
331
|
+
if (timedOut)
|
|
332
|
+
result.timedOut = true;
|
|
312
333
|
if (overlongLinesDropped > 0)
|
|
313
334
|
result.overlongLinesDropped = overlongLinesDropped;
|
|
314
335
|
if (DEBUG_DIR) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stage's own clock (`PipelineStage.timeBudgetMs`). The cloud plan stage
|
|
3
|
+
* once ran 40 minutes and more building the app, starting its server and
|
|
4
|
+
* copying the repository to try route layouts (staging, 2026-10-23), with
|
|
5
|
+
* nothing but the session's 6 hour window to stop it.
|
|
6
|
+
*
|
|
7
|
+
* Two halves, the scoping session's shape (PR #1994): the runner hands the
|
|
8
|
+
* child the moment its time is up (`YAGNI_STAGE_WRAP_UP_AT`, epoch ms on the
|
|
9
|
+
* sandbox's own clock, which parent and child share), and the child's
|
|
10
|
+
* extension refuses every tool call from then on with words that tell the
|
|
11
|
+
* model to write its answer now. A grace later the runner stops the child
|
|
12
|
+
* (`runner.ts`), and the orchestrator ends the run `budget_exceeded`.
|
|
13
|
+
*/
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
/** The child's env: the epoch millisecond its stage's time is up. */
|
|
16
|
+
export declare const STAGE_WRAP_UP_AT_ENV = "YAGNI_STAGE_WRAP_UP_AT";
|
|
17
|
+
/** The cloud plan stage's time, in minutes. Plans on staging took 8 to 16 minutes. */
|
|
18
|
+
export declare const CLOUD_PLAN_MINUTES = 20;
|
|
19
|
+
/** How long past its time a stage child may take to write its answer before it is stopped. */
|
|
20
|
+
export declare const STAGE_WRAP_UP_GRACE_MS: number;
|
|
21
|
+
/** The refusal a tool call gets once the stage's time is up. */
|
|
22
|
+
export declare const STAGE_TIME_UP_REASON: string;
|
|
23
|
+
/** The epoch millisecond in the env, or undefined for absent or garbage. */
|
|
24
|
+
export declare function stageWrapUpAt(env: NodeJS.ProcessEnv): number | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Refuse every tool call once the stage's time is up. Registered by every
|
|
27
|
+
* pi process the extension loads in; a process whose env carries no time
|
|
28
|
+
* (every stage but a timed one, and every interactive session) gets nothing.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerStageClock(pi: Pick<ExtensionAPI, "on">, env: NodeJS.ProcessEnv, now?: () => number): void;
|
|
31
|
+
//# sourceMappingURL=stageClock.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stage's own clock (`PipelineStage.timeBudgetMs`). The cloud plan stage
|
|
3
|
+
* once ran 40 minutes and more building the app, starting its server and
|
|
4
|
+
* copying the repository to try route layouts (staging, 2026-10-23), with
|
|
5
|
+
* nothing but the session's 6 hour window to stop it.
|
|
6
|
+
*
|
|
7
|
+
* Two halves, the scoping session's shape (PR #1994): the runner hands the
|
|
8
|
+
* child the moment its time is up (`YAGNI_STAGE_WRAP_UP_AT`, epoch ms on the
|
|
9
|
+
* sandbox's own clock, which parent and child share), and the child's
|
|
10
|
+
* extension refuses every tool call from then on with words that tell the
|
|
11
|
+
* model to write its answer now. A grace later the runner stops the child
|
|
12
|
+
* (`runner.ts`), and the orchestrator ends the run `budget_exceeded`.
|
|
13
|
+
*/
|
|
14
|
+
/** The child's env: the epoch millisecond its stage's time is up. */
|
|
15
|
+
export const STAGE_WRAP_UP_AT_ENV = "YAGNI_STAGE_WRAP_UP_AT";
|
|
16
|
+
/** The cloud plan stage's time, in minutes. Plans on staging took 8 to 16 minutes. */
|
|
17
|
+
export const CLOUD_PLAN_MINUTES = 20;
|
|
18
|
+
/** How long past its time a stage child may take to write its answer before it is stopped. */
|
|
19
|
+
export const STAGE_WRAP_UP_GRACE_MS = 10 * 60_000;
|
|
20
|
+
/** The refusal a tool call gets once the stage's time is up. */
|
|
21
|
+
export const STAGE_TIME_UP_REASON = "Your time for this stage is up, so no more tools run. Write your final answer now from what you " +
|
|
22
|
+
"have already read, in the format the task gives. State anything you could not check as an " +
|
|
23
|
+
"assumption the build verifies.";
|
|
24
|
+
/** The epoch millisecond in the env, or undefined for absent or garbage. */
|
|
25
|
+
export function stageWrapUpAt(env) {
|
|
26
|
+
const raw = env[STAGE_WRAP_UP_AT_ENV]?.trim();
|
|
27
|
+
if (!raw || !/^\d+$/.test(raw))
|
|
28
|
+
return undefined;
|
|
29
|
+
const value = Number(raw);
|
|
30
|
+
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Refuse every tool call once the stage's time is up. Registered by every
|
|
34
|
+
* pi process the extension loads in; a process whose env carries no time
|
|
35
|
+
* (every stage but a timed one, and every interactive session) gets nothing.
|
|
36
|
+
*/
|
|
37
|
+
export function registerStageClock(pi, env, now = Date.now) {
|
|
38
|
+
const wrapUpAt = stageWrapUpAt(env);
|
|
39
|
+
if (wrapUpAt === undefined)
|
|
40
|
+
return;
|
|
41
|
+
pi.on("tool_call", async () => now() >= wrapUpAt ? { block: true, reason: STAGE_TIME_UP_REASON } : undefined);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=stageClock.js.map
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { sanitizeCallerSegment } from "../config.js";
|
|
30
30
|
import { builderPersonaClause } from "./personas.js";
|
|
31
|
+
import { CLOUD_PLAN_MINUTES } from "./stageClock.js";
|
|
31
32
|
import { MAX_REVIEW_ROUNDS, } from "./types.js";
|
|
32
33
|
/** The three adversarial review angles, fanned out per round (spec §3). */
|
|
33
34
|
export const REVIEW_LENSES = ["correctness", "business_fit", "does_it_hold"];
|
|
@@ -169,21 +170,34 @@ export const PLAN_JSON_CONTRACT = "End your answer with a fenced ```plan-json bl
|
|
|
169
170
|
'- "decisionsCited" lists only decisions the read returned that the plan relies on. Omit what you do not use.';
|
|
170
171
|
/**
|
|
171
172
|
* The cloud plan stage's task: the input folder the host wrote, read first,
|
|
172
|
-
* then a plan
|
|
173
|
-
*
|
|
173
|
+
* then a plan grounded in the installed checkout, inside the stage's own
|
|
174
|
+
* clock (`stageClock.ts`). Planning is reading: on staging (2026-10-23) one
|
|
175
|
+
* plan stage built the app and copied the repository for 40 minutes and more,
|
|
176
|
+
* and another started Docker and Supabase until the disk was full and then
|
|
177
|
+
* pruned images to go on. `inputDir` is the folder the ticket file sits in.
|
|
174
178
|
*/
|
|
175
179
|
function cloudPlanTask(inputDir) {
|
|
176
180
|
const at = (file) => `${inputDir.replace(/\/+$/, "")}/${file}`;
|
|
177
|
-
return ("Plan this ticket on the repository checked out in your working directory.
|
|
178
|
-
"
|
|
181
|
+
return ("Plan this ticket on the repository checked out in your working directory. Nothing is built or run " +
|
|
182
|
+
"until the plan is approved, so change nothing in the repository.\n\n" +
|
|
183
|
+
`You have about ${CLOUD_PLAN_MINUTES} minutes. Past them every tool call is refused and you write the plan ` +
|
|
184
|
+
"from what you have already read, so read first and write the plan as soon as you can name the files " +
|
|
185
|
+
"and the steps.\n\n" +
|
|
179
186
|
"Read first:\n" +
|
|
180
187
|
`- ${at("ticket.md")}: the ticket and the evidence behind it. Open every image it names with the read tool.\n` +
|
|
181
188
|
`- ${at("design.md")} and ${at("images")}: the linked design and its frames, when they exist.\n` +
|
|
182
189
|
`- ${at("memo.md")}: what runs in this repository.\n` +
|
|
183
190
|
`- ${at("checks.json")}: the repository's checks, and which already fail on the unchanged code.\n` +
|
|
184
|
-
`- ${at("playbook.md")}: the Team's planning rules
|
|
185
|
-
`- ${at("plan-line.md")}: this Team's instructions for planning
|
|
186
|
-
"
|
|
191
|
+
`- ${at("playbook.md")}: the Team's planning rules. Always follow them.\n` +
|
|
192
|
+
`- ${at("plan-line.md")}: this Team's instructions for planning.\n\n` +
|
|
193
|
+
"Planning is reading code, nothing else. Settle a fact by reading files, grepping, listing folders, and " +
|
|
194
|
+
"reading the git history and the config. Run no tests and no type checks, build nothing, start no server, " +
|
|
195
|
+
"no Docker and no database (`supabase start` included), and do not copy the repository: those are the " +
|
|
196
|
+
"build's, and here they spend the time the plan needs and fill the sandbox's disk. When only running " +
|
|
197
|
+
"something would settle a fact, state it as an assumption and put the command that settles it in the " +
|
|
198
|
+
"plan's checks, which the build runs.\n\n" +
|
|
199
|
+
'If a command fails with "no space left on device", the disk is full: stop there. Do not delete files, ' +
|
|
200
|
+
"prune Docker or retry. Write the plan and say what you could not check.\n\n" +
|
|
187
201
|
"When the work depends on another repository the ticket's Other repositories section lists, clone it " +
|
|
188
202
|
"read-only on demand with `git clone --filter=blob:none --depth 1` into ~/context/<repo> and read it there.\n\n" +
|
|
189
203
|
`Call team_read with tool "${DECISIONS_READ_TOOL}" before you ask a person anything: a question an earlier ` +
|
|
@@ -215,6 +229,7 @@ export function cloudLane(inputDir) {
|
|
|
215
229
|
model: "peak",
|
|
216
230
|
tools: ["read", "grep", "find", "ls", "bash", "ask_yagni", "team_read", "prototype_read"],
|
|
217
231
|
taskTemplate: cloudPlanTask(inputDir),
|
|
232
|
+
timeBudgetMs: CLOUD_PLAN_MINUTES * 60_000,
|
|
218
233
|
},
|
|
219
234
|
{ ...implementStage(), model: "advanced" },
|
|
220
235
|
reviewStage(),
|
|
@@ -72,6 +72,13 @@ export interface PipelineStage {
|
|
|
72
72
|
* plan stage takes the host's `plan-persona.md` (Reeve's persona) here.
|
|
73
73
|
*/
|
|
74
74
|
persona?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The stage's own clock, in milliseconds (`stageClock.ts`). Past it the
|
|
77
|
+
* child refuses every tool call, so the model writes its answer from what it
|
|
78
|
+
* has; a grace later the runner stops the child and the result says
|
|
79
|
+
* `timedOut`. Absent on every stage but the cloud plan stage.
|
|
80
|
+
*/
|
|
81
|
+
timeBudgetMs?: number;
|
|
75
82
|
}
|
|
76
83
|
/**
|
|
77
84
|
* A stage list and the loop rules that go with it. The `full` lane is what
|
|
@@ -148,6 +155,8 @@ export interface StageResult {
|
|
|
148
155
|
* whole pipeline. Absent/undefined when nothing was dropped (the normal case).
|
|
149
156
|
*/
|
|
150
157
|
overlongLinesDropped?: number;
|
|
158
|
+
/** Set when the runner stopped the child at its {@link PipelineStage.timeBudgetMs} plus the grace. */
|
|
159
|
+
timedOut?: boolean;
|
|
151
160
|
}
|
|
152
161
|
/** One round of the bounded review→fix loop. */
|
|
153
162
|
export interface ReviewRound {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.2.6-staging.
|
|
3
|
+
"version": "1.2.6-staging.1982.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": "444aad8e6af41a68786645595b40d828477d3bff"
|
|
62
62
|
}
|