@yagni-app/code 0.3.2 → 0.3.3
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/cli.js +13 -0
- package/dist/extension/footer.d.ts +1 -1
- package/dist/extension/hooks.d.ts +111 -0
- package/dist/extension/hooks.js +666 -0
- package/dist/extension/index.d.ts +13 -6
- package/dist/extension/index.js +57 -7
- package/dist/extension/{approvedPrefixes.js → permission/approvedPrefixes.js} +1 -1
- package/dist/extension/permission/dbReadPolicy.d.ts +90 -0
- package/dist/extension/permission/dbReadPolicy.js +227 -0
- package/dist/extension/{execPolicy.js → permission/execPolicy.js} +41 -13
- package/dist/extension/{permission.d.ts → permission/gate.d.ts} +9 -2
- package/dist/extension/{permission.js → permission/gate.js} +103 -4
- package/dist/extension/{guardian.d.ts → permission/guardian.d.ts} +2 -2
- package/dist/extension/{guardian.js → permission/guardian.js} +1 -1
- package/dist/extension/permission/index.d.ts +14 -0
- package/dist/extension/permission/index.js +14 -0
- package/dist/extension/permission/packageManagerPolicy.d.ts +55 -0
- package/dist/extension/permission/packageManagerPolicy.js +170 -0
- package/dist/extension/pipeline/activityFeed.js +19 -5
- package/dist/extension/pipeline/checker.d.ts +99 -0
- package/dist/extension/pipeline/checker.js +238 -0
- package/dist/extension/pipeline/fanout.d.ts +116 -0
- package/dist/extension/pipeline/fanout.js +248 -0
- package/dist/extension/pipeline/fanoutBeats.d.ts +31 -0
- package/dist/extension/pipeline/fanoutBeats.js +86 -0
- package/dist/extension/pipeline/goCommand.d.ts +14 -0
- package/dist/extension/pipeline/goCommand.js +38 -1
- package/dist/extension/pipeline/headlessGo.d.ts +163 -0
- package/dist/extension/pipeline/headlessGo.js +333 -0
- package/dist/extension/pipeline/invocation.d.ts +7 -1
- package/dist/extension/pipeline/invocation.js +7 -1
- package/dist/extension/pipeline/mission.d.ts +55 -0
- package/dist/extension/pipeline/mission.js +70 -0
- package/dist/extension/pipeline/orchestrator.d.ts +48 -3
- package/dist/extension/pipeline/orchestrator.js +450 -9
- package/dist/extension/pipeline/personas.d.ts +16 -1
- package/dist/extension/pipeline/personas.js +117 -6
- package/dist/extension/pipeline/runSession.d.ts +45 -1
- package/dist/extension/pipeline/runState.d.ts +57 -12
- package/dist/extension/pipeline/runState.js +60 -18
- package/dist/extension/pipeline/runner.js +10 -1
- package/dist/extension/pipeline/stages.d.ts +84 -7
- package/dist/extension/pipeline/stages.js +166 -0
- package/dist/extension/pipeline/tierCap.d.ts +32 -0
- package/dist/extension/pipeline/tierCap.js +57 -0
- package/dist/extension/pipeline/types.d.ts +130 -1
- package/dist/extension/pipeline/types.js +17 -0
- package/dist/extension/pipeline/verify.d.ts +86 -3
- package/dist/extension/pipeline/verify.js +175 -6
- package/dist/extension/turnLog.d.ts +38 -0
- package/dist/extension/turnLog.js +93 -0
- package/dist/goHeadless.d.ts +75 -0
- package/dist/goHeadless.js +132 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +12 -0
- package/package.json +2 -2
- /package/dist/extension/{approvedPrefixes.d.ts → permission/approvedPrefixes.d.ts} +0 -0
- /package/dist/extension/{execPolicy.d.ts → permission/execPolicy.d.ts} +0 -0
|
@@ -18,7 +18,16 @@
|
|
|
18
18
|
*
|
|
19
19
|
* `selectStages(lane)` returns the full list for v1 and is the seam where v2
|
|
20
20
|
* complexity-lanes plug in without a rewrite.
|
|
21
|
+
*
|
|
22
|
+
* The implement stage additionally derives a DIAMOND from this same data:
|
|
23
|
+
* `orchestratorStage` (peak partitioner) → `builderStage` per workstream (the
|
|
24
|
+
* implement def at the workstream's own tier) → `synthesizerStage` (seam
|
|
25
|
+
* reconciler) → `fixerStage` for the bounded fix turns. Every one of them keeps
|
|
26
|
+
* `id: "implement"`, so `StageId` never widens and the feed, the five-stage
|
|
27
|
+
* rail, and the tier cap treat the whole diamond as today's single stage.
|
|
21
28
|
*/
|
|
29
|
+
import { sanitizeCallerSegment } from "../config.js";
|
|
30
|
+
import { builderPersonaClause } from "./personas.js";
|
|
22
31
|
/** The three adversarial review angles, fanned out per round (spec §3). */
|
|
23
32
|
export const REVIEW_LENSES = ["correctness", "business_fit", "does_it_hold"];
|
|
24
33
|
/**
|
|
@@ -104,6 +113,163 @@ export function selectStages(_lane = "full") {
|
|
|
104
113
|
* `buildStageInvocation` joins `--tools` with a comma, so an empty array would
|
|
105
114
|
* emit the broken `--tools ""`.
|
|
106
115
|
*/
|
|
116
|
+
/** The single implement stage the diamond re-enters per builder / fixer. */
|
|
117
|
+
function implementStage() {
|
|
118
|
+
const stage = PIPELINE_V1.find((s) => s.id === "implement");
|
|
119
|
+
if (!stage)
|
|
120
|
+
throw new Error("PIPELINE_V1 is missing the implement stage");
|
|
121
|
+
return stage;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The partitioner's caller label. Deliberately a `go:*` caller and never
|
|
125
|
+
* `subagent:*`: the server clamps `subagent:*` callers to `advanced`, which would
|
|
126
|
+
* silently downgrade the one peak judgment call in the whole implement stage.
|
|
127
|
+
*/
|
|
128
|
+
export const PARTITION_CALLER_LABEL = "go:implement:partition";
|
|
129
|
+
const IMPLEMENT_CALLER_PREFIX = "go:implement:";
|
|
130
|
+
/**
|
|
131
|
+
* The per-builder caller label, mirroring review's `go:review:<lens>` so
|
|
132
|
+
* `GET /spend?runId=` attributes the fan per workstream for free. Workstream
|
|
133
|
+
* names come from a model, so the segment is sanitized into the proxy's caller
|
|
134
|
+
* charset and the whole label is kept inside its 64-char cap.
|
|
135
|
+
*/
|
|
136
|
+
export function workstreamCallerLabel(name) {
|
|
137
|
+
return `${IMPLEMENT_CALLER_PREFIX}${sanitizeCallerSegment(name, 64 - IMPLEMENT_CALLER_PREFIX.length)}`;
|
|
138
|
+
}
|
|
139
|
+
function repoContextSection(ctx) {
|
|
140
|
+
const brief = ctx.repoBrief?.trim();
|
|
141
|
+
const memo = ctx.missionMemo?.trim();
|
|
142
|
+
const sections = [];
|
|
143
|
+
if (brief)
|
|
144
|
+
sections.push(`Repo map (from this run's map stage):\n${brief}`);
|
|
145
|
+
if (memo) {
|
|
146
|
+
sections.push("Repo context (cached scoping memo; this run entered at implement, so no map stage ran):\n" + memo);
|
|
147
|
+
}
|
|
148
|
+
if (sections.length > 0)
|
|
149
|
+
return sections.join("\n\n");
|
|
150
|
+
return ("Repo context: none. No map brief and no scoping memo was available for this run, " +
|
|
151
|
+
"so the plan is all you have. Thin context is exactly the case where you return " +
|
|
152
|
+
"single-writer: do not guess a partition.");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The peak-tier partitioner (spec "Partition contract"): reads the approved plan
|
|
156
|
+
* plus whatever repo context this run actually has and returns a ```partition
|
|
157
|
+
* block. It runs INSIDE the implement boundary (`id: "implement"`), so the feed,
|
|
158
|
+
* the five-stage rail, and the tier cap all keep working unchanged.
|
|
159
|
+
*/
|
|
160
|
+
export function orchestratorStage(ctx = {}) {
|
|
161
|
+
return {
|
|
162
|
+
id: "implement",
|
|
163
|
+
agent: "orchestrator",
|
|
164
|
+
model: "peak",
|
|
165
|
+
tools: [...READ_TOOLS, "ask_yagni"],
|
|
166
|
+
taskTemplate: "Decide how this approved plan should be built: fanned into parallel workstreams " +
|
|
167
|
+
"with disjoint file claims, or by a single writer.\n\n" +
|
|
168
|
+
"Ticket: {ticket}\n\n" +
|
|
169
|
+
"Approved plan:\n{previous}\n\n" +
|
|
170
|
+
`${repoContextSection(ctx)}\n\n` +
|
|
171
|
+
"Rules for this call:\n" +
|
|
172
|
+
(ctx.pinned
|
|
173
|
+
? "- This run is pinned to fan mode: split the plan when it can honestly be split, and " +
|
|
174
|
+
"return single-writer only when the work genuinely cannot be divided into disjoint " +
|
|
175
|
+
"file sets. Never invent a split to satisfy the pin.\n"
|
|
176
|
+
: "- The bias is conservative: fan only when the plan splits into obviously clean, " +
|
|
177
|
+
"non-touching surfaces. When in doubt, return single-writer with the reason.\n") +
|
|
178
|
+
"- Legal widths are 2, 4 or 8, and the width must equal the number of workstreams.\n" +
|
|
179
|
+
"- Every workstream claims the paths it will touch, including the directories of any " +
|
|
180
|
+
"files it will create. Claims are prefixes and must be pairwise disjoint; an overlap " +
|
|
181
|
+
"fails the run.\n" +
|
|
182
|
+
"- Give each workstream a tier: efficient for mechanical work, standard for " +
|
|
183
|
+
"load-bearing work.\n" +
|
|
184
|
+
"- End with the ```partition block and nothing after it.",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* One parallel builder: the implement stage re-entered at the workstream's
|
|
189
|
+
* assigned tier, tagged with its workstream, and pinned by a persona clause to
|
|
190
|
+
* the files it claimed (siblings' claims ride along so the seams stay aligned).
|
|
191
|
+
*/
|
|
192
|
+
export function builderStage(workstream, siblings = []) {
|
|
193
|
+
const implement = implementStage();
|
|
194
|
+
return {
|
|
195
|
+
...implement,
|
|
196
|
+
model: workstream.tier,
|
|
197
|
+
workstream: workstream.name,
|
|
198
|
+
personaClause: builderPersonaClause(workstream, siblings),
|
|
199
|
+
taskTemplate: `Implement ONLY your workstream of the plan below: "${workstream.name}".\n\n` +
|
|
200
|
+
`Your workstream: ${workstream.task}\n\n` +
|
|
201
|
+
"Write the code against the plan, staying inside the files you own, and call " +
|
|
202
|
+
"record_decision for any product-intent call you are forced to make.\n\n" +
|
|
203
|
+
"Plan:\n{previous}",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The seam-reconciler (spec decision 5). Not a patch applier: with one shared
|
|
208
|
+
* worktree the union diff is already in place, so this child fixes what does not
|
|
209
|
+
* line up between builders and writes the implement handoff the review loop reads.
|
|
210
|
+
*/
|
|
211
|
+
export function synthesizerStage() {
|
|
212
|
+
const implement = implementStage();
|
|
213
|
+
return {
|
|
214
|
+
...implement,
|
|
215
|
+
agent: "synthesizer",
|
|
216
|
+
model: "standard",
|
|
217
|
+
taskTemplate: "Several builders just finished in parallel in this working tree. Reconcile the " +
|
|
218
|
+
"seams between their changes, address the checker findings and any out-of-claim " +
|
|
219
|
+
"edits below, and write ONE implement summary for the reviewers.\n\n{previous}",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The synthesizer's caller label, in the same `go:implement:*` family as the
|
|
224
|
+
* partitioner and the builders, so `GET /spend?runId=` attributes the reconciler's
|
|
225
|
+
* own spend rather than folding it into the anonymous implement default.
|
|
226
|
+
*/
|
|
227
|
+
export const SYNTHESIZER_CALLER_LABEL = `${IMPLEMENT_CALLER_PREFIX}synthesizer`;
|
|
228
|
+
/**
|
|
229
|
+
* The synthesizer re-engaged inside a fix turn (spec decision 7): findings the
|
|
230
|
+
* checker could not attribute to any one workstream's claims are seam work, which
|
|
231
|
+
* is precisely this child's job. It re-writes the implement summary afterwards, so
|
|
232
|
+
* its output remains the handoff the review loop reads.
|
|
233
|
+
*/
|
|
234
|
+
export function synthesizerFixStage(findings) {
|
|
235
|
+
return {
|
|
236
|
+
...synthesizerStage(),
|
|
237
|
+
taskTemplate: "Verification failed on the merged tree, and these findings do not sit inside any single " +
|
|
238
|
+
"workstream's files, so they are yours. Fix ONLY what is listed, then re-write the implement " +
|
|
239
|
+
`summary for the reviewers in the same format.\n\nFindings:\n${findings}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* A bounded fix turn (spec decision 7): the builder re-entered at its ORIGINAL
|
|
244
|
+
* tier with the verify findings scoped to its claims. `workstream` is absent on
|
|
245
|
+
* the single-writer path, where the one builder is re-engaged the same way.
|
|
246
|
+
*/
|
|
247
|
+
export function fixerStage(workstream, findings, siblings = []) {
|
|
248
|
+
const base = workstream ? builderStage(workstream, siblings) : implementStage();
|
|
249
|
+
return {
|
|
250
|
+
...base,
|
|
251
|
+
personaClause: builderPersonaClause(workstream, siblings),
|
|
252
|
+
taskTemplate: "Verification failed. Fix ONLY the findings below" +
|
|
253
|
+
(workstream ? `, and only inside the files your workstream owns` : "") +
|
|
254
|
+
". Do not start a fresh pass over the plan and do not fix anything that is not " +
|
|
255
|
+
`listed.\n\nFindings:\n${findings}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The partition-contract re-ask: the same cheap format-recovery pass the review
|
|
260
|
+
* lenses get (`reaskStage`), pointed at the ```partition block instead of
|
|
261
|
+
* ```findings. One attempt only — a partitioner that still will not emit a
|
|
262
|
+
* readable block degrades to single-writer, never to a guessed partition.
|
|
263
|
+
*/
|
|
264
|
+
export function partitionReaskStage(orchestrator) {
|
|
265
|
+
return {
|
|
266
|
+
...orchestrator,
|
|
267
|
+
model: "efficient",
|
|
268
|
+
tools: ["read"],
|
|
269
|
+
taskTemplate: "Re-emit ONLY the ```partition block for the decision you just made, and nothing " +
|
|
270
|
+
"else:\n{previous}",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
107
273
|
export function reaskStage(review) {
|
|
108
274
|
return {
|
|
109
275
|
...review,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE tier ceiling for a whole /go run (spec: sandbox harness parity).
|
|
3
|
+
*
|
|
4
|
+
* The eval and template-smoke lanes need to exercise the REAL pipeline without
|
|
5
|
+
* paying real peak-tier review: a template rebuild's smoke run is a correctness
|
|
6
|
+
* check, not a quality benchmark. `YAGNI_GO_TIER_CAP=<rung>` clamps every stage
|
|
7
|
+
* and every child at that ceiling; unset (the default everywhere else) leaves
|
|
8
|
+
* the pipeline's own per-stage routing untouched.
|
|
9
|
+
*
|
|
10
|
+
* The clamp is applied in ONE place — `runStage`, the single seam every stage,
|
|
11
|
+
* review lens, format re-ask and subagent child passes through on its way to a
|
|
12
|
+
* pi invocation — and the env var rides the child env, so a grandchild (a
|
|
13
|
+
* subagent spawned by a stage) resolves the same ceiling from its own process.
|
|
14
|
+
*
|
|
15
|
+
* A cap only ever lowers a tier: naming a cap ABOVE a stage's own tier never
|
|
16
|
+
* promotes it (the pipeline's routing is the judgment; the cap is a budget).
|
|
17
|
+
*/
|
|
18
|
+
import type { ModelTier } from "./types.js";
|
|
19
|
+
/** The documented env var. Eval/smoke lanes only; unset everywhere else. */
|
|
20
|
+
export declare const TIER_CAP_ENV = "YAGNI_GO_TIER_CAP";
|
|
21
|
+
/**
|
|
22
|
+
* Parse a raw env value into a cap. Unset, empty, or unrecognized all yield
|
|
23
|
+
* undefined (no cap): a typo must never silently route a run onto some other
|
|
24
|
+
* tier, and a lane that meant to cap can see the honest "no cap" in the run's
|
|
25
|
+
* result object.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseTierCap(raw: string | undefined): ModelTier | undefined;
|
|
28
|
+
/** The effective tier for a stage under a cap: never above it, never promoted. */
|
|
29
|
+
export declare function clampTier(tier: ModelTier, cap: ModelTier | undefined): ModelTier;
|
|
30
|
+
/** Resolve the run's cap from an environment (the child env, or process.env). */
|
|
31
|
+
export declare function resolveTierCap(env: NodeJS.ProcessEnv): ModelTier | undefined;
|
|
32
|
+
//# sourceMappingURL=tierCap.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE tier ceiling for a whole /go run (spec: sandbox harness parity).
|
|
3
|
+
*
|
|
4
|
+
* The eval and template-smoke lanes need to exercise the REAL pipeline without
|
|
5
|
+
* paying real peak-tier review: a template rebuild's smoke run is a correctness
|
|
6
|
+
* check, not a quality benchmark. `YAGNI_GO_TIER_CAP=<rung>` clamps every stage
|
|
7
|
+
* and every child at that ceiling; unset (the default everywhere else) leaves
|
|
8
|
+
* the pipeline's own per-stage routing untouched.
|
|
9
|
+
*
|
|
10
|
+
* The clamp is applied in ONE place — `runStage`, the single seam every stage,
|
|
11
|
+
* review lens, format re-ask and subagent child passes through on its way to a
|
|
12
|
+
* pi invocation — and the env var rides the child env, so a grandchild (a
|
|
13
|
+
* subagent spawned by a stage) resolves the same ceiling from its own process.
|
|
14
|
+
*
|
|
15
|
+
* A cap only ever lowers a tier: naming a cap ABOVE a stage's own tier never
|
|
16
|
+
* promotes it (the pipeline's routing is the judgment; the cap is a budget).
|
|
17
|
+
*/
|
|
18
|
+
/** The documented env var. Eval/smoke lanes only; unset everywhere else. */
|
|
19
|
+
export const TIER_CAP_ENV = "YAGNI_GO_TIER_CAP";
|
|
20
|
+
/**
|
|
21
|
+
* Rung order, lowest first. `balanced` is not a rung a cap can name (it is a
|
|
22
|
+
* session routing policy, not a tier a child runs on — see mapModelTier), but a
|
|
23
|
+
* stage carrying it still needs a comparable rank, so it sits with `standard`,
|
|
24
|
+
* the concrete tier balanced resolves to.
|
|
25
|
+
*/
|
|
26
|
+
const RANK = {
|
|
27
|
+
efficient: 0,
|
|
28
|
+
standard: 1,
|
|
29
|
+
balanced: 1,
|
|
30
|
+
advanced: 2,
|
|
31
|
+
peak: 3,
|
|
32
|
+
};
|
|
33
|
+
/** The rungs a cap may name (`balanced` is deliberately excluded). */
|
|
34
|
+
const CAPPABLE = ["efficient", "standard", "advanced", "peak"];
|
|
35
|
+
/**
|
|
36
|
+
* Parse a raw env value into a cap. Unset, empty, or unrecognized all yield
|
|
37
|
+
* undefined (no cap): a typo must never silently route a run onto some other
|
|
38
|
+
* tier, and a lane that meant to cap can see the honest "no cap" in the run's
|
|
39
|
+
* result object.
|
|
40
|
+
*/
|
|
41
|
+
export function parseTierCap(raw) {
|
|
42
|
+
const normalized = raw?.trim().toLowerCase();
|
|
43
|
+
if (!normalized)
|
|
44
|
+
return undefined;
|
|
45
|
+
return CAPPABLE.includes(normalized) ? normalized : undefined;
|
|
46
|
+
}
|
|
47
|
+
/** The effective tier for a stage under a cap: never above it, never promoted. */
|
|
48
|
+
export function clampTier(tier, cap) {
|
|
49
|
+
if (!cap)
|
|
50
|
+
return tier;
|
|
51
|
+
return RANK[tier] > RANK[cap] ? cap : tier;
|
|
52
|
+
}
|
|
53
|
+
/** Resolve the run's cap from an environment (the child env, or process.env). */
|
|
54
|
+
export function resolveTierCap(env) {
|
|
55
|
+
return parseTierCap(env[TIER_CAP_ENV]);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=tierCap.js.map
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* §7.2) so future complexity-lanes select a subset, never a rewrite.
|
|
8
8
|
*/
|
|
9
9
|
import type { RunUsage } from "./budget.js";
|
|
10
|
+
import type { PartitionWidth, WorkstreamTier } from "./fanout.js";
|
|
10
11
|
/**
|
|
11
12
|
* Opaque per-stage model tier. These map to models server-side under the `yagni`
|
|
12
13
|
* provider — never a model id in the pipeline config. The pipeline uses `peak`
|
|
@@ -48,6 +49,19 @@ export interface PipelineStage {
|
|
|
48
49
|
taskTemplate: string;
|
|
49
50
|
/** Only set when the stage is fanned out per review lens. */
|
|
50
51
|
lens?: ReviewLens;
|
|
52
|
+
/**
|
|
53
|
+
* Only set when the stage is a fan-out child of the implement diamond: the
|
|
54
|
+
* name of the workstream this child owns. The direct analogue of `lens` on the
|
|
55
|
+
* review fan-out — it never changes {@link StageId} (the diamond lives INSIDE
|
|
56
|
+
* the implement boundary), it just attributes the child.
|
|
57
|
+
*/
|
|
58
|
+
workstream?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Extra system-prompt text appended after the persona body (see
|
|
61
|
+
* `personaBody`). The fan-out builders use it for their per-workstream clause:
|
|
62
|
+
* claimed files, sibling claims, and the shared-worktree discipline rules.
|
|
63
|
+
*/
|
|
64
|
+
personaClause?: string;
|
|
51
65
|
}
|
|
52
66
|
/** A single structured review finding parsed from a reviewer's output. */
|
|
53
67
|
export interface Finding {
|
|
@@ -70,6 +84,13 @@ export interface StageUsage {
|
|
|
70
84
|
export interface StageResult {
|
|
71
85
|
stageId: StageId;
|
|
72
86
|
agent: string;
|
|
87
|
+
/**
|
|
88
|
+
* The tier this stage ACTUALLY ran on: the stage's own tier, or the
|
|
89
|
+
* `YAGNI_GO_TIER_CAP` ceiling when the run is capped (eval/smoke lanes). Set
|
|
90
|
+
* by `runStage`; optional because a caller-injected runStage seam (the unit
|
|
91
|
+
* tests, a custom lane) need not report one.
|
|
92
|
+
*/
|
|
93
|
+
tier?: ModelTier;
|
|
73
94
|
exitCode: number;
|
|
74
95
|
finalOutput: string;
|
|
75
96
|
usage: StageUsage;
|
|
@@ -202,8 +223,55 @@ export interface PipelineResult {
|
|
|
202
223
|
* Purely informational for later sessions — planResume ignores it.
|
|
203
224
|
* - `run_finish` : the run reached a terminal stopReason — makes the key NOT
|
|
204
225
|
* resumable (a completed run, even `round_cap`, is not "interrupted").
|
|
226
|
+
* - `fanout_decision`: the implement diamond resolved how to build (fan + width,
|
|
227
|
+
* or the single-writer degrade and why). Purely informational —
|
|
228
|
+
* `planResume` ignores it, exactly as it ignores `finish_done` —
|
|
229
|
+
* so the decision survives the run for the journal and the /go
|
|
230
|
+
* status surfaces without ever steering a resume.
|
|
231
|
+
*/
|
|
232
|
+
export type CheckpointKind = "run_start" | "build_done" | "round_done" | "finish_done" | "run_finish" | "fanout_decision";
|
|
233
|
+
/**
|
|
234
|
+
* The recorded implement-diamond verdict (spec decision 2 + 3): the fan and its
|
|
235
|
+
* width, or the single-writer degrade, always with the reason that produced it —
|
|
236
|
+
* the orchestrator's own words, the budget seam's, or the pipeline's when a
|
|
237
|
+
* partition could not be read. `workstreams` carries the shape of the fan (name,
|
|
238
|
+
* tier, how many paths it claimed), never the claims themselves: the journal is a
|
|
239
|
+
* decision record, not a second copy of the partition.
|
|
240
|
+
*/
|
|
241
|
+
export interface FanoutCheckpoint {
|
|
242
|
+
mode: "fan" | "single";
|
|
243
|
+
width?: PartitionWidth;
|
|
244
|
+
reason: string;
|
|
245
|
+
workstreams?: FanoutWorkstreamShape[];
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The shape of ONE workstream as the run records it: its name, the tier it was
|
|
249
|
+
* assigned, and how many paths it claimed. Never the claims themselves — a
|
|
250
|
+
* recording surface names the child and how much of the plan it holds, and the
|
|
251
|
+
* partition itself stays in the stage's own handoff.
|
|
252
|
+
*/
|
|
253
|
+
export interface FanoutWorkstreamShape {
|
|
254
|
+
name: string;
|
|
255
|
+
tier: WorkstreamTier;
|
|
256
|
+
files: number;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* `go.fanout`: `auto` lets the partitioner's own (conservative) verdict stand;
|
|
260
|
+
* `always` pins the diamond attempt for benchmark/eval lanes — it tells the
|
|
261
|
+
* partitioner this run is pinned and overrides a budget denial, but it can still
|
|
262
|
+
* only fan on a partition the orchestrator actually produced (a partition is
|
|
263
|
+
* never guessed, spec decision 3).
|
|
264
|
+
*/
|
|
265
|
+
export type FanoutMode = "auto" | "always";
|
|
266
|
+
/**
|
|
267
|
+
* The injected budget seam's verdict (spec decision 8). The budget machinery is a
|
|
268
|
+
* separate workstream; this pipeline only consults the seam, and an ABSENT seam
|
|
269
|
+
* allows. `reason` is rendered as the recorded single-writer decision.
|
|
205
270
|
*/
|
|
206
|
-
export
|
|
271
|
+
export interface FanoutBudgetVerdict {
|
|
272
|
+
allow: boolean;
|
|
273
|
+
reason?: string;
|
|
274
|
+
}
|
|
207
275
|
/** One append-only journal record. Latest-applicable record wins on resume. */
|
|
208
276
|
export interface CheckpointRecord {
|
|
209
277
|
v: 1;
|
|
@@ -235,6 +303,8 @@ export interface CheckpointRecord {
|
|
|
235
303
|
commitSha?: string;
|
|
236
304
|
/** finish_done: the PR opened by --pr (absent when none was). */
|
|
237
305
|
prUrl?: string;
|
|
306
|
+
/** fanout_decision: how the implement stage resolved to build (see {@link FanoutCheckpoint}). */
|
|
307
|
+
fanout?: FanoutCheckpoint;
|
|
238
308
|
/** Cumulative run usage through this completed boundary. */
|
|
239
309
|
usage?: RunUsage;
|
|
240
310
|
}
|
|
@@ -343,6 +413,13 @@ export interface StageTag {
|
|
|
343
413
|
stageId: StageId;
|
|
344
414
|
lens?: ReviewLens;
|
|
345
415
|
round?: number;
|
|
416
|
+
/**
|
|
417
|
+
* Which fan-out child of the implement diamond this event belongs to. Set for
|
|
418
|
+
* the parallel builders (and their fix-turn re-engagements) exactly the way
|
|
419
|
+
* `lens` is set for the parallel review children, so interleaved events from
|
|
420
|
+
* concurrent builders stay attributed. Absent on the single-writer path.
|
|
421
|
+
*/
|
|
422
|
+
workstream?: string;
|
|
346
423
|
}
|
|
347
424
|
/**
|
|
348
425
|
* Structured progress signal (replaces the bare `onProgress(string)`); the feed
|
|
@@ -355,11 +432,14 @@ export type PipelineProgress = {
|
|
|
355
432
|
kind: "stage_start";
|
|
356
433
|
stageId: FeedStageId;
|
|
357
434
|
lens?: ReviewLens;
|
|
435
|
+
workstream?: string;
|
|
358
436
|
round?: number;
|
|
359
437
|
} | {
|
|
360
438
|
kind: "stage_done";
|
|
361
439
|
stageId: FeedStageId;
|
|
362
440
|
lens?: ReviewLens;
|
|
441
|
+
/** The fan-out child this beat belongs to (implement diamond); see {@link StageTag}. */
|
|
442
|
+
workstream?: string;
|
|
363
443
|
round?: number;
|
|
364
444
|
/**
|
|
365
445
|
* Per-lens finding counts. Attached only on the SECOND `stage_done` a lens
|
|
@@ -380,12 +460,51 @@ export type PipelineProgress = {
|
|
|
380
460
|
round: number;
|
|
381
461
|
total: number;
|
|
382
462
|
blocking: number;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* The implement diamond's partition verdict, emitted once per implement stage
|
|
466
|
+
* the moment it resolves: the fan and its width, or the single-writer degrade.
|
|
467
|
+
* `reason` is the orchestrator's own words (or the budget seam's), so the
|
|
468
|
+
* terminal summary and the run surface render the decision that was actually
|
|
469
|
+
* made rather than inferring one from the children that followed.
|
|
470
|
+
*
|
|
471
|
+
* `workstreams` carries the fan's shape (the same record the checkpoint
|
|
472
|
+
* journal keeps) so a recording surface can name each child, and its tier and
|
|
473
|
+
* claim count, from the decision alone rather than waiting to infer them from
|
|
474
|
+
* the beats that follow. Absent on the single-writer degrade.
|
|
475
|
+
*/
|
|
476
|
+
| {
|
|
477
|
+
kind: "fanout";
|
|
478
|
+
mode: "fan" | "single";
|
|
479
|
+
width?: PartitionWidth;
|
|
480
|
+
reason: string;
|
|
481
|
+
workstreams?: FanoutWorkstreamShape[];
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* One bounded fix turn of the implement diamond (spec decision 7): the turn
|
|
485
|
+
* number, how many findings that turn was answering, and which children were
|
|
486
|
+
* re-engaged to answer them (workstream names, or the synthesizer for
|
|
487
|
+
* unattributable findings). Emitted at most {@link MAX_FIX_TURNS} times per
|
|
488
|
+
* implement stage, and on the single-writer path too.
|
|
489
|
+
*/
|
|
490
|
+
| {
|
|
491
|
+
kind: "fix_turn";
|
|
492
|
+
turn: number;
|
|
493
|
+
findings: number;
|
|
494
|
+
reengaged: string[];
|
|
383
495
|
} | {
|
|
384
496
|
kind: "done";
|
|
385
497
|
stopReason: StopReason;
|
|
386
498
|
};
|
|
387
499
|
/** Stop the review→fix loop after at most this many rounds (spec §7.3). */
|
|
388
500
|
export declare const MAX_REVIEW_ROUNDS = 3;
|
|
501
|
+
/**
|
|
502
|
+
* Hard cap on the implement diamond's fix turns (spec decision 7). Verification
|
|
503
|
+
* may re-engage the builders that broke the tree, but only this many times: past
|
|
504
|
+
* the cap the remaining findings are named in `reviewInput` and the review loop
|
|
505
|
+
* takes over, rather than the stage grinding on the same defect forever.
|
|
506
|
+
*/
|
|
507
|
+
export declare const MAX_FIX_TURNS = 3;
|
|
389
508
|
/**
|
|
390
509
|
* R1 in-loop resilience policy: the tuning surface for {@link withResilience},
|
|
391
510
|
* the HOF that wraps every per-stage child spawn with idle + wall-clock timeouts
|
|
@@ -430,6 +549,16 @@ export declare const TOOL_ERROR_FAIL_RATE = 0.6;
|
|
|
430
549
|
export declare const MIN_TOOL_CALLS_FOR_HEALTH = 4;
|
|
431
550
|
/** Bound on parallel review-lens children per round (mirrors the subagent example). */
|
|
432
551
|
export declare const MAX_CONCURRENCY = 4;
|
|
552
|
+
/**
|
|
553
|
+
* Bound on parallel implement-diamond builders, and the wider ceiling while the
|
|
554
|
+
* session is in ultra mode. Deliberately the same numbers as the general subagent
|
|
555
|
+
* tool's `MAX_PARALLEL_SUBAGENTS` / `MAX_PARALLEL_SUBAGENTS_ULTRA` (subagents.ts)
|
|
556
|
+
* — one proven parallel-children ceiling for this process, restated here so the
|
|
557
|
+
* pipeline never imports the tool layer. A width-8 partition is therefore legal
|
|
558
|
+
* outside ultra: it runs 4 at a time rather than being refused.
|
|
559
|
+
*/
|
|
560
|
+
export declare const MAX_FANOUT_CONCURRENCY = 4;
|
|
561
|
+
export declare const MAX_FANOUT_CONCURRENCY_ULTRA = 8;
|
|
433
562
|
/** Per-stage final-output byte cap when handed forward as `{previous}`. */
|
|
434
563
|
export declare const PER_TASK_OUTPUT_CAP: number;
|
|
435
564
|
/** Max length of a narration headline surfaced into the feed (spec §3). */
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/** Stop the review→fix loop after at most this many rounds (spec §7.3). */
|
|
10
10
|
export const MAX_REVIEW_ROUNDS = 3;
|
|
11
|
+
/**
|
|
12
|
+
* Hard cap on the implement diamond's fix turns (spec decision 7). Verification
|
|
13
|
+
* may re-engage the builders that broke the tree, but only this many times: past
|
|
14
|
+
* the cap the remaining findings are named in `reviewInput` and the review loop
|
|
15
|
+
* takes over, rather than the stage grinding on the same defect forever.
|
|
16
|
+
*/
|
|
17
|
+
export const MAX_FIX_TURNS = 3;
|
|
11
18
|
/**
|
|
12
19
|
* Default resilience policy. Generous timeouts so a legitimately long but live
|
|
13
20
|
* stage is never killed (the idle window sits well under the 5-min run heartbeat,
|
|
@@ -39,6 +46,16 @@ export const TOOL_ERROR_FAIL_RATE = 0.6;
|
|
|
39
46
|
export const MIN_TOOL_CALLS_FOR_HEALTH = 4;
|
|
40
47
|
/** Bound on parallel review-lens children per round (mirrors the subagent example). */
|
|
41
48
|
export const MAX_CONCURRENCY = 4;
|
|
49
|
+
/**
|
|
50
|
+
* Bound on parallel implement-diamond builders, and the wider ceiling while the
|
|
51
|
+
* session is in ultra mode. Deliberately the same numbers as the general subagent
|
|
52
|
+
* tool's `MAX_PARALLEL_SUBAGENTS` / `MAX_PARALLEL_SUBAGENTS_ULTRA` (subagents.ts)
|
|
53
|
+
* — one proven parallel-children ceiling for this process, restated here so the
|
|
54
|
+
* pipeline never imports the tool layer. A width-8 partition is therefore legal
|
|
55
|
+
* outside ultra: it runs 4 at a time rather than being refused.
|
|
56
|
+
*/
|
|
57
|
+
export const MAX_FANOUT_CONCURRENCY = 4;
|
|
58
|
+
export const MAX_FANOUT_CONCURRENCY_ULTRA = 8;
|
|
42
59
|
/** Per-stage final-output byte cap when handed forward as `{previous}`. */
|
|
43
60
|
export const PER_TASK_OUTPUT_CAP = 50 * 1024;
|
|
44
61
|
/** Max length of a narration headline surfaced into the feed (spec §3). */
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* the fix stage gets the real errors.
|
|
10
10
|
*
|
|
11
11
|
* `detectVerifyCommands`, `detectVerifyTestCommands`, `parseVerifyFailures`,
|
|
12
|
-
* `classifyTestFailure`, `isFannedOutScript`, `parseChangedPaths
|
|
13
|
-
* `resolvePackageDirs` are PURE (unit-tested directly).
|
|
14
|
-
*
|
|
12
|
+
* `classifyTestFailure`, `isFannedOutScript`, `parseChangedPaths`,
|
|
13
|
+
* `resolvePackageDirs` and `resolveClaimDirs` are PURE (unit-tested directly).
|
|
14
|
+
* `makeRunVerify` (the whole gate) and `makeWorkstreamCheck` (the implement
|
|
15
|
+
* diamond's per-workstream scoped typecheck, spec decision 6) are the thin impure
|
|
16
|
+
* runners; they shell out (pi's ExtensionContext exposes no exec,
|
|
15
17
|
* mirroring workspace.ts) and is wrapped with R1's timeout composition
|
|
16
18
|
* (`composeAbortSignal`) so a slow or flaky suite is bounded.
|
|
17
19
|
*
|
|
@@ -215,6 +217,22 @@ export declare function classifyTestFailure(output: string, exitCode: number): T
|
|
|
215
217
|
* throws; always emits at least one finding on a non-zero exit.
|
|
216
218
|
*/
|
|
217
219
|
export declare function parseVerifyFailures(output: string, exitCode: number, label: string): Finding[];
|
|
220
|
+
/**
|
|
221
|
+
* One finding's file as the REPO ROOT sees it.
|
|
222
|
+
*
|
|
223
|
+
* The gate is scoped by design: a monorepo package's `typecheck` / `test` runs
|
|
224
|
+
* IN that package's dir, so its diagnostics come back package-relative
|
|
225
|
+
* (`src/routes/x.ts`). Everything downstream speaks repo-root-relative paths —
|
|
226
|
+
* the partition's file claims, the handoff, the fix loop's attribution
|
|
227
|
+
* (`checker.ts`'s `ownerOf`) — so an un-lifted path can never match the
|
|
228
|
+
* workstream that owns it and the implicated builder is never re-engaged.
|
|
229
|
+
*
|
|
230
|
+
* Lifts a package-relative path onto the command's dir, makes an absolute path
|
|
231
|
+
* under the repo root relative to it, and leaves anything else (a path outside
|
|
232
|
+
* the tree, a path already carrying the package prefix) untouched. Idempotent,
|
|
233
|
+
* so a finding that passes through twice is not double-prefixed.
|
|
234
|
+
*/
|
|
235
|
+
export declare function toRepoRelativePath(file: string, repoRoot: string, commandCwd: string): string;
|
|
218
236
|
/** Result of actually executing the resolved command. */
|
|
219
237
|
export interface VerifyExecResult {
|
|
220
238
|
code: number;
|
|
@@ -268,6 +286,71 @@ export interface MakeRunVerifyOpts {
|
|
|
268
286
|
/** Test-half env override for determinism in tests (default: buildTestEnv() per run). */
|
|
269
287
|
testEnv?: NodeJS.ProcessEnv;
|
|
270
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* One workstream as the scoped checker sees it: the name its findings are
|
|
291
|
+
* attributed to, and the prefix claims the partition gave it.
|
|
292
|
+
*/
|
|
293
|
+
export interface WorkstreamCheckTarget {
|
|
294
|
+
name: string;
|
|
295
|
+
files: string[];
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* One workstream's scoped typecheck verdict (spec decision 6, first half).
|
|
299
|
+
* `ran: false` is the honest fail-open: the claims resolved to no package with a
|
|
300
|
+
* usable build check, so the workstream is UNCHECKED rather than falsely clean.
|
|
301
|
+
*/
|
|
302
|
+
export interface WorkstreamCheckResult {
|
|
303
|
+
name: string;
|
|
304
|
+
ran: boolean;
|
|
305
|
+
ok: boolean;
|
|
306
|
+
command?: string;
|
|
307
|
+
reason?: string;
|
|
308
|
+
findings: Finding[];
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* A workstream's claims resolved to package dirs. Claims are PREFIXES and a claim
|
|
312
|
+
* is very often a directory (`packages/backend/src/routes`), while
|
|
313
|
+
* {@link resolvePackageDirs} takes the FILE paths git porcelain emits and walks up
|
|
314
|
+
* from each one's dirname. Appending a probe segment makes a directory claim walk
|
|
315
|
+
* up from the directory ITSELF, so a claim that IS a package dir resolves to that
|
|
316
|
+
* package instead of its parent; a claim that is a real file still resolves exactly
|
|
317
|
+
* as it does for the full gate. Pure (the `readFile` seam keeps it so).
|
|
318
|
+
*/
|
|
319
|
+
export declare function resolveClaimDirs(repoRoot: string, claims: string[], readFile: (path: string) => string): string[];
|
|
320
|
+
export interface MakeWorkstreamCheckOpts {
|
|
321
|
+
/** Claims → package dirs. Default: {@link resolveClaimDirs} over the real fs. */
|
|
322
|
+
claimDirs?: (cwd: string, claims: string[]) => string[];
|
|
323
|
+
/** The build check(s) for those dirs. Default: the same detection the full gate uses. */
|
|
324
|
+
detect?: (cwd: string, claimDirs: string[]) => VerifyCommand[];
|
|
325
|
+
exec?: VerifyExec;
|
|
326
|
+
timeoutMs?: number;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The per-workstream scoped check the implement diamond runs right after the fan
|
|
330
|
+
* (spec decision 6): the TYPECHECK half only, run in the package dirs a
|
|
331
|
+
* workstream's own claims resolve to, so a broken workstream is caught WITH
|
|
332
|
+
* attribution instead of surfacing later as an anonymous failure of the merged
|
|
333
|
+
* tree. The test half is deliberately not run here — the repo's tests run ONCE, on
|
|
334
|
+
* the merged candidate, through {@link makeRunVerify}.
|
|
335
|
+
*
|
|
336
|
+
* Three rules keep the attribution honest:
|
|
337
|
+
* - a command is kept only when it runs IN one of the workstream's own claim dirs.
|
|
338
|
+
* {@link detectVerifyCommands} falls back to the repo-root script when nothing
|
|
339
|
+
* scoped qualifies, and a repo-wide typecheck blamed on one workstream is
|
|
340
|
+
* exactly the false attribution this check exists to avoid.
|
|
341
|
+
* - identical commands are executed ONCE per pass and their result shared, so two
|
|
342
|
+
* workstreams inside the same package do not typecheck it twice.
|
|
343
|
+
* - a workstream is tagged only with the failures its OWN claims cover. A shared
|
|
344
|
+
* package's check can fail on its sibling's files (or on files neither claims):
|
|
345
|
+
* tagging every target with the whole output would report a healthy workstream
|
|
346
|
+
* broken and defeat the checker's file+line+message dedupe against the full
|
|
347
|
+
* verify. Failures outside the target's claims surface ONCE, untagged, so
|
|
348
|
+
* {@link attributeFindings} routes them by file or hands them to the synthesizer.
|
|
349
|
+
*
|
|
350
|
+
* Fail-open everywhere, like the full gate: an unrunnable check records a reason
|
|
351
|
+
* and produces no findings.
|
|
352
|
+
*/
|
|
353
|
+
export declare function makeWorkstreamCheck(opts?: MakeWorkstreamCheckOpts): (cwd: string, targets: WorkstreamCheckTarget[], signal?: AbortSignal) => Promise<WorkstreamCheckResult[]>;
|
|
271
354
|
/**
|
|
272
355
|
* Build the runVerify function injected into the orchestrator. It discovers the
|
|
273
356
|
* changed package(s) (scoped to this run's diff via `baselinePaths`), detects a
|