@yagni-app/code-staging 0.3.2-staging.1112.1 → 0.3.2-staging.1115.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.
Files changed (43) hide show
  1. package/dist/cli.js +13 -0
  2. package/dist/extension/hooks.d.ts +111 -0
  3. package/dist/extension/hooks.js +666 -0
  4. package/dist/extension/index.d.ts +7 -0
  5. package/dist/extension/index.js +22 -0
  6. package/dist/extension/permission.d.ts +7 -0
  7. package/dist/extension/permission.js +100 -1
  8. package/dist/extension/pipeline/activityFeed.js +19 -5
  9. package/dist/extension/pipeline/checker.d.ts +99 -0
  10. package/dist/extension/pipeline/checker.js +238 -0
  11. package/dist/extension/pipeline/fanout.d.ts +116 -0
  12. package/dist/extension/pipeline/fanout.js +248 -0
  13. package/dist/extension/pipeline/fanoutBeats.d.ts +31 -0
  14. package/dist/extension/pipeline/fanoutBeats.js +86 -0
  15. package/dist/extension/pipeline/goCommand.d.ts +14 -0
  16. package/dist/extension/pipeline/goCommand.js +38 -1
  17. package/dist/extension/pipeline/headlessGo.d.ts +163 -0
  18. package/dist/extension/pipeline/headlessGo.js +333 -0
  19. package/dist/extension/pipeline/invocation.d.ts +7 -1
  20. package/dist/extension/pipeline/invocation.js +7 -1
  21. package/dist/extension/pipeline/mission.d.ts +55 -0
  22. package/dist/extension/pipeline/mission.js +70 -0
  23. package/dist/extension/pipeline/orchestrator.d.ts +48 -3
  24. package/dist/extension/pipeline/orchestrator.js +450 -9
  25. package/dist/extension/pipeline/personas.d.ts +16 -1
  26. package/dist/extension/pipeline/personas.js +117 -6
  27. package/dist/extension/pipeline/runSession.d.ts +45 -1
  28. package/dist/extension/pipeline/runState.d.ts +57 -12
  29. package/dist/extension/pipeline/runState.js +60 -18
  30. package/dist/extension/pipeline/runner.js +10 -1
  31. package/dist/extension/pipeline/stages.d.ts +84 -7
  32. package/dist/extension/pipeline/stages.js +166 -0
  33. package/dist/extension/pipeline/tierCap.d.ts +32 -0
  34. package/dist/extension/pipeline/tierCap.js +57 -0
  35. package/dist/extension/pipeline/types.d.ts +130 -1
  36. package/dist/extension/pipeline/types.js +17 -0
  37. package/dist/extension/pipeline/verify.d.ts +86 -3
  38. package/dist/extension/pipeline/verify.js +175 -6
  39. package/dist/goHeadless.d.ts +75 -0
  40. package/dist/goHeadless.js +132 -0
  41. package/dist/paths.d.ts +9 -0
  42. package/dist/paths.js +12 -0
  43. package/package.json +2 -2
@@ -0,0 +1,116 @@
1
+ /**
2
+ * PURE partition contract for the implement diamond — the analogue of
3
+ * `findings.ts` for the fan-out orchestrator's output.
4
+ *
5
+ * `parsePartition` reads the orchestrator child's fenced ```partition JSON block
6
+ * and validates it against the contract (spec "Partition contract"): mode legal,
7
+ * width in {2,4,8} with a matching workstream count, per-workstream tier legal
8
+ * (absent defaults to `standard`), every workstream named + tasked with at least
9
+ * one file claim, and claims pairwise DISJOINT across workstreams.
10
+ *
11
+ * Two failure kinds, deliberately distinct because the pipeline treats them
12
+ * differently:
13
+ * - SOFT (`hard: false`): malformed block, illegal field, missing reason. The
14
+ * caller gets one cheap-tier format re-ask and then degrades to single-writer.
15
+ * A partition is never guessed.
16
+ * - HARD (`hard: true`): overlapping claims. Two writers on one file is the one
17
+ * thing the design refuses to discover at merge time, so it fails the stage at
18
+ * partition time with the colliding paths and workstreams named. The caller
19
+ * (orchestrator) raises it as a `PipelineStageError`; keeping the throw out of
20
+ * here keeps this module pure and free of an import cycle.
21
+ *
22
+ * `auditClaims` is the post-fan check: claims are path PREFIXES (a directory
23
+ * claims its whole subtree), so a file a builder legitimately created inside its
24
+ * claimed directory is in-claim, and anything else is named as a violation for
25
+ * the synthesizer to reconcile.
26
+ */
27
+ import type { FanoutMode } from "./types.js";
28
+ /** The tiers a workstream may run on: load-bearing work standard, mechanical work efficient. */
29
+ export type WorkstreamTier = "standard" | "efficient";
30
+ /**
31
+ * The `go.fanout` knob's environment surface (spec decision 8), named the way
32
+ * `YAGNI_GO_TIER_CAP` is (see tierCap.ts): a run-wide /go setting an eval or
33
+ * benchmark lane pins from the outside. `always` forces the diamond attempt;
34
+ * unset (everywhere else) leaves the partitioner's own conservative verdict as
35
+ * the only thing that decides.
36
+ */
37
+ export declare const FANOUT_MODE_ENV = "YAGNI_GO_FANOUT";
38
+ /**
39
+ * Parse a raw env value into a mode. Unset, empty, or unrecognized all yield
40
+ * undefined so a caller can warn about a typo; {@link resolveFanoutMode} is what
41
+ * turns that into the safe `auto` default. A typo must never pin a lane.
42
+ */
43
+ export declare function parseFanoutMode(raw: string | undefined): FanoutMode | undefined;
44
+ /** The run's fan-out mode from an environment. Defaults to `auto`. */
45
+ export declare function resolveFanoutMode(env: NodeJS.ProcessEnv): FanoutMode;
46
+ /** The only legal fan widths (execution concurrency is capped separately by the session). */
47
+ export declare const PARTITION_WIDTHS: readonly [2, 4, 8];
48
+ export type PartitionWidth = (typeof PARTITION_WIDTHS)[number];
49
+ /** One workstream: a named, tasked builder with a disjoint set of prefix claims. */
50
+ export interface PartitionWorkstream {
51
+ name: string;
52
+ task: string;
53
+ /** Normalized, deduped path prefixes; a directory claim covers its subtree. */
54
+ files: string[];
55
+ tier: WorkstreamTier;
56
+ }
57
+ /** The orchestrator's verdict: fan into workstreams, or run today's single writer. */
58
+ export interface PartitionDecision {
59
+ mode: "fan" | "single";
60
+ /** Fan only; always equals `workstreams.length`. */
61
+ width?: PartitionWidth;
62
+ /** Always present: why this width, or why single-writer. */
63
+ reason: string;
64
+ /** Fan only. */
65
+ workstreams?: PartitionWorkstream[];
66
+ }
67
+ /** One pair of colliding claims across two workstreams. */
68
+ export interface ClaimCollision {
69
+ path: string;
70
+ otherPath: string;
71
+ workstreams: [string, string];
72
+ }
73
+ /**
74
+ * Parse outcome. `ok: false` splits into the re-askable soft rejection and the
75
+ * hard overlap error (see the module comment).
76
+ */
77
+ export type PartitionParse = {
78
+ ok: true;
79
+ decision: PartitionDecision;
80
+ } | {
81
+ ok: false;
82
+ hard: false;
83
+ reason: string;
84
+ } | {
85
+ ok: false;
86
+ hard: true;
87
+ reason: string;
88
+ collisions: ClaimCollision[];
89
+ };
90
+ /** Extract the inner text of a fenced ```partition block, if present. */
91
+ export declare function extractPartitionBlock(raw: string): string | null;
92
+ /**
93
+ * Normalize a claim or changed path for prefix comparison: trim, convert
94
+ * backslashes, collapse doubled slashes, drop interior `.` segments, a leading
95
+ * `./`, and trailing slashes. Purely lexical — no filesystem access. Without
96
+ * the collapse, `packages//backend` and `packages/./backend` pass disjointness
97
+ * against `packages/backend` while claiming the same directory — the exact
98
+ * two-writers-one-file outcome the partition-time hard error exists to prevent.
99
+ */
100
+ export declare function normalizeClaimPath(path: string): string;
101
+ /** True when `claim` is the path itself or a directory containing it. */
102
+ export declare function claimCovers(claim: string, path: string): boolean;
103
+ /**
104
+ * Parse the orchestrator's raw output into a {@link PartitionDecision}. Never
105
+ * throws: an unusable block is a soft rejection (re-ask, then single-writer) and
106
+ * overlapping claims are the hard rejection the caller turns into a stage error.
107
+ */
108
+ export declare function parsePartition(raw: string): PartitionParse;
109
+ /**
110
+ * Post-fan claim audit: the changed paths not covered by any workstream's claims,
111
+ * normalized, deduped, in first-seen order. Claims are prefixes, so a file created
112
+ * under a claimed directory is in-claim; anything else is a named violation the
113
+ * synthesizer reconciles.
114
+ */
115
+ export declare function auditClaims(changedPaths: string[], workstreams: PartitionWorkstream[]): string[];
116
+ //# sourceMappingURL=fanout.d.ts.map
@@ -0,0 +1,248 @@
1
+ /**
2
+ * PURE partition contract for the implement diamond — the analogue of
3
+ * `findings.ts` for the fan-out orchestrator's output.
4
+ *
5
+ * `parsePartition` reads the orchestrator child's fenced ```partition JSON block
6
+ * and validates it against the contract (spec "Partition contract"): mode legal,
7
+ * width in {2,4,8} with a matching workstream count, per-workstream tier legal
8
+ * (absent defaults to `standard`), every workstream named + tasked with at least
9
+ * one file claim, and claims pairwise DISJOINT across workstreams.
10
+ *
11
+ * Two failure kinds, deliberately distinct because the pipeline treats them
12
+ * differently:
13
+ * - SOFT (`hard: false`): malformed block, illegal field, missing reason. The
14
+ * caller gets one cheap-tier format re-ask and then degrades to single-writer.
15
+ * A partition is never guessed.
16
+ * - HARD (`hard: true`): overlapping claims. Two writers on one file is the one
17
+ * thing the design refuses to discover at merge time, so it fails the stage at
18
+ * partition time with the colliding paths and workstreams named. The caller
19
+ * (orchestrator) raises it as a `PipelineStageError`; keeping the throw out of
20
+ * here keeps this module pure and free of an import cycle.
21
+ *
22
+ * `auditClaims` is the post-fan check: claims are path PREFIXES (a directory
23
+ * claims its whole subtree), so a file a builder legitimately created inside its
24
+ * claimed directory is in-claim, and anything else is named as a violation for
25
+ * the synthesizer to reconcile.
26
+ */
27
+ /**
28
+ * The `go.fanout` knob's environment surface (spec decision 8), named the way
29
+ * `YAGNI_GO_TIER_CAP` is (see tierCap.ts): a run-wide /go setting an eval or
30
+ * benchmark lane pins from the outside. `always` forces the diamond attempt;
31
+ * unset (everywhere else) leaves the partitioner's own conservative verdict as
32
+ * the only thing that decides.
33
+ */
34
+ export const FANOUT_MODE_ENV = "YAGNI_GO_FANOUT";
35
+ /**
36
+ * Parse a raw env value into a mode. Unset, empty, or unrecognized all yield
37
+ * undefined so a caller can warn about a typo; {@link resolveFanoutMode} is what
38
+ * turns that into the safe `auto` default. A typo must never pin a lane.
39
+ */
40
+ export function parseFanoutMode(raw) {
41
+ const normalized = raw?.trim().toLowerCase();
42
+ if (normalized === "auto" || normalized === "always")
43
+ return normalized;
44
+ return undefined;
45
+ }
46
+ /** The run's fan-out mode from an environment. Defaults to `auto`. */
47
+ export function resolveFanoutMode(env) {
48
+ return parseFanoutMode(env[FANOUT_MODE_ENV]) ?? "auto";
49
+ }
50
+ /** The only legal fan widths (execution concurrency is capped separately by the session). */
51
+ export const PARTITION_WIDTHS = [2, 4, 8];
52
+ const TIERS = ["standard", "efficient"];
53
+ function soft(reason) {
54
+ return { ok: false, hard: false, reason };
55
+ }
56
+ /** Extract the inner text of a fenced ```partition block, if present. */
57
+ export function extractPartitionBlock(raw) {
58
+ const m = raw.match(/```partition[^\n]*\n([\s\S]*?)```/i);
59
+ return m ? m[1] : null;
60
+ }
61
+ /**
62
+ * Normalize a claim or changed path for prefix comparison: trim, convert
63
+ * backslashes, collapse doubled slashes, drop interior `.` segments, a leading
64
+ * `./`, and trailing slashes. Purely lexical — no filesystem access. Without
65
+ * the collapse, `packages//backend` and `packages/./backend` pass disjointness
66
+ * against `packages/backend` while claiming the same directory — the exact
67
+ * two-writers-one-file outcome the partition-time hard error exists to prevent.
68
+ */
69
+ export function normalizeClaimPath(path) {
70
+ return path
71
+ .trim()
72
+ .replace(/\\/g, "/")
73
+ .split("/")
74
+ .filter((segment) => segment.length > 0 && segment !== ".")
75
+ .join("/");
76
+ }
77
+ /** True when `claim` is the path itself or a directory containing it. */
78
+ export function claimCovers(claim, path) {
79
+ const c = normalizeClaimPath(claim);
80
+ const p = normalizeClaimPath(path);
81
+ if (!c || !p)
82
+ return false;
83
+ return p === c || p.startsWith(`${c}/`);
84
+ }
85
+ /** Reject claims that escape the repo, are absolute, or claim everything. */
86
+ function claimIsLegal(claim) {
87
+ if (!claim || claim === "." || claim.startsWith("/") || claim.startsWith("\\"))
88
+ return false;
89
+ return !normalizeClaimPath(claim).split("/").includes("..");
90
+ }
91
+ function readWorkstream(value, index) {
92
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
93
+ return `workstream ${index + 1} is not an object`;
94
+ }
95
+ const obj = value;
96
+ const name = typeof obj.name === "string" ? obj.name.trim() : "";
97
+ if (!name)
98
+ return `workstream ${index + 1} has no name`;
99
+ const task = typeof obj.task === "string" ? obj.task.trim() : "";
100
+ if (!task)
101
+ return `workstream "${name}" has no task`;
102
+ if (!Array.isArray(obj.files))
103
+ return `workstream "${name}" has no file claims`;
104
+ const files = [];
105
+ for (const entry of obj.files) {
106
+ if (typeof entry !== "string")
107
+ return `workstream "${name}" has a non-string file claim`;
108
+ // Legality reads the RAW entry: normalization collapses the leading `/`
109
+ // (and `\`) markers legality has to see.
110
+ const claim = normalizeClaimPath(entry);
111
+ if (!claim || !claimIsLegal(entry.trim())) {
112
+ return `workstream "${name}" has an illegal file claim: ${entry}`;
113
+ }
114
+ if (!files.includes(claim))
115
+ files.push(claim);
116
+ }
117
+ if (files.length === 0)
118
+ return `workstream "${name}" has no file claims`;
119
+ let tier = "standard";
120
+ if (obj.tier !== undefined) {
121
+ if (typeof obj.tier !== "string" || !TIERS.includes(obj.tier)) {
122
+ return `workstream "${name}" has an illegal tier: ${String(obj.tier)}`;
123
+ }
124
+ tier = obj.tier;
125
+ }
126
+ return { name, task, files, tier };
127
+ }
128
+ /**
129
+ * Find every pair of claims that collide across two workstreams. Prefix nesting
130
+ * counts: a workstream claiming `packages/backend/src` collides with a sibling
131
+ * claiming `packages/backend/src/routes/x.ts`.
132
+ */
133
+ function findCollisions(workstreams) {
134
+ const collisions = [];
135
+ for (let i = 0; i < workstreams.length; i += 1) {
136
+ for (let j = i + 1; j < workstreams.length; j += 1) {
137
+ for (const a of workstreams[i].files) {
138
+ for (const b of workstreams[j].files) {
139
+ if (claimCovers(a, b) || claimCovers(b, a)) {
140
+ collisions.push({
141
+ path: a,
142
+ otherPath: b,
143
+ workstreams: [workstreams[i].name, workstreams[j].name],
144
+ });
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ return collisions;
151
+ }
152
+ function collisionReason(collisions) {
153
+ const parts = collisions.map((c) => c.path === c.otherPath
154
+ ? `${c.path} claimed by both ${c.workstreams[0]} and ${c.workstreams[1]}`
155
+ : `${c.path} (${c.workstreams[0]}) contains ${c.otherPath} (${c.workstreams[1]})`);
156
+ return `Overlapping file claims: ${parts.join("; ")}`;
157
+ }
158
+ /** Pull the JSON payload out of a partition fence, a json fence, or bare output. */
159
+ function extractPayload(raw) {
160
+ const partition = extractPartitionBlock(raw);
161
+ if (partition !== null)
162
+ return partition.trim();
163
+ const json = raw.match(/```json[^\n]*\n([\s\S]*?)```/i);
164
+ if (json)
165
+ return json[1].trim();
166
+ const trimmed = raw.trim();
167
+ return trimmed.startsWith("{") ? trimmed : null;
168
+ }
169
+ /**
170
+ * Parse the orchestrator's raw output into a {@link PartitionDecision}. Never
171
+ * throws: an unusable block is a soft rejection (re-ask, then single-writer) and
172
+ * overlapping claims are the hard rejection the caller turns into a stage error.
173
+ */
174
+ export function parsePartition(raw) {
175
+ if (!raw || !raw.trim())
176
+ return soft("empty partition output");
177
+ const payload = extractPayload(raw);
178
+ if (payload === null)
179
+ return soft("no ```partition block in the output");
180
+ let parsed;
181
+ try {
182
+ parsed = JSON.parse(payload);
183
+ }
184
+ catch {
185
+ return soft("the partition block is not valid JSON");
186
+ }
187
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
188
+ return soft("the partition block is not a JSON object");
189
+ }
190
+ const obj = parsed;
191
+ const mode = obj.mode;
192
+ if (mode !== "fan" && mode !== "single") {
193
+ return soft(`illegal partition mode: ${String(mode)}`);
194
+ }
195
+ const reason = typeof obj.reason === "string" ? obj.reason.trim() : "";
196
+ if (!reason)
197
+ return soft("the partition block has no reason");
198
+ if (mode === "single")
199
+ return { ok: true, decision: { mode: "single", reason } };
200
+ const width = obj.width;
201
+ if (typeof width !== "number" || !PARTITION_WIDTHS.includes(width)) {
202
+ return soft(`illegal fan width: ${String(width)}`);
203
+ }
204
+ if (!Array.isArray(obj.workstreams))
205
+ return soft("a fan partition has no workstreams");
206
+ const workstreams = [];
207
+ for (const [index, entry] of obj.workstreams.entries()) {
208
+ const read = readWorkstream(entry, index);
209
+ if (typeof read === "string")
210
+ return soft(read);
211
+ if (workstreams.some((w) => w.name === read.name)) {
212
+ return soft(`duplicate workstream name: ${read.name}`);
213
+ }
214
+ workstreams.push(read);
215
+ }
216
+ if (workstreams.length !== width) {
217
+ return soft(`fan width ${width} does not match ${workstreams.length} workstreams`);
218
+ }
219
+ const collisions = findCollisions(workstreams);
220
+ if (collisions.length > 0) {
221
+ return { ok: false, hard: true, reason: collisionReason(collisions), collisions };
222
+ }
223
+ return {
224
+ ok: true,
225
+ decision: { mode: "fan", width: width, reason, workstreams },
226
+ };
227
+ }
228
+ /**
229
+ * Post-fan claim audit: the changed paths not covered by any workstream's claims,
230
+ * normalized, deduped, in first-seen order. Claims are prefixes, so a file created
231
+ * under a claimed directory is in-claim; anything else is a named violation the
232
+ * synthesizer reconciles.
233
+ */
234
+ export function auditClaims(changedPaths, workstreams) {
235
+ const claims = workstreams.flatMap((w) => w.files);
236
+ const violations = [];
237
+ for (const raw of changedPaths) {
238
+ const path = normalizeClaimPath(raw);
239
+ if (!path)
240
+ continue;
241
+ if (claims.some((claim) => claimCovers(claim, path)))
242
+ continue;
243
+ if (!violations.includes(path))
244
+ violations.push(path);
245
+ }
246
+ return violations;
247
+ }
248
+ //# sourceMappingURL=fanout.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The implement diamond's RECORDING thread: pipeline progress in, run-session
3
+ * stage beats out (spec "Recording and the run surface").
4
+ *
5
+ * The pipeline already says everything the run surface needs — the partition
6
+ * verdict, each builder starting and finishing, each bounded fix turn — on the
7
+ * `onProgress` channel. This folds that stream into the additive
8
+ * `fanout` / `children` / `fixTurns` payloads on `POST /runs/:id/stages`, so the
9
+ * web run surface can render per-workstream rows without the pipeline growing a
10
+ * second reporting path.
11
+ *
12
+ * PURE and I/O-free, like `findings.ts` and `fanout.ts`: `apply` returns the beat
13
+ * to send (or null when the signal is not the diamond's), and the caller decides
14
+ * whether to POST it. Two properties are load-bearing:
15
+ *
16
+ * 1. **Recorded states only.** A child row exists only once its `stage_start`
17
+ * arrives, and it is `failed` only when the pipeline said the child died.
18
+ * There is no invented "pending", and no tick nobody earned.
19
+ * 2. **Every beat is a copy.** The roster keeps mutating as children finish, so
20
+ * a beat already handed to the (async, fail-soft) session must never change
21
+ * underneath it.
22
+ */
23
+ import type { StageArgs } from "./runSession.js";
24
+ import type { PipelineProgress } from "./types.js";
25
+ /** Folds the diamond's progress signals into the stage beats that record them. */
26
+ export interface FanoutBeats {
27
+ /** The beat this signal produces, or null when it is not the diamond's. */
28
+ apply(p: PipelineProgress): StageArgs | null;
29
+ }
30
+ export declare function makeFanoutBeats(): FanoutBeats;
31
+ //# sourceMappingURL=fanoutBeats.d.ts.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The implement diamond's RECORDING thread: pipeline progress in, run-session
3
+ * stage beats out (spec "Recording and the run surface").
4
+ *
5
+ * The pipeline already says everything the run surface needs — the partition
6
+ * verdict, each builder starting and finishing, each bounded fix turn — on the
7
+ * `onProgress` channel. This folds that stream into the additive
8
+ * `fanout` / `children` / `fixTurns` payloads on `POST /runs/:id/stages`, so the
9
+ * web run surface can render per-workstream rows without the pipeline growing a
10
+ * second reporting path.
11
+ *
12
+ * PURE and I/O-free, like `findings.ts` and `fanout.ts`: `apply` returns the beat
13
+ * to send (or null when the signal is not the diamond's), and the caller decides
14
+ * whether to POST it. Two properties are load-bearing:
15
+ *
16
+ * 1. **Recorded states only.** A child row exists only once its `stage_start`
17
+ * arrives, and it is `failed` only when the pipeline said the child died.
18
+ * There is no invented "pending", and no tick nobody earned.
19
+ * 2. **Every beat is a copy.** The roster keeps mutating as children finish, so
20
+ * a beat already handed to the (async, fail-soft) session must never change
21
+ * underneath it.
22
+ */
23
+ /** The stage every fan-out beat belongs to: the diamond lives INSIDE implement. */
24
+ const IMPLEMENT = "implement";
25
+ export function makeFanoutBeats() {
26
+ // Name -> tier + claim count from the partition verdict, so a child row can
27
+ // carry them the moment that child starts.
28
+ const shape = new Map();
29
+ const children = [];
30
+ const fixTurns = [];
31
+ const roster = () => children.map((c) => ({ ...c }));
32
+ const upsert = (name, state) => {
33
+ const existing = children.find((c) => c.name === name);
34
+ if (existing) {
35
+ existing.state = state;
36
+ }
37
+ else {
38
+ const meta = shape.get(name) ?? {};
39
+ children.push({
40
+ name,
41
+ ...(meta.tier ? { tier: meta.tier } : {}),
42
+ ...(meta.files !== undefined ? { files: meta.files } : {}),
43
+ state,
44
+ });
45
+ }
46
+ return { stage: IMPLEMENT, phase: "start", children: roster() };
47
+ };
48
+ return {
49
+ apply(p) {
50
+ switch (p.kind) {
51
+ case "fanout": {
52
+ for (const w of p.workstreams ?? [])
53
+ shape.set(w.name, { tier: w.tier, files: w.files });
54
+ return {
55
+ stage: IMPLEMENT,
56
+ phase: "start",
57
+ fanout: {
58
+ mode: p.mode,
59
+ ...(p.width ? { width: p.width } : {}),
60
+ reason: p.reason,
61
+ },
62
+ };
63
+ }
64
+ case "stage_start":
65
+ if (p.stageId !== IMPLEMENT || !p.workstream)
66
+ return null;
67
+ return upsert(p.workstream, "running");
68
+ case "stage_done":
69
+ if (p.stageId !== IMPLEMENT || !p.workstream)
70
+ return null;
71
+ return upsert(p.workstream, p.degraded === "failed" ? "failed" : "done");
72
+ case "fix_turn": {
73
+ fixTurns.push({ turn: p.turn, findings: p.findings, reengaged: [...p.reengaged] });
74
+ return {
75
+ stage: IMPLEMENT,
76
+ phase: "start",
77
+ fixTurns: fixTurns.map((t) => ({ ...t, reengaged: [...t.reengaged] })),
78
+ };
79
+ }
80
+ default:
81
+ return null;
82
+ }
83
+ },
84
+ };
85
+ }
86
+ //# sourceMappingURL=fanoutBeats.js.map
@@ -114,6 +114,20 @@ export interface RegisterGoDeps {
114
114
  exists?: (path: string) => boolean;
115
115
  /** Clock seam for registry rows + staleness. */
116
116
  now?: () => number;
117
+ /**
118
+ * Environment the run-wide /go settings are read from (today: `go.fanout` via
119
+ * YAGNI_GO_FANOUT). Defaults to `process.env`; injected so a test can pin a
120
+ * benchmark lane without mutating the process.
121
+ */
122
+ env?: NodeJS.ProcessEnv;
123
+ /**
124
+ * Is /ultra on right now? The same holder the subagent tool reads (wired in
125
+ * index.ts), consulted per call because /ultra can flip mid-session. It is the
126
+ * implement diamond's parallel ceiling: ultra runs the fan (and its fix turns)
127
+ * up to 8 wide, everything else stays at 4. Absent (an unwired embedder or a
128
+ * test) simply means "not ultra", which is today's behavior.
129
+ */
130
+ isUltra?: () => boolean;
117
131
  baseUrl?: string;
118
132
  getToken?: () => string | undefined;
119
133
  /**
@@ -63,6 +63,8 @@ import { existsSync } from "node:fs";
63
63
  import { join } from "node:path";
64
64
  import { eventToLine } from "./activity.js";
65
65
  import { ActivityFeed, SPINNER_FRAMES } from "./activityFeed.js";
66
+ import { resolveFanoutMode } from "./fanout.js";
67
+ import { makeFanoutBeats } from "./fanoutBeats.js";
66
68
  import { RunState } from "./runState.js";
67
69
  import { aggregateRunUsage } from "./budget.js";
68
70
  import { formatRunCostTable } from "./runCostTable.js";
@@ -78,7 +80,7 @@ import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
78
80
  import { composeAbortSignal } from "./resilience.js";
79
81
  import { planResume } from "./resume.js";
80
82
  import { activeRunCount, beginRun, classifyRunLiveness, findActiveRunByTicket, isRunInFlight, isTerminalStatus, lastJournalTs, loadRegistryRows, resolveMaxConcurrentRuns, settleRun, trackRunAbort, trackRunPromise, worktreesDir, } from "./runRegistry.js";
81
- import { makeRunSession as defaultMakeRunSession } from "./runSession.js";
83
+ import { makeRunSession as defaultMakeRunSession, } from "./runSession.js";
82
84
  import { recordSessionRun } from "../sessionRuns.js";
83
85
  import { resolveTicketBrief as defaultResolveTicketBrief } from "./ticketResolution.js";
84
86
  import { snapshotWorkspace as defaultSnapshotWorkspace } from "./workspace.js";
@@ -422,6 +424,13 @@ export function registerGoCommand(pi, deps = {}) {
422
424
  const loadJournal = deps.loadJournal ?? ((sessionKey) => makeFileCheckpointStore(sessionKey).load());
423
425
  const exists = deps.exists ?? existsSync;
424
426
  const now = deps.now ?? Date.now;
427
+ // `go.fanout` (spec decision 8): `always` pins the implement diamond for
428
+ // benchmark / eval lanes; unset leaves the partitioner's conservative verdict
429
+ // as the only thing that decides.
430
+ const fanoutMode = resolveFanoutMode(deps.env ?? process.env);
431
+ // The dial is held, never its value: /ultra can be toggled between runs, so
432
+ // the pipeline calls it when it needs the ceiling rather than reading it here.
433
+ const isUltra = deps.isUltra;
425
434
  // Task 8: no default — see the RegisterGoDeps doc comment for why (only
426
435
  // index.ts's real wiring makes sense here; absent, `resolveCostText` falls
427
436
  // back to the local client estimate).
@@ -559,6 +568,10 @@ export function registerGoCommand(pi, deps = {}) {
559
568
  const feed = ctx.hasUI && !desktop ? new ActivityFeed(ticket) : undefined;
560
569
  const startedAt = Date.now();
561
570
  const run = ctx.hasUI && desktop ? new RunState({ runId: runShortId, ticket, startedAt }) : undefined;
571
+ // The implement diamond's recording thread. Per RUN (never module-scoped):
572
+ // it holds this run's child roster and fix turns. Independent of `feed` /
573
+ // `run`, because the Work page's run surface is fed on headless runs too.
574
+ const fanoutBeats = makeFanoutBeats();
562
575
  const stateKey = `${STATE_KEY_PREFIX}${runShortId}`;
563
576
  let paintTimer;
564
577
  let animationTimer;
@@ -639,6 +652,21 @@ export function registerGoCommand(pi, deps = {}) {
639
652
  baseUrl: deps.baseUrl ?? resolveBaseUrl(),
640
653
  getToken: deps.getToken ?? defaultGetToken,
641
654
  });
655
+ // The diamond's own recording channel is STRICTLY ORDERED, and it is the
656
+ // only one that has to be. Every roster beat carries the WHOLE roster and
657
+ // the run surface derives it last-one-wins (FanoutSection's `readFanout`),
658
+ // so two beats in flight at once can land in either order and a "running"
659
+ // that overtakes its own "done" paints a finished builder as running for
660
+ // the rest of the run. Chaining them through one promise makes the order
661
+ // the surface reads the order the pipeline produced. Still fire-and-forget
662
+ // (nothing on the run's critical path waits on it) and still fail-soft: a
663
+ // rejected beat never breaks the chain. The other stage beats stay
664
+ // unserialized on purpose — each names its own boundary, so their arrival
665
+ // order carries no state a later beat can undo.
666
+ let fanBeatChain = Promise.resolve();
667
+ const recordFanBeat = (b) => {
668
+ fanBeatChain = fanBeatChain.then(() => session.stage(b)).catch(() => { });
669
+ };
642
670
  const repoCtx = await resolveRepoContext(runCwd, ctx.signal);
643
671
  // Durable resilience journal, keyed by the RUN tree (the worktree path for
644
672
  // a default run; ctx.cwd for --here). Detect an interrupted prior /go for
@@ -880,10 +908,19 @@ export function registerGoCommand(pi, deps = {}) {
880
908
  let result = await runPipeline(ticket, {
881
909
  cwd: runCwd,
882
910
  signal: runSignal,
911
+ fanout: fanoutMode,
912
+ ...(isUltra ? { isUltra } : {}),
883
913
  ...(ticketBrief ? { ticketBrief } : {}),
884
914
  onProgress: (p) => {
885
915
  feed?.applyProgress(p);
886
916
  run?.applyProgress(p, Date.now());
917
+ // The implement diamond's own beats (the partition verdict, each
918
+ // child's state, each fix turn) ride the SAME fail-soft stage
919
+ // channel as every other boundary, additively: a signal that is not
920
+ // the diamond's produces no beat at all.
921
+ const fanBeat = fanoutBeats.apply(p);
922
+ if (fanBeat)
923
+ recordFanBeat(fanBeat);
887
924
  // The ribbon replaces the status chip on the desktop, so the chip is
888
925
  // only set for the terminal's status-line fallback.
889
926
  if (ctx.hasUI && !desktop) {