@yagni-app/code 0.3.1 → 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/crashReport.d.ts +12 -0
- package/dist/crashReport.js +28 -1
- package/dist/extension/crashReport.d.ts +18 -0
- package/dist/extension/crashReport.js +35 -2
- 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} +99 -8
- package/dist/extension/{permission.d.ts → permission/gate.d.ts} +10 -3
- package/dist/extension/{permission.js → permission/gate.js} +156 -9
- 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 +31 -3
- package/dist/extension/pipeline/invocation.js +37 -3
- 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 +118 -7
- 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/subagents.js +13 -0
- 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/dist/promptEnrichment.d.ts +1 -1
- package/dist/promptEnrichment.js +1 -1
- 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
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* - reviewer (business-fit lens): call `review_business_match` and treat a
|
|
10
10
|
* conflict with a recorded decision as at least High.
|
|
11
11
|
*
|
|
12
|
+
* The implement diamond adds two more roles: `orchestrator` (the read-only
|
|
13
|
+
* partitioner, carrying the ```partition output contract `parsePartition` reads)
|
|
14
|
+
* and `synthesizer` (the seam-reconciler that writes the review handoff). Its
|
|
15
|
+
* parallel builders reuse the `worker` body plus a per-workstream clause from
|
|
16
|
+
* {@link builderPersonaClause}.
|
|
17
|
+
*
|
|
12
18
|
* The reviewer also carries the strict, parseable findings output contract so
|
|
13
19
|
* the `{previous}` handoff into the fix stage parses (see `findings.ts`).
|
|
14
20
|
*
|
|
@@ -95,6 +101,66 @@ The reasoning, grounded in what you actually read. Cite files and lines.
|
|
|
95
101
|
The specific evidence that would flip this. If you were not able to verify something load-bearing, say so here rather than hedging the recommendation.
|
|
96
102
|
|
|
97
103
|
Be terse and decisive. The caller is mid-task and paying peak rates for your turn.`;
|
|
104
|
+
const ORCHESTRATOR_INTRO = `You are a build orchestrator. You receive an approved implementation plan and decide ONE thing: whether the build can safely be split into parallel workstreams with disjoint file sets, or must run as a single writer. You do not implement anything.`;
|
|
105
|
+
const ORCHESTRATOR_GROUNDING = `You are grounded. Call ask_yagni before inferring a convention, an ownership boundary, or how this company splits work; a partition that cuts across a boundary this company treats as one surface is a bad partition even when the file sets look clean.`;
|
|
106
|
+
const ORCHESTRATOR_RULES = `You are READ-ONLY. You have read, grep, find, ls and read-only bash. Do not write, edit, build, or run anything that changes the tree: the builders you partition for do the work.
|
|
107
|
+
|
|
108
|
+
Your bias is CONSERVATIVE. Parallel writers earn their width; they do not get it by default. Fan ONLY when the plan splits into obviously clean surfaces whose file sets do not touch. When in doubt, when the plan is one cluster of coupled edits, when the repo context you were given is thin, or when you cannot name each workstream's files with confidence, return single-writer with the reason. A single-writer verdict is a good answer, not a failure; a guessed partition is the one outcome that is worse than running serially.
|
|
109
|
+
|
|
110
|
+
How to judge:
|
|
111
|
+
1. Read the plan's own "Files to modify / create" list first; it is the strongest evidence you have.
|
|
112
|
+
2. Group the work by surface (package, layer, directory), not by plan step ordering.
|
|
113
|
+
3. Check the groups for shared files, shared types, and edits that must land together. Any of those means fewer workstreams, or one.
|
|
114
|
+
4. Give each workstream a tier: "efficient" for mechanical work (test fixture updates, renames, copy sweeps, mechanical follow-through), "standard" for load-bearing work that carries judgment. This is cost fitting, not perspective: never split the same work across two workstreams.`;
|
|
115
|
+
/**
|
|
116
|
+
* The machine-readable partition contract the orchestrator must emit, kept in
|
|
117
|
+
* lockstep with `parsePartition` (`fanout.ts`). Separate from the body so the
|
|
118
|
+
* contract text has one home and the tests can read it directly.
|
|
119
|
+
*/
|
|
120
|
+
const PARTITION_CONTRACT = `=== Partition output contract (required) ===
|
|
121
|
+
End your answer with a fenced block in EXACTLY this form and nothing after it:
|
|
122
|
+
|
|
123
|
+
\`\`\`partition
|
|
124
|
+
{
|
|
125
|
+
"mode": "fan",
|
|
126
|
+
"width": 2,
|
|
127
|
+
"reason": "why this width, in one sentence",
|
|
128
|
+
"workstreams": [
|
|
129
|
+
{ "name": "backend-routes", "task": "what this builder does", "files": ["packages/backend/src/routes"], "tier": "standard" }
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
\`\`\`
|
|
133
|
+
|
|
134
|
+
Rules the parser enforces, so getting them wrong wastes the run:
|
|
135
|
+
- "mode" is "fan" or "single". A single-writer verdict is just {"mode": "single", "reason": "..."} with no width and no workstreams.
|
|
136
|
+
- "width" is 2, 4 or 8, and equals the number of workstreams.
|
|
137
|
+
- "reason" is always required, on both modes.
|
|
138
|
+
- Every workstream needs a name, a task, and at least one file claim.
|
|
139
|
+
- A file claim is a path PREFIX from the repo root: a directory claims its whole subtree. Claim the DIRECTORIES of any files a workstream will create, so a planned new file is not an out-of-claim edit.
|
|
140
|
+
- Claims must be pairwise DISJOINT across workstreams, nesting included: claiming a directory in one workstream and a file inside it in another is an overlap and fails the run.
|
|
141
|
+
- "tier" is "standard" (default) or "efficient".`;
|
|
142
|
+
const SYNTHESIZER_INTRO = `You are a synthesizer. Several builders just worked in parallel in this ONE shared working tree, each pinned to its own files. Their diffs are already in place: you are not applying patches, you are reconciling the seams between them and writing the single handoff the reviewers will read.`;
|
|
143
|
+
const SYNTHESIZER_GROUNDING = `You are grounded. Call ask_yagni before guessing about anything organization- or codebase-specific, and call record_decision for any product-intent call you are forced to make while reconciling.`;
|
|
144
|
+
const SYNTHESIZER_RULES = `What to reconcile:
|
|
145
|
+
1. Seams between workstreams: imports that do not line up, a shared type each builder assumed differently, duplicated helpers, a call site left on the old signature.
|
|
146
|
+
2. The checker's findings, which are deterministic and name real files.
|
|
147
|
+
3. Any out-of-claim edits reported to you, plus anything a builder flagged as needing a change outside its claims.
|
|
148
|
+
|
|
149
|
+
Make the minimal edits that make the union coherent. Do not re-litigate a builder's approach, do not refactor beyond the seams, and do not run git add, git commit, or git stash; committing is the finish stage's job.
|
|
150
|
+
|
|
151
|
+
Output:
|
|
152
|
+
## Completed
|
|
153
|
+
The change as ONE piece of work, not a list of builders.
|
|
154
|
+
## Files changed
|
|
155
|
+
- \`path\` - what changed
|
|
156
|
+
## Seams reconciled
|
|
157
|
+
What did not line up between workstreams, and what you did about it.
|
|
158
|
+
## Open findings
|
|
159
|
+
Anything the checker or a builder reported that is still not fixed, named honestly so the reviewers can pick it up. Say "none" when there are none.
|
|
160
|
+
## Notes
|
|
161
|
+
Anything the reviewer should know (exact file paths, key functions touched).`;
|
|
162
|
+
const ORCHESTRATOR_BODY = [ORCHESTRATOR_INTRO, ORCHESTRATOR_GROUNDING, ORCHESTRATOR_RULES].join("\n\n");
|
|
163
|
+
const SYNTHESIZER_BODY = [SYNTHESIZER_INTRO, SYNTHESIZER_GROUNDING, SYNTHESIZER_RULES].join("\n\n");
|
|
98
164
|
const GUARDIAN_BODY = `You are a safety reviewer. You review shell commands before they run and decide one of three outcomes: allow, ask, or deny. You may read files for context (a script the command runs, a config it touches) but you never write or execute anything. Your final output is ONLY a JSON verdict — no prose around it.
|
|
99
165
|
|
|
100
166
|
Risk taxonomy:
|
|
@@ -107,7 +173,7 @@ Risk taxonomy:
|
|
|
107
173
|
Outcome rules:
|
|
108
174
|
- "deny" is reserved for actions you are VERY CONFIDENT are very dangerous: irreversible destruction, exfiltration of sensitive data, credential probing, production-impacting security weakening. If you are not sure it is that bad, do not deny.
|
|
109
175
|
- "allow" when you are confident the action is safe: read-only, routine dev-loop, or clearly scoped and reversible.
|
|
110
|
-
- "ask" for
|
|
176
|
+
- "ask" for actions where a specific, concrete risk makes you unsure — the action has a plausible path to harm that you cannot rule out by reading the command. Do not ask just because a command "might" do something; ask when you can name the specific thing you are worried about. If you cannot articulate a concrete risk, allow. Prefer allow over ask: routine dev-loop operations, local file writes within a project, and standard git operations (add, commit, fetch) are safe — the agent is already operating in the user's repository with their consent.
|
|
111
177
|
|
|
112
178
|
Rationale rules:
|
|
113
179
|
- For "ask", the rationale MUST be a specific question addressed to the user, naming the concrete effect that made you unsure — e.g. "This pushes 3 commits to the shared main branch — do you want to publish them now?". You may be told why the static policy routed the command to you; your rationale must ADD information beyond that policy text, not restate it.
|
|
@@ -126,6 +192,8 @@ export const PERSONA_BODIES = {
|
|
|
126
192
|
reviewer: REVIEWER_BODY,
|
|
127
193
|
advisor: ADVISOR_BODY,
|
|
128
194
|
guardian: GUARDIAN_BODY,
|
|
195
|
+
orchestrator: [ORCHESTRATOR_BODY, PARTITION_CONTRACT].join("\n\n"),
|
|
196
|
+
synthesizer: SYNTHESIZER_BODY,
|
|
129
197
|
};
|
|
130
198
|
/**
|
|
131
199
|
* Grounding-FREE persona bodies for the M6 grounded-vs-blind eval ONLY. These are
|
|
@@ -217,6 +285,11 @@ export const BLIND_PERSONA_BODIES = {
|
|
|
217
285
|
reviewer: REVIEWER_BLIND,
|
|
218
286
|
advisor: ADVISOR_BLIND,
|
|
219
287
|
guardian: GUARDIAN_BODY,
|
|
288
|
+
// The diamond's own agents: same roles with the grounding paragraph dropped.
|
|
289
|
+
// No eval lane fans implement today, but `personaBody` throws on an unknown
|
|
290
|
+
// agent, so a missing twin would be a crash rather than a degraded measurement.
|
|
291
|
+
orchestrator: [ORCHESTRATOR_INTRO, ORCHESTRATOR_RULES, PARTITION_CONTRACT].join("\n\n"),
|
|
292
|
+
synthesizer: [SYNTHESIZER_INTRO, SYNTHESIZER_RULES].join("\n\n"),
|
|
220
293
|
};
|
|
221
294
|
/** The lens-specific clause appended to the reviewer body, one per review angle. */
|
|
222
295
|
const LENS_CLAUSES = {
|
|
@@ -249,10 +322,45 @@ const BLIND_LENS_CLAUSES = {
|
|
|
249
322
|
business_fit: "Lens: BUSINESS-FIT. Does this change match the apparent product intent and the conventions visible in the code? Right code doing the wrong thing is exactly the failure you exist to catch. A clear mismatch is at least High.",
|
|
250
323
|
does_it_hold: LENS_CLAUSES.does_it_hold,
|
|
251
324
|
};
|
|
325
|
+
/**
|
|
326
|
+
* The shared-worktree discipline every fan-out builder runs under. All builders
|
|
327
|
+
* edit the SAME tree concurrently (there are no per-child worktrees), so the
|
|
328
|
+
* rules that keep them out of each other's way are prompt discipline: stay
|
|
329
|
+
* inside your claims, never touch the git index (parallel index writes race on
|
|
330
|
+
* index.lock, and committing is FINISH's job anyway), and leave test execution
|
|
331
|
+
* to the deterministic checker that runs on the merged tree.
|
|
332
|
+
*/
|
|
333
|
+
const SHARED_TREE_RULES = `Shared working tree rules:
|
|
334
|
+
- Never run a git index operation: no git add, no git commit, no git stash, no git checkout, no git reset. Other builders may be writing this same tree right now, and committing is the finish stage's job.
|
|
335
|
+
- Do not run the repo's full test suite. A deterministic checker runs typecheck and the repo's own tests on the merged result after the fan; your job is the code.`;
|
|
336
|
+
/**
|
|
337
|
+
* The per-workstream clause appended to a fan-out builder's (or fixer's) persona:
|
|
338
|
+
* the files it owns, what its siblings own, and the shared-tree discipline. With
|
|
339
|
+
* no workstream (the single-writer path, including its fix turns) only the
|
|
340
|
+
* shared-tree rules apply, since there is nothing to pin against.
|
|
341
|
+
*/
|
|
342
|
+
export function builderPersonaClause(workstream, siblings = []) {
|
|
343
|
+
if (!workstream)
|
|
344
|
+
return SHARED_TREE_RULES;
|
|
345
|
+
const parts = [
|
|
346
|
+
`You are building ONE workstream of a plan that was split across parallel builders: "${workstream.name}".`,
|
|
347
|
+
["Files you own (path prefixes; a directory covers its whole subtree):", ...workstream.files.map((f) => `- ${f}`)].join("\n"),
|
|
348
|
+
"Create new files only inside the paths you own. Edit nothing outside them.",
|
|
349
|
+
];
|
|
350
|
+
if (siblings.length > 0) {
|
|
351
|
+
parts.push([
|
|
352
|
+
"Other builders are working in this same tree right now. Their files, so you know where the seams are (read them if you need to, never edit them):",
|
|
353
|
+
...siblings.map((s) => `- ${s.name}: ${s.files.join(", ")}`),
|
|
354
|
+
].join("\n"));
|
|
355
|
+
}
|
|
356
|
+
parts.push("If your workstream genuinely needs a change outside the paths you own, do NOT make it. Name the file and the change in your Notes so the synthesizer reconciles that seam.", SHARED_TREE_RULES);
|
|
357
|
+
return parts.join("\n\n");
|
|
358
|
+
}
|
|
252
359
|
/**
|
|
253
360
|
* Resolve the system-prompt body for a stage. For non-review stages this is the
|
|
254
361
|
* role body; for the review stage it appends the lens-specific clause and the
|
|
255
|
-
* required findings output contract so the reviewer's output parses.
|
|
362
|
+
* required findings output contract so the reviewer's output parses. A stage
|
|
363
|
+
* carrying a `personaClause` (the fan-out builders) gets it appended last.
|
|
256
364
|
*
|
|
257
365
|
* `grounded` defaults true (every real /go stage). The M6 eval passes
|
|
258
366
|
* `grounded: false` to select the grounding-free bodies + lens clauses for the
|
|
@@ -264,10 +372,13 @@ export function personaBody(stage, opts = {}) {
|
|
|
264
372
|
const base = bodies[stage.agent];
|
|
265
373
|
if (!base)
|
|
266
374
|
throw new Error(`No persona body for agent "${stage.agent}"`);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
375
|
+
const parts = [base];
|
|
376
|
+
if (stage.id === "review") {
|
|
377
|
+
const lens = stage.lens ?? "correctness";
|
|
378
|
+
parts.push((grounded ? LENS_CLAUSES : BLIND_LENS_CLAUSES)[lens], FINDINGS_CONTRACT);
|
|
379
|
+
}
|
|
380
|
+
if (stage.personaClause?.trim())
|
|
381
|
+
parts.push(stage.personaClause.trim());
|
|
382
|
+
return parts.join("\n\n");
|
|
272
383
|
}
|
|
273
384
|
//# sourceMappingURL=personas.js.map
|
|
@@ -89,12 +89,56 @@ export interface StartArgs {
|
|
|
89
89
|
repo?: string;
|
|
90
90
|
branch?: string;
|
|
91
91
|
}
|
|
92
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* The implement diamond's partition verdict as the run records it: the fan and
|
|
94
|
+
* its width, or the single-writer degrade, always with the reason that produced
|
|
95
|
+
* it. Rides one implement stage beat (see {@link StageArgs}).
|
|
96
|
+
*/
|
|
97
|
+
export interface StageFanoutDecision {
|
|
98
|
+
mode: "fan" | "single";
|
|
99
|
+
width?: number;
|
|
100
|
+
reason: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* One fan-out child on a stage beat: the workstream's name, the tier it runs on,
|
|
104
|
+
* how many paths it claimed, and the state the run has actually OBSERVED. There
|
|
105
|
+
* is no "pending" state on purpose: a child appears the moment it starts, so a
|
|
106
|
+
* surface can only ever paint children that really ran.
|
|
107
|
+
*/
|
|
108
|
+
export interface StageChildRow {
|
|
109
|
+
name: string;
|
|
110
|
+
tier?: "standard" | "efficient";
|
|
111
|
+
state: "running" | "done" | "failed";
|
|
112
|
+
/** How many path claims the partition gave this workstream; absent when unrecorded. */
|
|
113
|
+
files?: number;
|
|
114
|
+
note?: string;
|
|
115
|
+
}
|
|
116
|
+
/** One bounded fix turn: which findings it answered and which children it re-engaged. */
|
|
117
|
+
export interface StageFixTurn {
|
|
118
|
+
turn: number;
|
|
119
|
+
findings: number;
|
|
120
|
+
reengaged: string[];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A pipeline stage boundary event.
|
|
124
|
+
*
|
|
125
|
+
* `fanout` / `children` / `fixTurns` are the implement diamond's additive
|
|
126
|
+
* payloads (spec "Recording and the run surface"). They ride INTERIM implement
|
|
127
|
+
* beats and are append-only: an older CLI simply never sends them, and a backend
|
|
128
|
+
* that does not know them ignores them, so the contract stays backwards
|
|
129
|
+
* compatible in both directions. Every beat carries at most one of them.
|
|
130
|
+
*/
|
|
93
131
|
export interface StageArgs {
|
|
94
132
|
stage: string;
|
|
95
133
|
phase: "start" | "finish";
|
|
96
134
|
round?: number;
|
|
97
135
|
findings?: number;
|
|
136
|
+
/** The diamond's verdict, sent once per implement stage the moment it resolves. */
|
|
137
|
+
fanout?: StageFanoutDecision;
|
|
138
|
+
/** The whole per-workstream roster, re-sent on every child state change. */
|
|
139
|
+
children?: StageChildRow[];
|
|
140
|
+
/** Every fix turn recorded so far, re-sent as each new one lands. */
|
|
141
|
+
fixTurns?: StageFixTurn[];
|
|
98
142
|
}
|
|
99
143
|
/** An in-loop correction captured during the run. */
|
|
100
144
|
export interface CorrectionArgs {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* the panel's cross-lens interleaving is preserved by code that never moved.
|
|
24
24
|
*/
|
|
25
25
|
import type { ActivityLine, FeedStageId, JsonEvent, PipelineProgress, ReviewLens, StageTag, StopReason } from "./types.js";
|
|
26
|
+
import type { PartitionWidth } from "./fanout.js";
|
|
26
27
|
/** The fixed stage order shown by both surfaces (v1 stages + the post-loop FINISH row). */
|
|
27
28
|
export declare const STAGE_ORDER: FeedStageId[];
|
|
28
29
|
/**
|
|
@@ -48,11 +49,18 @@ export interface AgentUsage {
|
|
|
48
49
|
export interface AgentNode {
|
|
49
50
|
/**
|
|
50
51
|
* Stable across paints: the stage id, `review:<lens>:r<round>` for a lens
|
|
51
|
-
* child, or `fix:r<round>` for
|
|
52
|
+
* child, `implement:<workstream>` for a fan-out builder, or `fix:r<round>` for
|
|
53
|
+
* a per-round fix execution.
|
|
52
54
|
*/
|
|
53
55
|
id: string;
|
|
54
56
|
stageId: FeedStageId;
|
|
55
57
|
lens?: ReviewLens;
|
|
58
|
+
/**
|
|
59
|
+
* Which fan-out child of the implement diamond this node is (the direct
|
|
60
|
+
* analogue of `lens`). A re-engaged builder keeps its node across fix turns, so
|
|
61
|
+
* one workstream is one row for the whole stage.
|
|
62
|
+
*/
|
|
63
|
+
workstream?: string;
|
|
56
64
|
round?: number;
|
|
57
65
|
status: AgentStatus;
|
|
58
66
|
summary: string;
|
|
@@ -80,6 +88,18 @@ export interface RoundTally {
|
|
|
80
88
|
* no pids, no absolute paths — so it stays portable to a non-local executor at
|
|
81
89
|
* zero cost. The tab does not need them.
|
|
82
90
|
*/
|
|
91
|
+
/** The implement diamond's recorded verdict, as the desktop reads it. */
|
|
92
|
+
export interface FanoutRecord {
|
|
93
|
+
mode: "fan" | "single";
|
|
94
|
+
width?: PartitionWidth;
|
|
95
|
+
reason: string;
|
|
96
|
+
}
|
|
97
|
+
/** One recorded fix turn of the implement diamond. */
|
|
98
|
+
export interface FixTurnRecord {
|
|
99
|
+
turn: number;
|
|
100
|
+
findings: number;
|
|
101
|
+
reengaged: string[];
|
|
102
|
+
}
|
|
83
103
|
export interface GoRunState {
|
|
84
104
|
v: 1;
|
|
85
105
|
runId: string;
|
|
@@ -103,16 +123,26 @@ export interface GoRunState {
|
|
|
103
123
|
* `UNTRACKED_NOTICE` in goCommand.ts): there is no server id to give it.
|
|
104
124
|
*/
|
|
105
125
|
serverRunId?: string;
|
|
126
|
+
/**
|
|
127
|
+
* How the implement stage resolved to build (the diamond's partition verdict,
|
|
128
|
+
* or its recorded single-writer degrade). Absent on a run whose implement stage
|
|
129
|
+
* never reported one, so an older run reads exactly as it did before.
|
|
130
|
+
*/
|
|
131
|
+
fanout?: FanoutRecord;
|
|
132
|
+
/** The diamond's bounded fix turns, in order. Absent when none ran. */
|
|
133
|
+
fixTurns?: FixTurnRecord[];
|
|
106
134
|
}
|
|
107
135
|
/**
|
|
108
|
-
* Stable agent id: a lens child is keyed by its lens AND round,
|
|
109
|
-
* only lens-less stage that re-executes per
|
|
110
|
-
* round's fix keeps its own clock and usage instead
|
|
111
|
-
* timer that resets (the "181k tok in 59s" artifact).
|
|
112
|
-
* stay on the bare `review` node: they mark the round
|
|
113
|
-
* children carry the round's real work.
|
|
136
|
+
* Stable agent id: a lens child is keyed by its lens AND round, a fan-out builder
|
|
137
|
+
* by its workstream, and fix — the only lens-less stage that re-executes per
|
|
138
|
+
* round — by its round, so each round's fix keeps its own clock and usage instead
|
|
139
|
+
* of accumulating tokens on a timer that resets (the "181k tok in 59s" artifact).
|
|
140
|
+
* Review stage-level events stay on the bare `review` node: they mark the round
|
|
141
|
+
* boundary, and the lens children carry the round's real work. The implement
|
|
142
|
+
* diamond behaves the same way: the bare `implement` node is the stage boundary,
|
|
143
|
+
* and the workstream children carry the parallel build.
|
|
114
144
|
*/
|
|
115
|
-
export declare function agentId(stageId: FeedStageId, lens?: ReviewLens, round?: number): string;
|
|
145
|
+
export declare function agentId(stageId: FeedStageId, lens?: ReviewLens, round?: number, workstream?: string): string;
|
|
116
146
|
export interface RunStateInit {
|
|
117
147
|
runId: string;
|
|
118
148
|
ticket: string;
|
|
@@ -130,14 +160,18 @@ export declare class RunState {
|
|
|
130
160
|
private findingsBlocking;
|
|
131
161
|
private readonly rounds;
|
|
132
162
|
private readonly agents;
|
|
163
|
+
private fanout;
|
|
164
|
+
private readonly fixTurns;
|
|
165
|
+
/** Width the partition asked for; the children themselves are the honest denominator. */
|
|
166
|
+
private fanoutWidth;
|
|
133
167
|
constructor(init: RunStateInit);
|
|
134
168
|
/** The lens-less stage agent, or undefined for an unknown stage. */
|
|
135
169
|
private stageAgent;
|
|
136
170
|
/**
|
|
137
|
-
* The agent for a tag, created on demand. Lens children are
|
|
138
|
-
* after their parent stage and any existing siblings, so the
|
|
139
|
-
* reads top-to-bottom for the desktop and the
|
|
140
|
-
* STAGE_ORDER for the terminal.
|
|
171
|
+
* The agent for a tag, created on demand. Lens and workstream children are
|
|
172
|
+
* inserted directly after their parent stage and any existing siblings, so the
|
|
173
|
+
* array order still reads top-to-bottom for the desktop and the child-less
|
|
174
|
+
* filter still yields STAGE_ORDER for the terminal.
|
|
141
175
|
*/
|
|
142
176
|
private ensureAgent;
|
|
143
177
|
/** Fold a structured progress signal. `at` is only read by the desktop surface. */
|
|
@@ -165,6 +199,17 @@ export declare class RunState {
|
|
|
165
199
|
* not a stale bare placeholder.
|
|
166
200
|
*/
|
|
167
201
|
stageAgents(): AgentNode[];
|
|
202
|
+
/**
|
|
203
|
+
* The implement diamond's compact row summary for the terminal: how the stage
|
|
204
|
+
* resolved to build, how many children it fanned into, and how many of them
|
|
205
|
+
* have finished (a failed child is finished too). Undefined until the stage
|
|
206
|
+
* reports a verdict, so a run that never fanned renders exactly as before.
|
|
207
|
+
*/
|
|
208
|
+
fanoutRow(): {
|
|
209
|
+
mode: "fan" | "single";
|
|
210
|
+
total: number;
|
|
211
|
+
done: number;
|
|
212
|
+
} | undefined;
|
|
168
213
|
header(): {
|
|
169
214
|
ticket: string;
|
|
170
215
|
round: number;
|
|
@@ -34,16 +34,20 @@ function emptyUsage() {
|
|
|
34
34
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Stable agent id: a lens child is keyed by its lens AND round,
|
|
38
|
-
* only lens-less stage that re-executes per
|
|
39
|
-
* round's fix keeps its own clock and usage instead
|
|
40
|
-
* timer that resets (the "181k tok in 59s" artifact).
|
|
41
|
-
* stay on the bare `review` node: they mark the round
|
|
42
|
-
* children carry the round's real work.
|
|
37
|
+
* Stable agent id: a lens child is keyed by its lens AND round, a fan-out builder
|
|
38
|
+
* by its workstream, and fix — the only lens-less stage that re-executes per
|
|
39
|
+
* round — by its round, so each round's fix keeps its own clock and usage instead
|
|
40
|
+
* of accumulating tokens on a timer that resets (the "181k tok in 59s" artifact).
|
|
41
|
+
* Review stage-level events stay on the bare `review` node: they mark the round
|
|
42
|
+
* boundary, and the lens children carry the round's real work. The implement
|
|
43
|
+
* diamond behaves the same way: the bare `implement` node is the stage boundary,
|
|
44
|
+
* and the workstream children carry the parallel build.
|
|
43
45
|
*/
|
|
44
|
-
export function agentId(stageId, lens, round) {
|
|
46
|
+
export function agentId(stageId, lens, round, workstream) {
|
|
45
47
|
if (lens)
|
|
46
48
|
return `${stageId}:${lens}:r${round ?? 1}`;
|
|
49
|
+
if (workstream)
|
|
50
|
+
return `${stageId}:${workstream}`;
|
|
47
51
|
if (stageId === "fix" && round !== undefined)
|
|
48
52
|
return `fix:r${round}`;
|
|
49
53
|
return stageId;
|
|
@@ -60,6 +64,10 @@ export class RunState {
|
|
|
60
64
|
findingsBlocking = 0;
|
|
61
65
|
rounds = [];
|
|
62
66
|
agents;
|
|
67
|
+
fanout;
|
|
68
|
+
fixTurns = [];
|
|
69
|
+
/** Width the partition asked for; the children themselves are the honest denominator. */
|
|
70
|
+
fanoutWidth;
|
|
63
71
|
constructor(init) {
|
|
64
72
|
this.runId = init.runId;
|
|
65
73
|
this.ticket = init.ticket;
|
|
@@ -78,13 +86,14 @@ export class RunState {
|
|
|
78
86
|
return this.agents.find((a) => a.id === stageId);
|
|
79
87
|
}
|
|
80
88
|
/**
|
|
81
|
-
* The agent for a tag, created on demand. Lens children are
|
|
82
|
-
* after their parent stage and any existing siblings, so the
|
|
83
|
-
* reads top-to-bottom for the desktop and the
|
|
84
|
-
* STAGE_ORDER for the terminal.
|
|
89
|
+
* The agent for a tag, created on demand. Lens and workstream children are
|
|
90
|
+
* inserted directly after their parent stage and any existing siblings, so the
|
|
91
|
+
* array order still reads top-to-bottom for the desktop and the child-less
|
|
92
|
+
* filter still yields STAGE_ORDER for the terminal.
|
|
85
93
|
*/
|
|
86
|
-
ensureAgent(
|
|
87
|
-
const
|
|
94
|
+
ensureAgent(tag, at) {
|
|
95
|
+
const { stageId, lens, round, workstream } = tag;
|
|
96
|
+
const id = agentId(stageId, lens, round, workstream);
|
|
88
97
|
const found = this.agents.find((a) => a.id === id);
|
|
89
98
|
if (found)
|
|
90
99
|
return found;
|
|
@@ -92,6 +101,7 @@ export class RunState {
|
|
|
92
101
|
id,
|
|
93
102
|
stageId,
|
|
94
103
|
...(lens ? { lens } : {}),
|
|
104
|
+
...(workstream ? { workstream } : {}),
|
|
95
105
|
...(round !== undefined ? { round } : {}),
|
|
96
106
|
status: "active",
|
|
97
107
|
summary: "",
|
|
@@ -122,7 +132,7 @@ export class RunState {
|
|
|
122
132
|
// own child; a bare stage signal (including review's round boundary,
|
|
123
133
|
// which must return to active even though a prior round left it done)
|
|
124
134
|
// lands on the pre-seeded stage node.
|
|
125
|
-
const target = this.ensureAgent(p
|
|
135
|
+
const target = this.ensureAgent(p, at);
|
|
126
136
|
target.status = "active";
|
|
127
137
|
target.startedAt = at;
|
|
128
138
|
// `delete`, not `= undefined`: an explicitly-undefined property still
|
|
@@ -133,7 +143,7 @@ export class RunState {
|
|
|
133
143
|
case "stage_done": {
|
|
134
144
|
if (typeof p.round === "number")
|
|
135
145
|
this.round = p.round;
|
|
136
|
-
const target = this.ensureAgent(p
|
|
146
|
+
const target = this.ensureAgent(p, at);
|
|
137
147
|
if (p.degraded === "failed") {
|
|
138
148
|
target.status = "failed";
|
|
139
149
|
}
|
|
@@ -149,6 +159,19 @@ export class RunState {
|
|
|
149
159
|
target.degraded = p.degraded;
|
|
150
160
|
break;
|
|
151
161
|
}
|
|
162
|
+
case "fanout": {
|
|
163
|
+
this.fanout = {
|
|
164
|
+
mode: p.mode,
|
|
165
|
+
...(p.width ? { width: p.width } : {}),
|
|
166
|
+
reason: p.reason,
|
|
167
|
+
};
|
|
168
|
+
this.fanoutWidth = p.width ?? p.workstreams?.length;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case "fix_turn": {
|
|
172
|
+
this.fixTurns.push({ turn: p.turn, findings: p.findings, reengaged: [...p.reengaged] });
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
152
175
|
case "findings": {
|
|
153
176
|
this.round = p.round;
|
|
154
177
|
this.findingsTotal = p.total;
|
|
@@ -181,7 +204,7 @@ export class RunState {
|
|
|
181
204
|
* actions ride BOTH the agent's own list and the terminal's flat ring buffer.
|
|
182
205
|
*/
|
|
183
206
|
applyLine(line, tag, at = 0) {
|
|
184
|
-
const target = this.ensureAgent(tag
|
|
207
|
+
const target = this.ensureAgent(tag, at);
|
|
185
208
|
if (line.kind === "narration") {
|
|
186
209
|
target.summary = line.text;
|
|
187
210
|
return;
|
|
@@ -207,7 +230,7 @@ export class RunState {
|
|
|
207
230
|
applyUsage(ev, tag, at = 0) {
|
|
208
231
|
if (ev.type !== "message_end" || ev.message?.role !== "assistant")
|
|
209
232
|
return;
|
|
210
|
-
const target = this.ensureAgent(tag
|
|
233
|
+
const target = this.ensureAgent(tag, at);
|
|
211
234
|
target.usage.turns += 1;
|
|
212
235
|
const u = ev.message.usage;
|
|
213
236
|
if (!u)
|
|
@@ -240,11 +263,28 @@ export class RunState {
|
|
|
240
263
|
*/
|
|
241
264
|
stageAgents() {
|
|
242
265
|
return STAGE_ORDER.flatMap((id) => {
|
|
243
|
-
const perRound = this.agents.filter((a) => a.stageId === id && a.lens === undefined && a.id !== id);
|
|
266
|
+
const perRound = this.agents.filter((a) => a.stageId === id && a.lens === undefined && a.workstream === undefined && a.id !== id);
|
|
244
267
|
const row = perRound[perRound.length - 1] ?? this.stageAgent(id);
|
|
245
268
|
return row ? [row] : [];
|
|
246
269
|
});
|
|
247
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* The implement diamond's compact row summary for the terminal: how the stage
|
|
273
|
+
* resolved to build, how many children it fanned into, and how many of them
|
|
274
|
+
* have finished (a failed child is finished too). Undefined until the stage
|
|
275
|
+
* reports a verdict, so a run that never fanned renders exactly as before.
|
|
276
|
+
*/
|
|
277
|
+
fanoutRow() {
|
|
278
|
+
if (!this.fanout)
|
|
279
|
+
return undefined;
|
|
280
|
+
const children = this.agents.filter((a) => a.workstream !== undefined);
|
|
281
|
+
const done = children.filter((a) => a.status === "done" || a.status === "failed").length;
|
|
282
|
+
return {
|
|
283
|
+
mode: this.fanout.mode,
|
|
284
|
+
total: this.fanoutWidth ?? children.length,
|
|
285
|
+
done,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
248
288
|
header() {
|
|
249
289
|
return {
|
|
250
290
|
ticket: this.ticket,
|
|
@@ -269,6 +309,8 @@ export class RunState {
|
|
|
269
309
|
...(this.stopReason ? { stopReason: this.stopReason } : {}),
|
|
270
310
|
agents: this.agents,
|
|
271
311
|
...(this.serverRunId !== undefined ? { serverRunId: this.serverRunId } : {}),
|
|
312
|
+
...(this.fanout ? { fanout: this.fanout } : {}),
|
|
313
|
+
...(this.fixTurns.length > 0 ? { fixTurns: this.fixTurns } : {}),
|
|
272
314
|
};
|
|
273
315
|
}
|
|
274
316
|
}
|
|
@@ -27,6 +27,7 @@ import { trackChild } from "./childRegistry.js";
|
|
|
27
27
|
import { finalOutputFrom, foldEvent, newEventAccumulator } from "./events.js";
|
|
28
28
|
import { buildStageInvocation, groundedChildArgv } from "./invocation.js";
|
|
29
29
|
import { personaBody } from "./personas.js";
|
|
30
|
+
import { clampTier, resolveTierCap } from "./tierCap.js";
|
|
30
31
|
import { PER_TASK_OUTPUT_CAP, } from "./types.js";
|
|
31
32
|
/**
|
|
32
33
|
* Default per-line stdout ceiling (16 MiB). A legitimate NDJSON event — even one
|
|
@@ -85,6 +86,12 @@ export async function runStage(stage, ctx, deps) {
|
|
|
85
86
|
const spawnFn = deps.spawn ?? nodeSpawn;
|
|
86
87
|
const writePrompt = deps.writePrompt ?? defaultWritePrompt;
|
|
87
88
|
const resolveChild = deps.resolveChild ?? defaultResolveChild;
|
|
89
|
+
// The run's tier ceiling (`YAGNI_GO_TIER_CAP`), resolved ONCE here — the
|
|
90
|
+
// single seam every stage, review lens, format re-ask and subagent child
|
|
91
|
+
// passes through. Absent on a normal run, so tiers stay the pipeline's own.
|
|
92
|
+
const envForCap = deps.env ?? process.env;
|
|
93
|
+
const tierCap = resolveTierCap(envForCap);
|
|
94
|
+
const effectiveTier = clampTier(stage.model, tierCap);
|
|
88
95
|
const personaStage = ctx.lens ? { ...stage, lens: ctx.lens } : stage;
|
|
89
96
|
// grounded defaults true; only the M6 blind eval lane passes grounded: false.
|
|
90
97
|
const body = (deps.personaBody ?? personaBody)(personaStage, { grounded: ctx.grounded !== false });
|
|
@@ -92,6 +99,7 @@ export async function runStage(stage, ctx, deps) {
|
|
|
92
99
|
const result = {
|
|
93
100
|
stageId: stage.id,
|
|
94
101
|
agent: stage.agent,
|
|
102
|
+
tier: effectiveTier,
|
|
95
103
|
exitCode: 0,
|
|
96
104
|
finalOutput: "",
|
|
97
105
|
usage: { ...EMPTY_USAGE },
|
|
@@ -105,6 +113,7 @@ export async function runStage(stage, ctx, deps) {
|
|
|
105
113
|
previous: ctx.previous,
|
|
106
114
|
promptFilePath: filePath,
|
|
107
115
|
lens: ctx.lens,
|
|
116
|
+
...(tierCap ? { tierCap } : {}),
|
|
108
117
|
});
|
|
109
118
|
const { piCli, extensionPath } = resolveChild();
|
|
110
119
|
const argv = groundedChildArgv(passthrough, { piCli, extensionPath });
|
|
@@ -141,7 +150,7 @@ export async function runStage(stage, ctx, deps) {
|
|
|
141
150
|
// to the session id alone.
|
|
142
151
|
const callerLabel = deps.callerLabel ?? (ctx.lens ? `go:${stage.id}:${ctx.lens}` : `go:${stage.id}`);
|
|
143
152
|
const childEnv = {
|
|
144
|
-
...
|
|
153
|
+
...envForCap,
|
|
145
154
|
YAGNI_CALLER: callerLabel,
|
|
146
155
|
...(deps.attribution?.runId ? { YAGNI_RUN_ID: deps.attribution.runId } : {}),
|
|
147
156
|
};
|