@yagni-app/code 0.3.2 → 0.3.4

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.
Files changed (60) hide show
  1. package/dist/cli.js +13 -0
  2. package/dist/extension/config.d.ts +8 -0
  3. package/dist/extension/config.js +6 -0
  4. package/dist/extension/footer.d.ts +1 -1
  5. package/dist/extension/hooks.d.ts +111 -0
  6. package/dist/extension/hooks.js +666 -0
  7. package/dist/extension/index.d.ts +13 -6
  8. package/dist/extension/index.js +68 -8
  9. package/dist/extension/{approvedPrefixes.js → permission/approvedPrefixes.js} +1 -1
  10. package/dist/extension/permission/dbReadPolicy.d.ts +90 -0
  11. package/dist/extension/permission/dbReadPolicy.js +227 -0
  12. package/dist/extension/{execPolicy.js → permission/execPolicy.js} +41 -13
  13. package/dist/extension/{permission.d.ts → permission/gate.d.ts} +9 -2
  14. package/dist/extension/{permission.js → permission/gate.js} +103 -4
  15. package/dist/extension/{guardian.d.ts → permission/guardian.d.ts} +22 -3
  16. package/dist/extension/{guardian.js → permission/guardian.js} +42 -6
  17. package/dist/extension/permission/index.d.ts +14 -0
  18. package/dist/extension/permission/index.js +14 -0
  19. package/dist/extension/permission/packageManagerPolicy.d.ts +55 -0
  20. package/dist/extension/permission/packageManagerPolicy.js +170 -0
  21. package/dist/extension/pipeline/activityFeed.js +19 -5
  22. package/dist/extension/pipeline/checker.d.ts +99 -0
  23. package/dist/extension/pipeline/checker.js +238 -0
  24. package/dist/extension/pipeline/fanout.d.ts +116 -0
  25. package/dist/extension/pipeline/fanout.js +248 -0
  26. package/dist/extension/pipeline/fanoutBeats.d.ts +31 -0
  27. package/dist/extension/pipeline/fanoutBeats.js +86 -0
  28. package/dist/extension/pipeline/goCommand.d.ts +14 -0
  29. package/dist/extension/pipeline/goCommand.js +38 -1
  30. package/dist/extension/pipeline/headlessGo.d.ts +163 -0
  31. package/dist/extension/pipeline/headlessGo.js +333 -0
  32. package/dist/extension/pipeline/invocation.d.ts +7 -1
  33. package/dist/extension/pipeline/invocation.js +7 -1
  34. package/dist/extension/pipeline/mission.d.ts +55 -0
  35. package/dist/extension/pipeline/mission.js +70 -0
  36. package/dist/extension/pipeline/orchestrator.d.ts +48 -3
  37. package/dist/extension/pipeline/orchestrator.js +450 -9
  38. package/dist/extension/pipeline/personas.d.ts +16 -1
  39. package/dist/extension/pipeline/personas.js +117 -6
  40. package/dist/extension/pipeline/runSession.d.ts +45 -1
  41. package/dist/extension/pipeline/runState.d.ts +57 -12
  42. package/dist/extension/pipeline/runState.js +60 -18
  43. package/dist/extension/pipeline/runner.js +10 -1
  44. package/dist/extension/pipeline/stages.d.ts +84 -7
  45. package/dist/extension/pipeline/stages.js +166 -0
  46. package/dist/extension/pipeline/tierCap.d.ts +32 -0
  47. package/dist/extension/pipeline/tierCap.js +57 -0
  48. package/dist/extension/pipeline/types.d.ts +130 -1
  49. package/dist/extension/pipeline/types.js +17 -0
  50. package/dist/extension/pipeline/verify.d.ts +86 -3
  51. package/dist/extension/pipeline/verify.js +175 -6
  52. package/dist/extension/turnLog.d.ts +38 -0
  53. package/dist/extension/turnLog.js +93 -0
  54. package/dist/goHeadless.d.ts +75 -0
  55. package/dist/goHeadless.js +132 -0
  56. package/dist/paths.d.ts +9 -0
  57. package/dist/paths.js +12 -0
  58. package/package.json +2 -2
  59. /package/dist/extension/{approvedPrefixes.d.ts → permission/approvedPrefixes.d.ts} +0 -0
  60. /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 type CheckpointKind = "run_start" | "build_done" | "round_done" | "finish_done" | "run_finish";
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` and
13
- * `resolvePackageDirs` are PURE (unit-tested directly). `makeRunVerify` is the
14
- * thin impure runner; it shells out (pi's ExtensionContext exposes no exec,
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