@yagni-app/code 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/cli.js +13 -0
  2. package/dist/extension/footer.d.ts +1 -1
  3. package/dist/extension/hooks.d.ts +111 -0
  4. package/dist/extension/hooks.js +666 -0
  5. package/dist/extension/index.d.ts +13 -6
  6. package/dist/extension/index.js +57 -7
  7. package/dist/extension/{approvedPrefixes.js → permission/approvedPrefixes.js} +1 -1
  8. package/dist/extension/permission/dbReadPolicy.d.ts +90 -0
  9. package/dist/extension/permission/dbReadPolicy.js +227 -0
  10. package/dist/extension/{execPolicy.js → permission/execPolicy.js} +41 -13
  11. package/dist/extension/{permission.d.ts → permission/gate.d.ts} +9 -2
  12. package/dist/extension/{permission.js → permission/gate.js} +103 -4
  13. package/dist/extension/{guardian.d.ts → permission/guardian.d.ts} +2 -2
  14. package/dist/extension/{guardian.js → permission/guardian.js} +1 -1
  15. package/dist/extension/permission/index.d.ts +14 -0
  16. package/dist/extension/permission/index.js +14 -0
  17. package/dist/extension/permission/packageManagerPolicy.d.ts +55 -0
  18. package/dist/extension/permission/packageManagerPolicy.js +170 -0
  19. package/dist/extension/pipeline/activityFeed.js +19 -5
  20. package/dist/extension/pipeline/checker.d.ts +99 -0
  21. package/dist/extension/pipeline/checker.js +238 -0
  22. package/dist/extension/pipeline/fanout.d.ts +116 -0
  23. package/dist/extension/pipeline/fanout.js +248 -0
  24. package/dist/extension/pipeline/fanoutBeats.d.ts +31 -0
  25. package/dist/extension/pipeline/fanoutBeats.js +86 -0
  26. package/dist/extension/pipeline/goCommand.d.ts +14 -0
  27. package/dist/extension/pipeline/goCommand.js +38 -1
  28. package/dist/extension/pipeline/headlessGo.d.ts +163 -0
  29. package/dist/extension/pipeline/headlessGo.js +333 -0
  30. package/dist/extension/pipeline/invocation.d.ts +7 -1
  31. package/dist/extension/pipeline/invocation.js +7 -1
  32. package/dist/extension/pipeline/mission.d.ts +55 -0
  33. package/dist/extension/pipeline/mission.js +70 -0
  34. package/dist/extension/pipeline/orchestrator.d.ts +48 -3
  35. package/dist/extension/pipeline/orchestrator.js +450 -9
  36. package/dist/extension/pipeline/personas.d.ts +16 -1
  37. package/dist/extension/pipeline/personas.js +117 -6
  38. package/dist/extension/pipeline/runSession.d.ts +45 -1
  39. package/dist/extension/pipeline/runState.d.ts +57 -12
  40. package/dist/extension/pipeline/runState.js +60 -18
  41. package/dist/extension/pipeline/runner.js +10 -1
  42. package/dist/extension/pipeline/stages.d.ts +84 -7
  43. package/dist/extension/pipeline/stages.js +166 -0
  44. package/dist/extension/pipeline/tierCap.d.ts +32 -0
  45. package/dist/extension/pipeline/tierCap.js +57 -0
  46. package/dist/extension/pipeline/types.d.ts +130 -1
  47. package/dist/extension/pipeline/types.js +17 -0
  48. package/dist/extension/pipeline/verify.d.ts +86 -3
  49. package/dist/extension/pipeline/verify.js +175 -6
  50. package/dist/extension/turnLog.d.ts +38 -0
  51. package/dist/extension/turnLog.js +93 -0
  52. package/dist/goHeadless.d.ts +75 -0
  53. package/dist/goHeadless.js +132 -0
  54. package/dist/paths.d.ts +9 -0
  55. package/dist/paths.js +12 -0
  56. package/package.json +2 -2
  57. /package/dist/extension/{approvedPrefixes.d.ts → permission/approvedPrefixes.d.ts} +0 -0
  58. /package/dist/extension/{execPolicy.d.ts → permission/execPolicy.d.ts} +0 -0
@@ -0,0 +1,333 @@
1
+ /**
2
+ * The headless front door to the /go pipeline (spec: sandbox harness parity).
3
+ *
4
+ * `/go` is an interactive pi slash command and pi's print mode ignores slash
5
+ * commands, so until now the ONLY way to drive the pipeline from a script was
6
+ * to import `runPipeline` directly (`scripts/eval-go-harness.ts` did exactly
7
+ * that). That gap is why the cloud mission sandbox ran a single flat model
8
+ * session instead of the product's staged pipeline. This module closes it:
9
+ *
10
+ * yagni go --headless --ticket-file <p> [--plan-file <p>] [--memo-file <p>]
11
+ * [--run-id <id>] [--cwd <p>] [--json]
12
+ *
13
+ * It is the SAME `runPipeline` path the interactive command drives (no forked
14
+ * driver) with the interactive-only machinery left out: no worktree, no run
15
+ * registry, no run session mint (the caller already owns the run row and passes
16
+ * its id with `--run-id`), no checkpoint journal, no terminal UI.
17
+ *
18
+ * MISSION MODE (`--plan-file`, optionally `--memo-file`): the factory already
19
+ * produced the plan and a human approved it at the mission's plan gate, so the
20
+ * pipeline skips its own map/plan stages and enters at implement on that plan,
21
+ * with the scoping memo as the repo context the map brief would have given (see
22
+ * mission.ts for the pure rules). Delivery is NOT ours in mission mode: the
23
+ * FINISH stage is a `/go`-session step and is never driven here, so a mission
24
+ * run ends at the reviewed candidate and never commits, pushes, opens a PR, or
25
+ * reports one. The mission's own prHandoff owns all of that.
26
+ *
27
+ * Output contract:
28
+ * - `--json`: one NDJSON line per event on stdout. Child events are the pi
29
+ * `--mode json` objects VERBATIM plus an additive `yagni` attribution key
30
+ * (stage / lens / round), so the event vocabulary is unchanged; pipeline-level
31
+ * lines carry their own `pipeline_*` types.
32
+ * - `{type:"pipeline_fanout", stage, phase, fanout?, children?, fixTurns?}`:
33
+ * the implement diamond's recording beats. Interactive `/go` POSTs these to
34
+ * its run session; headlessly there IS no session, so the SAME
35
+ * `makeFanoutBeats` thread rides the stream instead and the mission
36
+ * collector folds them back onto the Run. Without this a cloud mission would
37
+ * record no partition decision, no per-workstream row, and no fix turn.
38
+ * - Always, as the last line: the result object
39
+ * `{type:"pipeline_result", ok, stopReason, rounds, findings, stages:[{stage,
40
+ * tier, usage}], …}` — or `{type:"pipeline_error", …}` when a build stage
41
+ * threw.
42
+ * - Exit codes: 0 ONLY on a verified candidate (a `clean` stop), 1 for any
43
+ * other outcome or a thrown pipeline error, 2 for a usage/input problem.
44
+ *
45
+ * `YAGNI_GO_TIER_CAP` (eval + template-smoke lanes) is resolved here for the
46
+ * result object and forwarded on the child env; the clamp itself is applied
47
+ * centrally in `runStage` (see tierCap.ts). `YAGNI_GO_FANOUT` is read the same
48
+ * way and pins the implement diamond for a benchmark lane (see fanout.ts).
49
+ */
50
+ import { readFileSync } from "node:fs";
51
+ import { resolveFanoutMode } from "./fanout.js";
52
+ import { makeFanoutBeats } from "./fanoutBeats.js";
53
+ import { normalizeMission } from "./mission.js";
54
+ import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
55
+ import { parseTierCap, TIER_CAP_ENV } from "./tierCap.js";
56
+ /** One-line usage copy, shared by every argument error. */
57
+ export const HEADLESS_GO_USAGE = "Usage: yagni go --headless --ticket-file <path> [--plan-file <path>] [--memo-file <path>] [--run-id <id>] [--cwd <path>] [--json]";
58
+ /**
59
+ * Exit codes. `verified` is deliberately narrow: only a `clean` stop means the
60
+ * run produced a reviewed candidate, so a round_cap / no_changes / failed run
61
+ * can never read as success to a script, a smoke gate, or CI.
62
+ */
63
+ export const HEADLESS_GO_EXIT = { verified: 0, unverified: 1, usage: 2 };
64
+ const VALUE_FLAGS = {
65
+ "--ticket-file": "ticketFile",
66
+ "--plan-file": "planFile",
67
+ "--memo-file": "memoFile",
68
+ "--run-id": "runId",
69
+ "--cwd": "cwd",
70
+ };
71
+ /** A token is flag-shaped when it looks like `--word` (mirrors goFlags.ts). */
72
+ const FLAG_SHAPE = /^--[A-Za-z][\w-]*$/;
73
+ /**
74
+ * Parse the argv remainder after `go`. Accepts both `--flag value` and
75
+ * `--flag=value`; unknown flags and stray positionals are collected rather than
76
+ * silently absorbed, so a typo fails loudly instead of running the wrong thing.
77
+ */
78
+ export function parseHeadlessGoArgs(argv) {
79
+ const args = {
80
+ headless: false,
81
+ json: false,
82
+ unknownFlags: [],
83
+ positionals: [],
84
+ missingValues: [],
85
+ };
86
+ for (let i = 0; i < argv.length; i++) {
87
+ const token = argv[i];
88
+ if (!token)
89
+ continue;
90
+ const eq = token.indexOf("=");
91
+ const name = eq > 0 ? token.slice(0, eq) : token;
92
+ const inlineValue = eq > 0 ? token.slice(eq + 1) : undefined;
93
+ if (name === "--headless") {
94
+ args.headless = true;
95
+ continue;
96
+ }
97
+ if (name === "--json") {
98
+ args.json = true;
99
+ continue;
100
+ }
101
+ const key = VALUE_FLAGS[name];
102
+ if (key) {
103
+ const next = inlineValue ?? argv[i + 1];
104
+ if (!next || FLAG_SHAPE.test(next)) {
105
+ args.missingValues.push(name);
106
+ }
107
+ else {
108
+ args[key] = next;
109
+ if (inlineValue === undefined)
110
+ i++;
111
+ }
112
+ continue;
113
+ }
114
+ if (FLAG_SHAPE.test(name))
115
+ args.unknownFlags.push(name);
116
+ else
117
+ args.positionals.push(token);
118
+ }
119
+ return args;
120
+ }
121
+ /** The argument problem, or undefined when the invocation is usable. */
122
+ export function validateHeadlessGoArgs(args) {
123
+ if (args.unknownFlags.length > 0) {
124
+ return `Unknown flag${args.unknownFlags.length === 1 ? "" : "s"}: ${args.unknownFlags.join(", ")}.`;
125
+ }
126
+ if (args.positionals.length > 0) {
127
+ return `Unexpected argument${args.positionals.length === 1 ? "" : "s"}: ${args.positionals.join(", ")}. The ticket is passed as a file.`;
128
+ }
129
+ if (args.missingValues.length > 0) {
130
+ return `Missing value for ${args.missingValues.join(", ")}.`;
131
+ }
132
+ if (!args.headless)
133
+ return "yagni go is headless-only: pass --headless (use /go inside a session for the interactive run).";
134
+ if (!args.ticketFile)
135
+ return "Missing --ticket-file.";
136
+ return undefined;
137
+ }
138
+ const isBlocking = (f) => f.severity === "critical" || f.severity === "high";
139
+ /**
140
+ * Fold the pipeline result into the reported per-stage usage table. Build/fix
141
+ * stages come from `stages`, review lenses from each round's `lensResults`
142
+ * (tagged with their round). Rows are grouped, not strictly chronological: a
143
+ * later round's fix sits with the build stages, which is honest for a usage
144
+ * table and avoids inventing timestamps the pipeline never recorded.
145
+ */
146
+ function stageRows(result) {
147
+ const rows = result.stages.map((s) => ({
148
+ stage: s.stageId,
149
+ ...(s.tier ? { tier: s.tier } : {}),
150
+ usage: s.usage,
151
+ }));
152
+ for (const round of result.rounds) {
153
+ for (const lensResult of round.lensResults) {
154
+ rows.push({
155
+ stage: lensResult.stageId,
156
+ ...(lensResult.tier ? { tier: lensResult.tier } : {}),
157
+ round: round.round,
158
+ usage: lensResult.usage,
159
+ });
160
+ }
161
+ }
162
+ return rows;
163
+ }
164
+ function buildResult(result, ctx) {
165
+ // FINISH is suppressed in mission mode (delivery belongs to the mission's
166
+ // prHandoff), so a mission result never carries a commit or a PR — and if some
167
+ // caller's pipeline produced one anyway, this run does not claim it as its
168
+ // delivery. Outside mission mode the FINISH info passes through untouched.
169
+ const finish = ctx.missionMode ? undefined : result.finish;
170
+ return {
171
+ ok: result.stopReason === "clean",
172
+ stopReason: result.stopReason,
173
+ rounds: result.rounds.length,
174
+ findings: result.findings.length,
175
+ blocking: result.findings.filter(isBlocking).length,
176
+ stages: stageRows(result),
177
+ ...(ctx.runId ? { runId: ctx.runId } : {}),
178
+ ...(ctx.tierCap ? { tierCap: ctx.tierCap } : {}),
179
+ ...(ctx.missionMode ? { missionMode: true } : {}),
180
+ ...(finish?.commitSha ? { commitSha: finish.commitSha } : {}),
181
+ ...(finish?.prUrl ? { prUrl: finish.prUrl } : {}),
182
+ ...(result.verifyNote ? { verifyNote: result.verifyNote } : {}),
183
+ ...(result.verifyCommand ? { verifyCommand: result.verifyCommand } : {}),
184
+ };
185
+ }
186
+ /** Attribution rider for a streamed child event (never overwrites pi's own keys). */
187
+ function attribution(tag) {
188
+ return {
189
+ stage: tag.stageId,
190
+ ...(tag.lens ? { lens: tag.lens } : {}),
191
+ ...(tag.round ? { round: tag.round } : {}),
192
+ };
193
+ }
194
+ /**
195
+ * Run the pipeline headlessly. Never throws: every failure resolves to an exit
196
+ * code and an honest final line, so the sandbox harness and the smoke gate can
197
+ * read one contract.
198
+ */
199
+ export async function runHeadlessGo(argv, deps = {}) {
200
+ const write = deps.write ?? ((line) => void process.stdout.write(`${line}\n`));
201
+ const writeErr = deps.writeErr ?? ((line) => void process.stderr.write(`${line}\n`));
202
+ const readFile = deps.readFile ?? ((path) => readFileSync(path, "utf8"));
203
+ const runPipeline = deps.runPipeline ?? defaultRunPipeline;
204
+ const env = deps.env ?? process.env;
205
+ const args = parseHeadlessGoArgs(argv);
206
+ const problem = validateHeadlessGoArgs(args);
207
+ if (problem) {
208
+ writeErr(problem);
209
+ writeErr(HEADLESS_GO_USAGE);
210
+ return { exitCode: HEADLESS_GO_EXIT.usage, error: problem };
211
+ }
212
+ // Read every named file up front: an unreadable plan is a hard stop, not a
213
+ // silent fall back to running the pipeline's own planning (which would
214
+ // discard the approved plan the mission gate produced).
215
+ const read = (path, label) => {
216
+ try {
217
+ return readFile(path);
218
+ }
219
+ catch (err) {
220
+ return { error: `Could not read the ${label} at ${path}: ${err instanceof Error ? err.message : String(err)}` };
221
+ }
222
+ };
223
+ const files = {
224
+ ticket: undefined,
225
+ plan: undefined,
226
+ memo: undefined,
227
+ };
228
+ for (const [label, path] of [
229
+ ["ticket", args.ticketFile],
230
+ ["plan", args.planFile],
231
+ ["memo", args.memoFile],
232
+ ]) {
233
+ if (!path)
234
+ continue;
235
+ const contents = read(path, `${label} file`);
236
+ if (typeof contents !== "string") {
237
+ writeErr(contents.error);
238
+ return { exitCode: HEADLESS_GO_EXIT.usage, error: contents.error };
239
+ }
240
+ files[label] = contents.trim();
241
+ }
242
+ if (!files.ticket) {
243
+ const message = `The ticket file ${args.ticketFile} is empty; there is nothing to run.`;
244
+ writeErr(message);
245
+ return { exitCode: HEADLESS_GO_EXIT.usage, error: message };
246
+ }
247
+ const rawCap = env[TIER_CAP_ENV];
248
+ const tierCap = parseTierCap(rawCap);
249
+ if (rawCap?.trim() && !tierCap) {
250
+ writeErr(`Ignoring ${TIER_CAP_ENV}="${rawCap}": not a tier (peak, advanced, standard, efficient). Running uncapped.`);
251
+ }
252
+ const cwd = args.cwd ?? deps.cwd ?? process.cwd();
253
+ const emit = (obj) => write(JSON.stringify(obj));
254
+ const stream = args.json;
255
+ const childEnv = {
256
+ ...(deps.childEnv ?? env),
257
+ ...(tierCap ? { [TIER_CAP_ENV]: tierCap } : {}),
258
+ };
259
+ // A blank plan or memo file is NOT mission context: it must never read as an
260
+ // approved plan (which would skip the pipeline's planning on nothing at all).
261
+ const mission = normalizeMission({ plan: files.plan, memo: files.memo });
262
+ // The implement diamond's recording thread, per RUN. Interactive /go hands
263
+ // these beats to its run session; there is no session here, so they ride the
264
+ // NDJSON stream and the mission collector folds them onto the Run instead.
265
+ const fanoutBeats = makeFanoutBeats();
266
+ const pipelineDeps = {
267
+ cwd,
268
+ childEnv,
269
+ // `go.fanout` rides the same env surface as the tier cap, so a benchmark lane
270
+ // pins the implement diamond without a second flag (spec decision 8).
271
+ fanout: resolveFanoutMode(env),
272
+ checkpointMeta: {
273
+ // No journal is written headlessly (no checkpoint store is injected), so
274
+ // this key only identifies the run in the records the pipeline builds.
275
+ sessionKey: `headless:${args.runId ?? cwd}`,
276
+ ...(args.runId ? { runId: args.runId } : {}),
277
+ },
278
+ ...(deps.signal ? { signal: deps.signal } : {}),
279
+ ...(deps.budget ? { budget: deps.budget } : {}),
280
+ ...(deps.stages ? { stages: deps.stages } : {}),
281
+ ...(deps.grounded !== undefined ? { grounded: deps.grounded } : {}),
282
+ ...(mission ? { mission } : {}),
283
+ onEvent: (ev, tag) => {
284
+ deps.onEvent?.(ev, tag);
285
+ if (stream)
286
+ emit({ ...ev, yagni: attribution(tag) });
287
+ },
288
+ onProgress: (p) => {
289
+ if (!stream)
290
+ return;
291
+ emit({ type: "pipeline_progress", progress: p });
292
+ // Additive, and only for the diamond's own signals: a beat is emitted
293
+ // when the partition resolves, when a child changes state, and when a fix
294
+ // turn lands. Anything else produces no beat at all.
295
+ const beat = fanoutBeats.apply(p);
296
+ if (beat)
297
+ emit({ type: "pipeline_fanout", ...beat });
298
+ },
299
+ onStage: (s) => {
300
+ // `output` carries the plan stage's full text; the stream reports the
301
+ // boundary, never the body.
302
+ const { output: _output, ...boundary } = s;
303
+ if (stream)
304
+ emit({ type: "pipeline_stage", ...boundary });
305
+ },
306
+ logger: (event, data) => {
307
+ deps.logger?.(event, data);
308
+ if (stream)
309
+ emit({ type: "pipeline_log", event, ...(data === undefined ? {} : { data }) });
310
+ },
311
+ };
312
+ try {
313
+ const result = await runPipeline(files.ticket, pipelineDeps);
314
+ const headless = buildResult(result, {
315
+ ...(args.runId ? { runId: args.runId } : {}),
316
+ ...(tierCap ? { tierCap } : {}),
317
+ ...(mission ? { missionMode: true } : {}),
318
+ });
319
+ emit({ type: "pipeline_result", ...headless });
320
+ return {
321
+ exitCode: headless.ok ? HEADLESS_GO_EXIT.verified : HEADLESS_GO_EXIT.unverified,
322
+ result: headless,
323
+ };
324
+ }
325
+ catch (err) {
326
+ const message = err instanceof Error ? err.message : String(err);
327
+ const stage = err?.stageId;
328
+ emit({ type: "pipeline_error", ...(stage ? { stage } : {}), message });
329
+ writeErr(message);
330
+ return { exitCode: HEADLESS_GO_EXIT.unverified, error: message };
331
+ }
332
+ }
333
+ //# sourceMappingURL=headlessGo.js.map
@@ -24,7 +24,7 @@
24
24
  * `renderTemplate` does the {ticket}/{previous} substitution, mirroring the
25
25
  * chain's `step.task.replace(/{previous}/g, …)`.
26
26
  */
27
- import type { PipelineStage, ReviewLens } from "./types.js";
27
+ import type { ModelTier, PipelineStage, ReviewLens } from "./types.js";
28
28
  /**
29
29
  * Anti-fabrication preamble injected as the first `--append-system-prompt` on
30
30
  * every child pi process (both /go stages and subagent tool calls). The
@@ -47,12 +47,18 @@ export declare function renderTemplate(tmpl: string, vars: {
47
47
  /**
48
48
  * Build the pi passthrough argv for one stage (from `--mode` onward). The
49
49
  * positional rendered prompt is always the last arg, prefixed `Task: `.
50
+ *
51
+ * `ctx.tierCap` (the resolved `YAGNI_GO_TIER_CAP` ceiling, absent on a normal
52
+ * run) lowers the emitted `--model` to the cap; it never promotes a stage above
53
+ * its own tier. Passed IN rather than read from the environment here so this
54
+ * builder stays pure — `runStage` owns the env read.
50
55
  */
51
56
  export declare function buildStageInvocation(stage: PipelineStage, ctx: {
52
57
  ticket: string;
53
58
  previous?: string;
54
59
  promptFilePath: string;
55
60
  lens?: ReviewLens;
61
+ tierCap?: ModelTier;
56
62
  }): string[];
57
63
  /**
58
64
  * Prepend the grounding args (the pi cli script, our extension, and the default
@@ -24,6 +24,7 @@
24
24
  * `renderTemplate` does the {ticket}/{previous} substitution, mirroring the
25
25
  * chain's `step.task.replace(/{previous}/g, …)`.
26
26
  */
27
+ import { clampTier } from "./tierCap.js";
27
28
  /**
28
29
  * Anti-fabrication preamble injected as the first `--append-system-prompt` on
29
30
  * every child pi process (both /go stages and subagent tool calls). The
@@ -51,6 +52,11 @@ export function renderTemplate(tmpl, vars) {
51
52
  /**
52
53
  * Build the pi passthrough argv for one stage (from `--mode` onward). The
53
54
  * positional rendered prompt is always the last arg, prefixed `Task: `.
55
+ *
56
+ * `ctx.tierCap` (the resolved `YAGNI_GO_TIER_CAP` ceiling, absent on a normal
57
+ * run) lowers the emitted `--model` to the cap; it never promotes a stage above
58
+ * its own tier. Passed IN rather than read from the environment here so this
59
+ * builder stays pure — `runStage` owns the env read.
54
60
  */
55
61
  export function buildStageInvocation(stage, ctx) {
56
62
  const task = renderTemplate(stage.taskTemplate, {
@@ -63,7 +69,7 @@ export function buildStageInvocation(stage, ctx) {
63
69
  "-p",
64
70
  "--no-session",
65
71
  "--model",
66
- stage.model,
72
+ clampTier(stage.model, ctx.tierCap),
67
73
  "--tools",
68
74
  stage.tools.join(","),
69
75
  "--append-system-prompt",
@@ -0,0 +1,55 @@
1
+ /**
2
+ * PURE mission-mode rules for the /go pipeline (spec: sandbox harness parity).
3
+ *
4
+ * A cloud mission arrives with work the factory has ALREADY done: a plan a human
5
+ * approved at the mission's plan gate, and (when the work item was scoped) a
6
+ * cached scoping memo written against the base commit the run starts from.
7
+ * Re-running the pipeline's own map/plan stages would silently discard the
8
+ * approved plan and hollow out that gate, so an injected plan makes the pipeline
9
+ * enter at implement instead.
10
+ *
11
+ * The three rules, each one line:
12
+ * - an injected PLAN skips map and plan; the plan itself seeds implement,
13
+ * - an injected MEMO stands in for the map stage's repo brief, so map is
14
+ * skipped whenever either input is present,
15
+ * - a memo WITHOUT a plan seeds the (still running) plan stage as its repo map.
16
+ *
17
+ * Repo context is OPTIONAL by contract: with a plan and no memo, implement runs
18
+ * from the plan alone and the seed says nothing about a repo brief that does not
19
+ * exist. Stages must never assume a map brief was produced in mission mode.
20
+ *
21
+ * Delivery is NOT this module's business, but the invariant lives with it: in
22
+ * mission mode the pipeline's FINISH stage does not run (the mission's own
23
+ * prHandoff owns the commit, the push and the PR), so a mission run ends at the
24
+ * reviewed candidate diff.
25
+ */
26
+ import type { StageId } from "./types.js";
27
+ /**
28
+ * The mission inputs, as read from `--plan-file` / `--memo-file`. Both optional:
29
+ * a mission may carry a plan with no memo, and (rarely) a memo with no plan.
30
+ */
31
+ export interface MissionInputs {
32
+ /** The plan markdown a human approved at the mission's plan gate. */
33
+ plan?: string;
34
+ /** The cached scoping memo for the work item at the approved base SHA. */
35
+ memo?: string;
36
+ }
37
+ /** Trim the inputs and drop the blank ones; undefined when nothing usable is left. */
38
+ export declare function normalizeMission(mission: MissionInputs | undefined): MissionInputs | undefined;
39
+ /** True when the run carries mission inputs at all (already-normalized or not). */
40
+ export declare function isMissionMode(mission: MissionInputs | undefined): boolean;
41
+ /**
42
+ * The build stages this mission does NOT run. Map goes whenever there is mission
43
+ * context to enter on; plan goes only when a real approved plan was injected, so
44
+ * a memo-only mission still gets a planning pass (grounded on the memo) rather
45
+ * than sending the worker in with no plan at all.
46
+ */
47
+ export declare function missionSkippedStages(mission: MissionInputs | undefined): StageId[];
48
+ /**
49
+ * The `{previous}` handoff the first surviving build stage is seeded with: the
50
+ * approved plan (implement's "Plan:" slot), with the memo appended as clearly
51
+ * labelled repo context when there is one; or the memo alone when the plan stage
52
+ * still runs and the memo is its repo map.
53
+ */
54
+ export declare function missionSeed(mission: MissionInputs | undefined): string | undefined;
55
+ //# sourceMappingURL=mission.d.ts.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * PURE mission-mode rules for the /go pipeline (spec: sandbox harness parity).
3
+ *
4
+ * A cloud mission arrives with work the factory has ALREADY done: a plan a human
5
+ * approved at the mission's plan gate, and (when the work item was scoped) a
6
+ * cached scoping memo written against the base commit the run starts from.
7
+ * Re-running the pipeline's own map/plan stages would silently discard the
8
+ * approved plan and hollow out that gate, so an injected plan makes the pipeline
9
+ * enter at implement instead.
10
+ *
11
+ * The three rules, each one line:
12
+ * - an injected PLAN skips map and plan; the plan itself seeds implement,
13
+ * - an injected MEMO stands in for the map stage's repo brief, so map is
14
+ * skipped whenever either input is present,
15
+ * - a memo WITHOUT a plan seeds the (still running) plan stage as its repo map.
16
+ *
17
+ * Repo context is OPTIONAL by contract: with a plan and no memo, implement runs
18
+ * from the plan alone and the seed says nothing about a repo brief that does not
19
+ * exist. Stages must never assume a map brief was produced in mission mode.
20
+ *
21
+ * Delivery is NOT this module's business, but the invariant lives with it: in
22
+ * mission mode the pipeline's FINISH stage does not run (the mission's own
23
+ * prHandoff owns the commit, the push and the PR), so a mission run ends at the
24
+ * reviewed candidate diff.
25
+ */
26
+ const REPO_CONTEXT_HEADING = "## Repo context (cached scoping memo)";
27
+ const REPO_CONTEXT_NOTE = "This run entered at implement, so no map stage ran. The memo below is the repo brief that stands in for it: " +
28
+ "it was cached when this work item was scoped, against the base commit this run started from. " +
29
+ "Treat it as a starting point and verify anything you lean on.";
30
+ /** Trim the inputs and drop the blank ones; undefined when nothing usable is left. */
31
+ export function normalizeMission(mission) {
32
+ const plan = mission?.plan?.trim();
33
+ const memo = mission?.memo?.trim();
34
+ if (!plan && !memo)
35
+ return undefined;
36
+ return { ...(plan ? { plan } : {}), ...(memo ? { memo } : {}) };
37
+ }
38
+ /** True when the run carries mission inputs at all (already-normalized or not). */
39
+ export function isMissionMode(mission) {
40
+ return normalizeMission(mission) !== undefined;
41
+ }
42
+ /**
43
+ * The build stages this mission does NOT run. Map goes whenever there is mission
44
+ * context to enter on; plan goes only when a real approved plan was injected, so
45
+ * a memo-only mission still gets a planning pass (grounded on the memo) rather
46
+ * than sending the worker in with no plan at all.
47
+ */
48
+ export function missionSkippedStages(mission) {
49
+ const normalized = normalizeMission(mission);
50
+ if (!normalized)
51
+ return [];
52
+ return normalized.plan ? ["map", "plan"] : ["map"];
53
+ }
54
+ /**
55
+ * The `{previous}` handoff the first surviving build stage is seeded with: the
56
+ * approved plan (implement's "Plan:" slot), with the memo appended as clearly
57
+ * labelled repo context when there is one; or the memo alone when the plan stage
58
+ * still runs and the memo is its repo map.
59
+ */
60
+ export function missionSeed(mission) {
61
+ const normalized = normalizeMission(mission);
62
+ if (!normalized)
63
+ return undefined;
64
+ if (!normalized.plan)
65
+ return normalized.memo;
66
+ if (!normalized.memo)
67
+ return normalized.plan;
68
+ return `${normalized.plan}\n\n${REPO_CONTEXT_HEADING}\n\n${REPO_CONTEXT_NOTE}\n\n${normalized.memo}`;
69
+ }
70
+ //# sourceMappingURL=mission.js.map
@@ -3,7 +3,11 @@
3
3
  *
4
4
  * Sequences `runStage` for the build stages (map → plan → implement), threading
5
5
  * each stage's `finalOutput` into the next as `{previous}` (mirrors the subagent
6
- * example's chain). Then it runs the bounded review→fix LOOP: each round fans the
6
+ * example's chain). In MISSION mode (`deps.mission`, from the headless entry's
7
+ * `--plan-file` / `--memo-file`) the build half is entered part-way: an approved
8
+ * plan skips map + plan and seeds implement, an injected memo stands in for the
9
+ * map brief, and the review→fix loop below runs exactly as it always does — see
10
+ * mission.ts. Then it runs the bounded review→fix LOOP: each round fans the
7
11
  * review stage out across the 3 lenses (parallel, bounded by MAX_CONCURRENCY),
8
12
  * parses each lens with `parseFindings` (with ONE cheap format-recovery re-ask
9
13
  * for a healthy lens that broke the findings contract — spec §3e), unions them,
@@ -21,10 +25,11 @@
21
25
  * findings — i.e. a false "reviewed and clean").
22
26
  */
23
27
  import { type RunBudget } from "./budget.js";
28
+ import { type MissionInputs } from "./mission.js";
24
29
  import { runStage as defaultRunStage } from "./runner.js";
25
- import { type VerifyOutcome } from "./verify.js";
30
+ import { type VerifyOutcome, type WorkstreamCheckResult, type WorkstreamCheckTarget } from "./verify.js";
26
31
  import { snapshotWorkspace as defaultSnapshotWorkspace } from "./workspace.js";
27
- import { type CheckpointStore, type PipelineStage, type JsonEvent, type PipelineProgress, type PipelineResult, type ResumePlan, type StageId, type StageResult, type StageTag } from "./types.js";
32
+ import { type CheckpointStore, type FanoutBudgetVerdict, type FanoutMode, type PipelineStage, type JsonEvent, type PipelineProgress, type PipelineResult, type ResumePlan, type StageId, type StageResult, type StageTag } from "./types.js";
28
33
  export interface RunPipelineDeps {
29
34
  cwd: string;
30
35
  signal?: AbortSignal;
@@ -38,6 +43,15 @@ export interface RunPipelineDeps {
38
43
  * and records an honest note. Injected as a whole for unit testing.
39
44
  */
40
45
  runVerify?: (cwd: string, signal?: AbortSignal, baselinePaths?: string[]) => Promise<VerifyOutcome>;
46
+ /**
47
+ * The implement diamond's per-workstream scoped typecheck (spec decision 6):
48
+ * each workstream's claims resolved to package dirs and checked THERE, so a
49
+ * broken workstream is attributed by name before the merged tree is verified as
50
+ * a whole. Defaults to the real `makeWorkstreamCheck`; injected as a whole for
51
+ * unit testing, exactly like `runVerify`. Never consulted on the single-writer
52
+ * path (one builder owns everything, so there is nothing to attribute).
53
+ */
54
+ runWorkstreamCheck?: (cwd: string, targets: WorkstreamCheckTarget[], signal?: AbortSignal) => Promise<WorkstreamCheckResult[]>;
41
55
  /**
42
56
  * The REAL ticket brief (verbatim title + description) resolved from the ticket
43
57
  * reference, injected into every stage's `{ticket}` prompt slot so the agents plan
@@ -112,6 +126,37 @@ export interface RunPipelineDeps {
112
126
  * tree) and re-enters the bounded review→fix loop at `resumeFrom.round`.
113
127
  */
114
128
  resumeFrom?: ResumePlan;
129
+ /**
130
+ * Mission-mode inputs, populated by the headless entry point's `--plan-file`
131
+ * / `--memo-file` (spec: sandbox harness parity). `plan` is the plan the
132
+ * human already approved at the mission's plan gate, and `memo` is the cached
133
+ * scoping memo that stands in for the map stage's repo brief. Both are
134
+ * OPTIONAL by contract: mission mode must degrade honestly when repo context
135
+ * is absent rather than assume a map brief exists. Carried as one object so
136
+ * the mission-mode path reads a single field; absent on every interactive run.
137
+ * The stage-skipping and seeding rules are pure and live in `mission.ts`.
138
+ */
139
+ mission?: MissionInputs;
140
+ /**
141
+ * `go.fanout` (spec decision 8). `auto` (the default) runs the partitioner and
142
+ * lets its conservative verdict stand; `always` pins the diamond for benchmark
143
+ * lanes — it biases the partition prompt and overrides a budget denial, but it
144
+ * still cannot fan without a partition the orchestrator actually produced.
145
+ */
146
+ fanout?: FanoutMode;
147
+ /**
148
+ * The fan-out budget seam (spec decision 8). Consulted ONCE, only when the
149
+ * partition came back `fan`: a denial degrades the stage to single-writer with
150
+ * the seam's own reason recorded. The machinery behind it is a separate
151
+ * workstream; an ABSENT seam allows, so nothing here waits on it.
152
+ */
153
+ fanoutBudget?: () => Promise<FanoutBudgetVerdict> | FanoutBudgetVerdict;
154
+ /**
155
+ * Live ultra-mode probe (/ultra), mirroring the general subagent tool's seam:
156
+ * it widens the diamond's concurrent-builder ceiling from 4 to 8. It changes
157
+ * NOTHING else — not the width the partitioner may choose, not a tier.
158
+ */
159
+ isUltra?: () => boolean;
115
160
  /** Identity stamped onto every checkpoint record (run key + backend handle). */
116
161
  checkpointMeta?: {
117
162
  sessionKey: string;