@yagni-app/code-staging 1.2.3-staging.1570.1 → 1.2.3-staging.1580.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/askAdvisorTool.d.ts +6 -1
- package/dist/extension/askAdvisorTool.js +6 -1
- package/dist/extension/index.d.ts +2 -0
- package/dist/extension/index.js +24 -0
- package/dist/extension/permission/gate.js +3 -3
- package/dist/extension/pipeline/budget.d.ts +32 -0
- package/dist/extension/pipeline/budget.js +29 -0
- package/dist/extension/pipeline/goCommand.d.ts +3 -0
- package/dist/extension/pipeline/goCommand.js +30 -0
- package/dist/extension/pipeline/orchestrator.d.ts +22 -1
- package/dist/extension/pipeline/orchestrator.js +42 -9
- package/dist/extension/pipeline/resilience.d.ts +18 -0
- package/dist/extension/pipeline/resilience.js +6 -2
- package/dist/extension/pipeline/stages.js +5 -5
- package/dist/extension/pipeline/types.d.ts +9 -6
- package/dist/extension/pipeline/types.js +8 -5
- package/dist/extension/prototypeTool.d.ts +151 -0
- package/dist/extension/prototypeTool.js +330 -0
- package/dist/extension/prototypeVerify.d.ts +77 -0
- package/dist/extension/prototypeVerify.js +219 -0
- package/dist/extension/subagents.d.ts +3 -2
- package/dist/extension/subagents.js +3 -2
- package/package.json +2 -2
|
@@ -32,13 +32,18 @@ import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
|
|
|
32
32
|
import type { ChildUsageHandle } from "./childUsage.js";
|
|
33
33
|
import type { WorkingLineHandle } from "./workingLine.js";
|
|
34
34
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
35
|
-
import { type PipelineStage } from "./pipeline/types.js";
|
|
35
|
+
import { type PipelineStage, type ResiliencePolicy } from "./pipeline/types.js";
|
|
36
36
|
import { type RenderTheme, type SubagentTaskProgress } from "./subagentRender.js";
|
|
37
37
|
/**
|
|
38
38
|
* Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
|
|
39
39
|
* reason documented there: enough tools to navigate instead of flailing on a
|
|
40
40
|
* single `read`. No `bash`.
|
|
41
41
|
*/
|
|
42
|
+
/**
|
|
43
|
+
* A consult is one read-only question, so it keeps the 20-minute wall clock
|
|
44
|
+
* the /go stages had before their default grew to an hour.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ADVISOR_RESILIENCE_POLICY: ResiliencePolicy;
|
|
42
47
|
export declare const ADVISOR_TOOLS: string[];
|
|
43
48
|
/** The consult's tool list under `grounding: false` — grounding read
|
|
44
49
|
* stripped via the canonical filter (grounding.ts). */
|
|
@@ -40,6 +40,11 @@ import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, p
|
|
|
40
40
|
* reason documented there: enough tools to navigate instead of flailing on a
|
|
41
41
|
* single `read`. No `bash`.
|
|
42
42
|
*/
|
|
43
|
+
/**
|
|
44
|
+
* A consult is one read-only question, so it keeps the 20-minute wall clock
|
|
45
|
+
* the /go stages had before their default grew to an hour.
|
|
46
|
+
*/
|
|
47
|
+
export const ADVISOR_RESILIENCE_POLICY = { ...DEFAULT_RESILIENCE_POLICY, wallTimeoutMs: 20 * 60_000 };
|
|
43
48
|
export const ADVISOR_TOOLS = ["read", "grep", "find", "ls", "ask_yagni"];
|
|
44
49
|
/** The consult's tool list under `grounding: false` — grounding read
|
|
45
50
|
* stripped via the canonical filter (grounding.ts). */
|
|
@@ -144,7 +149,7 @@ export function makeAskAdvisorTool(opts) {
|
|
|
144
149
|
// driver's tool call until the user presses Esc. The synthetic stage id is
|
|
145
150
|
// "plan" (read-only tools, no bash), so the wrapper's write-gate never
|
|
146
151
|
// blocks a retry — re-running a consult cannot double-apply anything.
|
|
147
|
-
const runStage = opts.runStage ?? withResilience(defaultRunStage,
|
|
152
|
+
const runStage = opts.runStage ?? withResilience(defaultRunStage, ADVISOR_RESILIENCE_POLICY);
|
|
148
153
|
return {
|
|
149
154
|
name: "ask_advisor",
|
|
150
155
|
label: "Ask the advisor",
|
|
@@ -166,6 +166,8 @@ export type { MakeRecordContextToolOptions } from "./recordContextTool.js";
|
|
|
166
166
|
export { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
167
167
|
export type { MakeRecordDecisionToolOptions } from "./recordDecisionTool.js";
|
|
168
168
|
export { makeSuggestNextWorkTool, defaultNextAction } from "./nextWorkTool.js";
|
|
169
|
+
export { makePrototypeCreateTool, makePrototypeAttachTool, makePrototypeReadTool, makePrototypeDraftTool, makePrototypeVerificationTool, makePrototypeReviseTool, makePrototypeListTool, makePrototypeShareTool, } from "./prototypeTool.js";
|
|
170
|
+
export type { MakePrototypeToolOptions, PrototypeSummary } from "./prototypeTool.js";
|
|
169
171
|
export type { MakeNextWorkToolOptions, DefaultNextAction, NextActionOption, } from "./nextWorkTool.js";
|
|
170
172
|
export { recordDecision } from "./recordDecisionTool.js";
|
|
171
173
|
export type { RecordDecisionParams } from "./recordDecisionTool.js";
|
package/dist/extension/index.js
CHANGED
|
@@ -13,8 +13,10 @@ import { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js
|
|
|
13
13
|
import { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
14
14
|
import { registerCmuxBridge } from "./cmux/index.js";
|
|
15
15
|
import { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
16
|
+
import { makePrototypeVerifyTool } from "./prototypeVerify.js";
|
|
16
17
|
import { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
17
18
|
import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
|
|
19
|
+
import { makePrototypeCreateTool, makePrototypeAttachTool, makePrototypeReadTool, makePrototypeDraftTool, makePrototypeVerificationTool, makePrototypeReviseTool, makePrototypeListTool, makePrototypeShareTool, } from "./prototypeTool.js";
|
|
18
20
|
import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_DRIVER_GROUNDING_FREE, YAGNI_IDENTITY_GROUNDING_FREE, YAGNI_IDENTITY_ULTRA, YAGNI_IDENTITY_ULTRA_GROUNDING_FREE } from "./branding.js";
|
|
19
21
|
import { claudeRulesSection } from "./claudeRules.js";
|
|
20
22
|
import { ensureScratchpadDir, SCRATCHPAD_TMPDIR_ENV, scratchpadDir as scratchpadDirFor, scratchpadOwnerRootFor, scratchpadSection } from "./scratchpad.js";
|
|
@@ -408,6 +410,25 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
408
410
|
onRecorded: (id) => recordedThisSession.add(id),
|
|
409
411
|
}));
|
|
410
412
|
}
|
|
413
|
+
if (!evalMode) {
|
|
414
|
+
// Prototypes v1: create/revise/list/share runnable single-file HTML
|
|
415
|
+
// Prototypes through the app's REST API. Workspace writes, so gated out of
|
|
416
|
+
// eval mode with the other external-side-effect tools.
|
|
417
|
+
pi.registerTool(makePrototypeReadTool(toolOpts));
|
|
418
|
+
pi.registerTool(makePrototypeDraftTool({ ...toolOpts, getSessionId: () => env.YAGNI_SESSION_ID }));
|
|
419
|
+
pi.registerTool(makePrototypeVerificationTool(toolOpts));
|
|
420
|
+
pi.registerTool(makePrototypeVerifyTool(toolOpts));
|
|
421
|
+
pi.registerTool(makePrototypeCreateTool({ ...toolOpts, getSessionId: () => env.YAGNI_SESSION_ID }));
|
|
422
|
+
pi.registerTool(makePrototypeAttachTool(toolOpts));
|
|
423
|
+
pi.registerTool(makePrototypeReviseTool(toolOpts));
|
|
424
|
+
pi.registerTool(makePrototypeListTool(toolOpts));
|
|
425
|
+
pi.registerTool(makePrototypeShareTool(toolOpts));
|
|
426
|
+
}
|
|
427
|
+
else if (env.YAGNI_MISSION_DIR && env.YAGNI_RUN_ID) {
|
|
428
|
+
// The server authorizes only exact revisions in this Run's approved packet.
|
|
429
|
+
pi.registerTool(makePrototypeReadTool(toolOpts));
|
|
430
|
+
pi.registerTool(makePrototypeVerifyTool(toolOpts));
|
|
431
|
+
}
|
|
411
432
|
// YAG-574: the silent-turn reminder, driver-only. A child/subagent/advisor
|
|
412
433
|
// process has no direct user to answer, so it is never nudged (same gating
|
|
413
434
|
// as the delegation identity in branding.ts); eval mode is untouched so its
|
|
@@ -444,6 +465,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
444
465
|
// map → plan → implement → review → fix, each child grounded by inheritance.
|
|
445
466
|
registerGoCommand(pi, {
|
|
446
467
|
childUsage,
|
|
468
|
+
canPrepareDesign: () => !evalMode && modeHolder.get() === "auto",
|
|
447
469
|
attribution,
|
|
448
470
|
// The grounding switch threads into every /go run: `grounded: false`
|
|
449
471
|
// selects the existing blind personas + blind lens clauses (the M6
|
|
@@ -2029,6 +2051,8 @@ export { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
|
2029
2051
|
export { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
2030
2052
|
export { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
2031
2053
|
export { makeSuggestNextWorkTool, defaultNextAction } from "./nextWorkTool.js";
|
|
2054
|
+
// Prototypes v1: the four prototype tools (create/revise/list/share).
|
|
2055
|
+
export { makePrototypeCreateTool, makePrototypeAttachTool, makePrototypeReadTool, makePrototypeDraftTool, makePrototypeVerificationTool, makePrototypeReviseTool, makePrototypeListTool, makePrototypeShareTool, } from "./prototypeTool.js";
|
|
2032
2056
|
export { recordDecision } from "./recordDecisionTool.js";
|
|
2033
2057
|
// Onramp Door B (as amended by ADR-0033): the first-run welcome (write-free,
|
|
2034
2058
|
// free text first-class) + the explicit /setup-team drafting flow.
|
|
@@ -51,9 +51,9 @@ export function createModeHolder(initial = "auto") {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
export const DEFAULT_PERMISSION_POLICY = {
|
|
54
|
-
planBlockTools: ["write", "edit", "bash", "file_ticket", "update_ticket_status"],
|
|
55
|
-
reviewConfirmTools: ["write", "edit", "bash", "file_ticket", "update_ticket_status"],
|
|
56
|
-
alwaysConfirmTools: ["file_ticket", "update_ticket_status"],
|
|
54
|
+
planBlockTools: ["write", "edit", "bash", "file_ticket", "update_ticket_status", "prototype_share", "prototype_create", "prototype_attach", "prototype_revise", "prototype_draft", "prototype_verification", "prototype_verify"],
|
|
55
|
+
reviewConfirmTools: ["write", "edit", "bash", "file_ticket", "update_ticket_status", "prototype_share", "prototype_create", "prototype_attach", "prototype_revise", "prototype_draft", "prototype_verification", "prototype_verify"],
|
|
56
|
+
alwaysConfirmTools: ["file_ticket", "update_ticket_status", "prototype_share"],
|
|
57
57
|
};
|
|
58
58
|
export function allowRuleFloorVerdict(toolName, params, mode, policy) {
|
|
59
59
|
// Floor 0 — plan mode: the mode's contract (no mutations without Guardian
|
|
@@ -78,4 +78,36 @@ export declare const DEFAULT_STAGE_ESTIMATE_MS: Record<TimedStageKind, number>;
|
|
|
78
78
|
* so the only safe place to stop is before it begins.
|
|
79
79
|
*/
|
|
80
80
|
export declare function stageWontFit(budget: RunBudget, kind: TimedStageKind, expectedMs: number, nowMs?: number): string | null;
|
|
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.
|
|
85
|
+
*/
|
|
86
|
+
export declare const STAGE_WALL_FLOOR_MS: number;
|
|
87
|
+
/** Time the verify gate is allowed after the last write, on top of the stage estimates. */
|
|
88
|
+
export declare const VERIFY_ALLOWANCE_MS: number;
|
|
89
|
+
/**
|
|
90
|
+
* What must still run after a stage stops: a review round, one fix, and the
|
|
91
|
+
* verify gate. Held back from the stage's wall clock so a long stage still
|
|
92
|
+
* leaves the run time to review what it wrote.
|
|
93
|
+
*/
|
|
94
|
+
export declare const STAGE_WALL_RESERVE_MS: number;
|
|
95
|
+
/**
|
|
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.
|
|
98
|
+
*/
|
|
99
|
+
export type StageWallSource = "deadline" | "floor" | "default";
|
|
100
|
+
/**
|
|
101
|
+
* The wall clock for a stage spawning at `nowMs`. With a deadline (sandbox and
|
|
102
|
+
* mission runs) a working stage gets the build's remaining time less
|
|
103
|
+
* {@link STAGE_WALL_RESERVE_MS}, never under {@link STAGE_WALL_FLOOR_MS}: the
|
|
104
|
+
* 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
|
+
* (interactive /go, where a person can abort) `fallbackMs` applies. The
|
|
107
|
+
* source rides the stage's attempt record.
|
|
108
|
+
*/
|
|
109
|
+
export declare function stageWall(budget: RunBudget, fallbackMs: number, nowMs?: number): {
|
|
110
|
+
ms: number;
|
|
111
|
+
source: StageWallSource;
|
|
112
|
+
};
|
|
81
113
|
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -107,4 +107,33 @@ export function stageWontFit(budget, kind, expectedMs, nowMs = Date.now()) {
|
|
|
107
107
|
return null;
|
|
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
|
+
/**
|
|
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.
|
|
114
|
+
*/
|
|
115
|
+
export const STAGE_WALL_FLOOR_MS = 20 * 60_000;
|
|
116
|
+
/** Time the verify gate is allowed after the last write, on top of the stage estimates. */
|
|
117
|
+
export const VERIFY_ALLOWANCE_MS = 5 * 60_000;
|
|
118
|
+
/**
|
|
119
|
+
* What must still run after a stage stops: a review round, one fix, and the
|
|
120
|
+
* verify gate. Held back from the stage's wall clock so a long stage still
|
|
121
|
+
* leaves the run time to review what it wrote.
|
|
122
|
+
*/
|
|
123
|
+
export const STAGE_WALL_RESERVE_MS = DEFAULT_STAGE_ESTIMATE_MS.review + DEFAULT_STAGE_ESTIMATE_MS.fix + VERIFY_ALLOWANCE_MS;
|
|
124
|
+
/**
|
|
125
|
+
* The wall clock for a stage spawning at `nowMs`. With a deadline (sandbox and
|
|
126
|
+
* mission runs) a working stage gets the build's remaining time less
|
|
127
|
+
* {@link STAGE_WALL_RESERVE_MS}, never under {@link STAGE_WALL_FLOOR_MS}: the
|
|
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
|
|
130
|
+
* (interactive /go, where a person can abort) `fallbackMs` applies. The
|
|
131
|
+
* source rides the stage's attempt record.
|
|
132
|
+
*/
|
|
133
|
+
export function stageWall(budget, fallbackMs, nowMs = Date.now()) {
|
|
134
|
+
if (budget.deadlineMs === undefined)
|
|
135
|
+
return { ms: fallbackMs, source: "default" };
|
|
136
|
+
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" };
|
|
138
|
+
}
|
|
110
139
|
//# sourceMappingURL=budget.js.map
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
*/
|
|
61
61
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
62
62
|
import type { SpendResponse } from "../costHud.js";
|
|
63
|
+
import { draftPrototype } from "../prototypeTool.js";
|
|
63
64
|
import { type CrashReporter } from "../crashReport.js";
|
|
64
65
|
import { runFinish as defaultRunFinish } from "./finish.js";
|
|
65
66
|
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
@@ -144,6 +145,8 @@ export interface RegisterGoDeps {
|
|
|
144
145
|
* test) simply means "not ultra", which is today's behavior.
|
|
145
146
|
*/
|
|
146
147
|
isUltra?: () => boolean;
|
|
148
|
+
canPrepareDesign?: () => boolean;
|
|
149
|
+
draftPrototype?: typeof draftPrototype;
|
|
147
150
|
/**
|
|
148
151
|
* The boot-time `grounding` settings switch, threaded into the orchestrator's
|
|
149
152
|
* `grounded` flag: false runs the whole pipeline on the existing blind
|
|
@@ -69,6 +69,7 @@ import { RunState } from "./runState.js";
|
|
|
69
69
|
import { aggregateRunUsage } from "./budget.js";
|
|
70
70
|
import { formatRunCostTable } from "./runCostTable.js";
|
|
71
71
|
import { makeCombinedCheckpointStore, makeFileCheckpointStore, makePiJournalCheckpointStore, } from "./checkpoint.js";
|
|
72
|
+
import { absoluteUrl, draftPrototype } from "../prototypeTool.js";
|
|
72
73
|
import { getToken as defaultGetToken, resolveBaseUrl } from "../config.js";
|
|
73
74
|
import { makeCrashReporter } from "../crashReport.js";
|
|
74
75
|
import { logEvent } from "../errorSink.js";
|
|
@@ -785,6 +786,35 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
785
786
|
let result = await runPipeline(ticket, {
|
|
786
787
|
cwd: runCwd,
|
|
787
788
|
signal: runSignal,
|
|
789
|
+
// Interactive /go tracks no backend work item (#2134), so the draft
|
|
790
|
+
// is private to this session and keyed on the run: a resumed run
|
|
791
|
+
// reuses its prototype instead of drafting a second one.
|
|
792
|
+
...(deps.canPrepareDesign?.() ? {
|
|
793
|
+
prepareDesign: async ({ ticket: designTicket, exploration, signal }) => {
|
|
794
|
+
if (!deps.canPrepareDesign?.())
|
|
795
|
+
return undefined;
|
|
796
|
+
const baseUrl = deps.baseUrl ?? resolveBaseUrl();
|
|
797
|
+
try {
|
|
798
|
+
const draft = await (deps.draftPrototype ?? draftPrototype)({ baseUrl, getToken: deps.getToken ?? defaultGetToken }, {
|
|
799
|
+
title: designTicket.slice(0, 160), spec: `${designTicket}\n\nRepository exploration:\n${exploration}`,
|
|
800
|
+
sessionId: (deps.env ?? process.env).YAGNI_SESSION_ID,
|
|
801
|
+
automatic: true, requestKey: `go:${sessionKey}`,
|
|
802
|
+
}, signal);
|
|
803
|
+
if (draft.reference) {
|
|
804
|
+
notify(`Prototype ready: ${absoluteUrl(baseUrl, draft.prototype?.url ?? draft.reference.url ?? `/prototypes/${draft.reference.prototypeId}`)}`, "info");
|
|
805
|
+
return `## Prototype design reference\n${JSON.stringify(draft.reference)}\nRead this exact revision with prototype_read. Review it with the plan. Implement with the repository's real components. Preserve this reference through implementation and review. Run prototype_verify against the implemented local app with agreed interaction states; it captures mobile/desktop screenshots and records observed checks or explicitly unverified when no browser is available. Visually inspect those screenshots against this design and document intentional departures. Document intentional departures. Never substitute a newer revision or share automatically.`;
|
|
806
|
+
}
|
|
807
|
+
if (draft.status === "failed")
|
|
808
|
+
notify("Prototype drafting failed. Continue without it or retry with prototype_draft before approving the plan.", "warning");
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
if (!signal?.aborted)
|
|
813
|
+
notify("Prototype drafting unavailable. Continue without it or retry before plan approval.", "warning");
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
} : {}),
|
|
788
818
|
fanout: fanoutMode,
|
|
789
819
|
grounded: goGrounded,
|
|
790
820
|
...(isUltra ? { isUltra } : {}),
|
|
@@ -26,11 +26,18 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { type RunBudget } from "./budget.js";
|
|
28
28
|
import { type MissionInputs } from "./mission.js";
|
|
29
|
+
import { type ResilienceAttemptRecord, type RunStageFn } from "./resilience.js";
|
|
29
30
|
import { runStage as defaultRunStage } from "./runner.js";
|
|
30
31
|
import { type VerifyOutcome, type WorkstreamCheckResult, type WorkstreamCheckTarget } from "./verify.js";
|
|
31
32
|
import { snapshotWorkspace as defaultSnapshotWorkspace } from "./workspace.js";
|
|
32
|
-
import { type CheckpointStore, type FanoutBudgetVerdict, type FanoutMode, type PipelineStage, type JsonEvent, type PipelineProgress, type PipelineResult, type ResumePlan, type StageId, type StageResult, type StageTag } from "./types.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";
|
|
33
34
|
export interface RunPipelineDeps {
|
|
35
|
+
/** Bounded external design preparation; never runs inside the read-only planner or approved missions. */
|
|
36
|
+
prepareDesign?: (input: {
|
|
37
|
+
ticket: string;
|
|
38
|
+
exploration: string;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}) => Promise<string | undefined>;
|
|
34
41
|
cwd: string;
|
|
35
42
|
signal?: AbortSignal;
|
|
36
43
|
runStage?: typeof defaultRunStage;
|
|
@@ -174,5 +181,19 @@ export declare class PipelineStageError extends Error {
|
|
|
174
181
|
readonly partial: StageResult[];
|
|
175
182
|
constructor(stageId: string, message: string, partial: StageResult[]);
|
|
176
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* The resilience policy for a run's stages. With a deadline, a stage that
|
|
186
|
+
* spent its wall clock spent the build's time: a retry would only get the
|
|
187
|
+
* floor and run past the deadline, so it fails instead of retrying.
|
|
188
|
+
*/
|
|
189
|
+
export declare function stageRunnerPolicy(budget: RunBudget): ResiliencePolicy;
|
|
190
|
+
/**
|
|
191
|
+
* The pipeline's default stage runner: `base` under the R1 resilience wrapper
|
|
192
|
+
* with {@link stageRunnerPolicy}, each stage's wall clock taken from the run's
|
|
193
|
+
* deadline when it spawns (budget.ts#stageWall). A run with no deadline keeps
|
|
194
|
+
* the policy's fixed wall clock. Exported so a test can pin the wall a stage
|
|
195
|
+
* actually gets.
|
|
196
|
+
*/
|
|
197
|
+
export declare function resilientStageRunner(base: RunStageFn, budget: RunBudget, clock: () => number, telemetry: (rec: ResilienceAttemptRecord) => void): RunStageFn;
|
|
177
198
|
export declare function runPipeline(ticket: string, deps: RunPipelineDeps): Promise<PipelineResult>;
|
|
178
199
|
//# sourceMappingURL=orchestrator.d.ts.map
|
|
@@ -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, DEFAULT_STAGE_ESTIMATE_MS, EMPTY_RUN_USAGE, exceedsBudget, stageWontFit, } from "./budget.js";
|
|
27
|
+
import { addRunUsage, aggregateRunUsage, DEFAULT_RUN_BUDGET, DEFAULT_STAGE_ESTIMATE_MS, EMPTY_RUN_USAGE, exceedsBudget, stageWall, 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";
|
|
@@ -188,6 +188,27 @@ async function mapWithConcurrencyLimit(items, concurrency, fn) {
|
|
|
188
188
|
await Promise.all(workers);
|
|
189
189
|
return results;
|
|
190
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The resilience policy for a run's stages. With a deadline, a stage that
|
|
193
|
+
* spent its wall clock spent the build's time: a retry would only get the
|
|
194
|
+
* floor and run past the deadline, so it fails instead of retrying.
|
|
195
|
+
*/
|
|
196
|
+
export function stageRunnerPolicy(budget) {
|
|
197
|
+
return budget.deadlineMs === undefined ? DEFAULT_RESILIENCE_POLICY : { ...DEFAULT_RESILIENCE_POLICY, retryWallTimeout: false };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The pipeline's default stage runner: `base` under the R1 resilience wrapper
|
|
201
|
+
* with {@link stageRunnerPolicy}, each stage's wall clock taken from the run's
|
|
202
|
+
* deadline when it spawns (budget.ts#stageWall). A run with no deadline keeps
|
|
203
|
+
* the policy's fixed wall clock. Exported so a test can pin the wall a stage
|
|
204
|
+
* actually gets.
|
|
205
|
+
*/
|
|
206
|
+
export function resilientStageRunner(base, budget, clock, telemetry) {
|
|
207
|
+
return withResilience(base, stageRunnerPolicy(budget), {
|
|
208
|
+
telemetry,
|
|
209
|
+
wallFor: () => stageWall(budget, DEFAULT_RESILIENCE_POLICY.wallTimeoutMs, clock()),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
191
212
|
export async function runPipeline(ticket, deps) {
|
|
192
213
|
const log = (event, data) => deps.logger?.(event, data);
|
|
193
214
|
// R1: wrap the real child runner with in-loop resilience (idle/wall timeout +
|
|
@@ -195,10 +216,11 @@ export async function runPipeline(ticket, deps) {
|
|
|
195
216
|
// across its up-to-8 spawns. A caller that injects its own `runStage` (the unit
|
|
196
217
|
// tests, a custom lane) bypasses the wrapper untouched. Per-attempt telemetry
|
|
197
218
|
// rides the existing logger seam, so resume + the checkpoint journal are unaffected.
|
|
219
|
+
// With a deadline (sandbox / mission runs) each stage's wall clock is the
|
|
220
|
+
// time left less a reserve for review, a fix and verify, so a live stage is
|
|
221
|
+
// not killed at a fixed limit the run had room to exceed.
|
|
198
222
|
const runStageFn = deps.runStage ??
|
|
199
|
-
|
|
200
|
-
telemetry: (rec) => log("resilience_attempt", rec),
|
|
201
|
-
});
|
|
223
|
+
resilientStageRunner(defaultRunStage, deps.budget ?? DEFAULT_RUN_BUDGET, deps.now ?? Date.now, (rec) => log("resilience_attempt", rec));
|
|
202
224
|
const snapshotFn = deps.snapshotWorkspace ?? defaultSnapshotWorkspace;
|
|
203
225
|
const runVerifyFn = deps.runVerify ?? makeRunVerify();
|
|
204
226
|
const runWorkstreamCheckFn = deps.runWorkstreamCheck ?? makeWorkstreamCheck();
|
|
@@ -313,6 +335,11 @@ export async function runPipeline(ticket, deps) {
|
|
|
313
335
|
const rounds = resume ? [...resume.priorRounds] : [];
|
|
314
336
|
let round = resume ? resume.round : 1;
|
|
315
337
|
let reviewInput = resume ? resume.reviewInput : "";
|
|
338
|
+
const designMarker = "## Pinned design context";
|
|
339
|
+
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;
|
|
316
343
|
let stopReason = "clean";
|
|
317
344
|
// P4: honest note carried out when the verify gate could not give a verdict on
|
|
318
345
|
// the final round (no command / unrunnable), so /go never claims a build it did
|
|
@@ -727,6 +754,12 @@ export async function runPipeline(ticket, deps) {
|
|
|
727
754
|
baselinePaths = parseChangedPaths(beforeBuild.status);
|
|
728
755
|
// Sequential build half — stop at the first failure (mirrors the chain).
|
|
729
756
|
for (const stage of buildStages) {
|
|
757
|
+
if (stage.id === "plan" && !mission && deps.prepareDesign && !deps.signal?.aborted && !overBudget()) {
|
|
758
|
+
const prepared = await deps.prepareDesign({ ticket: promptTicket, exploration: previous ?? "", signal: deps.signal });
|
|
759
|
+
designContext = prepared ? `${designMarker}\n${prepared}` : undefined;
|
|
760
|
+
if (designContext)
|
|
761
|
+
previous = `${previous ?? ""}\n\n${designContext}`;
|
|
762
|
+
}
|
|
730
763
|
progress({ kind: "stage_start", stageId: stage.id });
|
|
731
764
|
stageEvent(stage.id, "start");
|
|
732
765
|
// The implement stage is a DIAMOND (partition → parallel builders →
|
|
@@ -817,12 +850,12 @@ export async function runPipeline(ticket, deps) {
|
|
|
817
850
|
progress({ kind: "stage_done", stageId: stage.id });
|
|
818
851
|
// The plan finish carries its finalOutput so /go can record the plan onto the
|
|
819
852
|
// work item; no other boundary streams the full stage text.
|
|
820
|
-
stageEvent(stage.id, "finish", stage.id === "plan" ? { output: r.finalOutput } : undefined);
|
|
853
|
+
stageEvent(stage.id, "finish", stage.id === "plan" ? { output: designContext ? `${r.finalOutput}\n\n${designContext}` : r.finalOutput } : undefined);
|
|
821
854
|
// The map brief is the partitioner's repo context on an interactive run
|
|
822
855
|
// (mission runs skip map and hand it the injected memo instead).
|
|
823
856
|
if (stage.id === "map")
|
|
824
857
|
mapBrief = r.finalOutput;
|
|
825
|
-
previous = r.finalOutput;
|
|
858
|
+
previous = designContext ? `${r.finalOutput}\n\n${designContext}` : r.finalOutput;
|
|
826
859
|
// R3-b: stop honestly if the build half alone blew the run budget.
|
|
827
860
|
const over = overBudget();
|
|
828
861
|
if (over) {
|
|
@@ -1042,7 +1075,7 @@ export async function runPipeline(ticket, deps) {
|
|
|
1042
1075
|
const fixStartedAt = clock();
|
|
1043
1076
|
progress({ kind: "stage_start", stageId: "fix", round });
|
|
1044
1077
|
stageEvent("fix", "start", { round });
|
|
1045
|
-
const fixResult = await runStageFn(fix, { ticket: promptTicket, previous: serializeBlocking(findings), grounded }, stageDepsFor({ stageId: "fix", round }));
|
|
1078
|
+
const fixResult = await runStageFn(fix, { ticket: promptTicket, previous: designContext ? `${serializeBlocking(findings)}\n\n${designContext}` : serializeBlocking(findings), grounded }, stageDepsFor({ stageId: "fix", round }));
|
|
1046
1079
|
observedMs.fix = clock() - fixStartedAt;
|
|
1047
1080
|
stages.push(fixResult);
|
|
1048
1081
|
log("stage_done", { stageId: "fix", exitCode: fixResult.exitCode });
|
|
@@ -1092,8 +1125,8 @@ export async function runPipeline(ticket, deps) {
|
|
|
1092
1125
|
// crash now can resume at the NEXT round against this fix's output. Snapshot
|
|
1093
1126
|
// the post-fix tree as the resume consistency key.
|
|
1094
1127
|
const afterFix = await snapshotFn(deps.cwd, deps.signal);
|
|
1095
|
-
|
|
1096
|
-
reviewInput
|
|
1128
|
+
reviewInput = designContext ? `${fixResult.finalOutput}\n\n${designContext}` : fixResult.finalOutput;
|
|
1129
|
+
writeCheckpoint(mkRecord("round_done", { round, reviewInput, findings, snapshot: afterFix, usage: currentUsage() }));
|
|
1097
1130
|
round += 1;
|
|
1098
1131
|
}
|
|
1099
1132
|
log("pipeline_stop", { reason: stopReason, round });
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* Pure-ish: all impurity (timers, sleep, clock, randomness) sits behind injectable
|
|
28
28
|
* seams so the wrapper is unit-tested without real waits.
|
|
29
29
|
*/
|
|
30
|
+
import type { StageWallSource } from "./budget.js";
|
|
30
31
|
import type { RunStageDeps } from "./runner.js";
|
|
31
32
|
import { type PipelineStage, type ResiliencePolicy, type ReviewLens, type StageResult } from "./types.js";
|
|
32
33
|
export type { ResiliencePolicy } from "./types.js";
|
|
@@ -54,6 +55,13 @@ export interface ResilienceAttemptRecord {
|
|
|
54
55
|
/** 1-based attempt number. */
|
|
55
56
|
attempt: number;
|
|
56
57
|
outcome: "ok" | "transient" | "fatal" | "timeout" | "aborted";
|
|
58
|
+
/** The wall clock this attempt was given (ms). */
|
|
59
|
+
wallTimeoutMs?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Where that wall clock came from, when the caller said: the build's time
|
|
62
|
+
* left, the floor because the deadline was close, or the no-deadline default.
|
|
63
|
+
*/
|
|
64
|
+
wallSource?: StageWallSource;
|
|
57
65
|
/** Which timer fired, when the attempt timed out. */
|
|
58
66
|
timeoutKind?: "idle" | "wall";
|
|
59
67
|
/** On a timeout: ms the attempt spent with a tool running (the rest went to the model). */
|
|
@@ -76,6 +84,16 @@ export interface ResilienceSeams {
|
|
|
76
84
|
}
|
|
77
85
|
export interface WithResilienceOpts {
|
|
78
86
|
telemetry?: (rec: ResilienceAttemptRecord) => void;
|
|
87
|
+
/**
|
|
88
|
+
* The wall clock for one attempt, asked when the attempt spawns so a later
|
|
89
|
+
* stage (or a retry) gets the time actually left. Absent, every attempt gets
|
|
90
|
+
* `policy.wallTimeoutMs`. The idle window always comes from the policy.
|
|
91
|
+
* `source` rides the attempt record as `wallSource`.
|
|
92
|
+
*/
|
|
93
|
+
wallFor?: (stage: PipelineStage) => {
|
|
94
|
+
ms: number;
|
|
95
|
+
source?: StageWallSource;
|
|
96
|
+
};
|
|
79
97
|
seams?: ResilienceSeams;
|
|
80
98
|
}
|
|
81
99
|
/**
|
|
@@ -163,7 +163,9 @@ export function withResilience(base, policy, opts = {}) {
|
|
|
163
163
|
}
|
|
164
164
|
fireTimeout("idle");
|
|
165
165
|
};
|
|
166
|
-
const
|
|
166
|
+
const wall = opts.wallFor?.(stage);
|
|
167
|
+
const wallTimeoutMs = wall?.ms ?? policy.wallTimeoutMs;
|
|
168
|
+
const wallTimer = setTimeout(() => fireTimeout("wall"), wallTimeoutMs);
|
|
167
169
|
wallTimer.unref?.();
|
|
168
170
|
armIdle();
|
|
169
171
|
const originalOnEvent = deps.onEvent;
|
|
@@ -207,6 +209,8 @@ export function withResilience(base, policy, opts = {}) {
|
|
|
207
209
|
...(deps.taskIndex !== undefined ? { taskIndex: deps.taskIndex } : {}),
|
|
208
210
|
attempt,
|
|
209
211
|
outcome,
|
|
212
|
+
wallTimeoutMs,
|
|
213
|
+
...(wall?.source ? { wallSource: wall.source } : {}),
|
|
210
214
|
...(timedOut && timeoutKind ? { timeoutKind } : {}),
|
|
211
215
|
...(timedOut ? { toolMs, modelBound } : {}),
|
|
212
216
|
exitCode: last.exitCode,
|
|
@@ -229,7 +233,7 @@ export function withResilience(base, policy, opts = {}) {
|
|
|
229
233
|
const transient = classifyTransient(last, timedOut);
|
|
230
234
|
// A wall-clock exhaustion is a "task too big" signal, not a stall: a
|
|
231
235
|
// lane that opts out (subagents) fails honestly instead of re-burning
|
|
232
|
-
// the same
|
|
236
|
+
// the same wall clock per attempt. Idle stalls stay retryable.
|
|
233
237
|
const wallNotRetryable = timedOut && timeoutKind === "wall" && policy.retryWallTimeout === false;
|
|
234
238
|
// No double-apply: a write stage that already started working is never re-run.
|
|
235
239
|
const blockedByWriteGate = isWriteStage && sawAnyEvent;
|
|
@@ -41,9 +41,9 @@ export const REVIEW_LENSES = ["correctness", "business_fit", "does_it_hold"];
|
|
|
41
41
|
*/
|
|
42
42
|
export const REQUIRED_LENSES = ["correctness"];
|
|
43
43
|
/** Read-only recon + grounding tools shared by scout and reviewer. */
|
|
44
|
-
const READ_TOOLS = ["read", "grep", "find", "ls", "bash"];
|
|
44
|
+
const READ_TOOLS = ["read", "grep", "find", "ls", "bash", "prototype_read"];
|
|
45
45
|
/** Full write tools for the worker stages. */
|
|
46
|
-
const WRITE_TOOLS = ["read", "write", "edit", "bash", "grep", "find", "ls"];
|
|
46
|
+
const WRITE_TOOLS = ["read", "write", "edit", "bash", "grep", "find", "ls", "prototype_read", "prototype_verification", "prototype_verify"];
|
|
47
47
|
const TICKET_CONTEXT = "Follow the approved plan and its explicit constraints, including approved answers. " +
|
|
48
48
|
"Prior summaries and review findings do not authorize expanding scope or publication " +
|
|
49
49
|
"permissions. Report conflicts requiring a scope change for approval.\n\n" +
|
|
@@ -65,7 +65,7 @@ export const PIPELINE_V1 = [
|
|
|
65
65
|
// Read-only recon (read/grep/find/ls) so the planner can actually navigate
|
|
66
66
|
// instead of flailing on a single `read` and ending mid-exploration; NO bash
|
|
67
67
|
// (it must not build/run), and ask_yagni for grounding.
|
|
68
|
-
tools: ["read", "grep", "find", "ls", "ask_yagni"],
|
|
68
|
+
tools: ["read", "grep", "find", "ls", "ask_yagni", "prototype_read"],
|
|
69
69
|
taskTemplate: "Turn the ticket and the repo map into an ordered, grounded implementation " +
|
|
70
70
|
"plan. Call ask_yagni before inferring a convention.\n\n" +
|
|
71
71
|
"Ticket: {ticket}\n\nRepo map:\n{previous}",
|
|
@@ -78,7 +78,7 @@ export const PIPELINE_V1 = [
|
|
|
78
78
|
taskTemplate: TICKET_CONTEXT +
|
|
79
79
|
"Implement the plan. Write the code against it, and call record_decision " +
|
|
80
80
|
"only for a consequential product-intent or architecture call the plan did " +
|
|
81
|
-
"not settle (never for taste, naming, or formatting).\n\nPlan:\n{previous}",
|
|
81
|
+
"not settle (never for taste, naming, or formatting). If the plan names a prototype revision, read that exact revision with prototype_read, implement with the repository components, then call prototype_verify against the local app with the agreed interaction states. Inspect its mobile/desktop screenshots against the design and document departures; unavailable browser checks remain unverified.\n\nPlan:\n{previous}",
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
id: "review",
|
|
@@ -95,7 +95,7 @@ export const PIPELINE_V1 = [
|
|
|
95
95
|
tools: [...WRITE_TOOLS, "ask_yagni"],
|
|
96
96
|
taskTemplate: TICKET_CONTEXT +
|
|
97
97
|
"Reconcile ONLY the blocking review findings against the plan. Leave " +
|
|
98
|
-
"non-blocking findings alone.\n\nBlocking findings:\n{previous}",
|
|
98
|
+
"non-blocking findings alone. When fixing a prototype-backed UI, run prototype_verify again against the pinned revision and inspect the captured screenshots; never substitute a newer design.\n\nBlocking findings:\n{previous}",
|
|
99
99
|
},
|
|
100
100
|
];
|
|
101
101
|
/** The single review stage the orchestrator fans out across REVIEW_LENSES. */
|
|
@@ -527,18 +527,21 @@ export interface ResiliencePolicy {
|
|
|
527
527
|
/**
|
|
528
528
|
* Whether a WALL-clock exhaustion is retryable. Default true (the /go
|
|
529
529
|
* doctrine: a wedged stage is worth another attempt). A lane whose children
|
|
530
|
-
* run read-only, fast work (subagents) sets false: a
|
|
531
|
-
* the task is too big, and re-running it just multiplies the spend before
|
|
530
|
+
* run read-only, fast work (subagents) sets false: a full wall-clock burn
|
|
531
|
+
* means the task is too big, and re-running it just multiplies the spend before
|
|
532
532
|
* the same honest fail. Idle-stall timeouts stay retryable either way.
|
|
533
533
|
*/
|
|
534
534
|
retryWallTimeout?: boolean;
|
|
535
535
|
}
|
|
536
536
|
/**
|
|
537
537
|
* Default resilience policy. Generous timeouts so a legitimately long but live
|
|
538
|
-
* stage is never killed
|
|
539
|
-
* and the wall window bounds a
|
|
540
|
-
*
|
|
541
|
-
*
|
|
538
|
+
* stage is never killed: the idle window (well under the 5-min run heartbeat) is
|
|
539
|
+
* the check for a stuck model, and the wall window only bounds a runaway that
|
|
540
|
+
* keeps emitting events. The wall is 60 minutes for an interactive /go, where a
|
|
541
|
+
* person can abort; a run with a deadline replaces it per stage with the time
|
|
542
|
+
* left (budget.ts#stageWall). Three attempts with short, jittered
|
|
543
|
+
* backoff so a single transient blip across the up-to-8 child spawns no longer
|
|
544
|
+
* terminates the whole run.
|
|
542
545
|
*/
|
|
543
546
|
export declare const DEFAULT_RESILIENCE_POLICY: ResiliencePolicy;
|
|
544
547
|
/**
|
|
@@ -17,14 +17,17 @@ export const MAX_REVIEW_ROUNDS = 3;
|
|
|
17
17
|
export const MAX_FIX_TURNS = 3;
|
|
18
18
|
/**
|
|
19
19
|
* Default resilience policy. Generous timeouts so a legitimately long but live
|
|
20
|
-
* stage is never killed
|
|
21
|
-
* and the wall window bounds a
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* stage is never killed: the idle window (well under the 5-min run heartbeat) is
|
|
21
|
+
* the check for a stuck model, and the wall window only bounds a runaway that
|
|
22
|
+
* keeps emitting events. The wall is 60 minutes for an interactive /go, where a
|
|
23
|
+
* person can abort; a run with a deadline replaces it per stage with the time
|
|
24
|
+
* left (budget.ts#stageWall). Three attempts with short, jittered
|
|
25
|
+
* backoff so a single transient blip across the up-to-8 child spawns no longer
|
|
26
|
+
* terminates the whole run.
|
|
24
27
|
*/
|
|
25
28
|
export const DEFAULT_RESILIENCE_POLICY = {
|
|
26
29
|
idleTimeoutMs: 4 * 60_000,
|
|
27
|
-
wallTimeoutMs:
|
|
30
|
+
wallTimeoutMs: 60 * 60_000,
|
|
28
31
|
maxAttempts: 3,
|
|
29
32
|
backoffBaseMs: 1_000,
|
|
30
33
|
backoffMaxMs: 30_000,
|