@yagni-app/code-staging 1.2.6-staging.1952.1 → 1.2.6-staging.1958.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 (30) hide show
  1. package/dist/extension/branding.d.ts +21 -1
  2. package/dist/extension/branding.js +53 -12
  3. package/dist/extension/index.js +5 -0
  4. package/dist/extension/pipeline/changeScan.d.ts +62 -0
  5. package/dist/extension/pipeline/changeScan.js +307 -0
  6. package/dist/extension/pipeline/cloud.d.ts +205 -0
  7. package/dist/extension/pipeline/cloud.js +382 -0
  8. package/dist/extension/pipeline/continuation.d.ts +7 -3
  9. package/dist/extension/pipeline/continuation.js +29 -13
  10. package/dist/extension/pipeline/findings.d.ts +9 -4
  11. package/dist/extension/pipeline/findings.js +25 -10
  12. package/dist/extension/pipeline/finish.d.ts +11 -1
  13. package/dist/extension/pipeline/finish.js +51 -5
  14. package/dist/extension/pipeline/headlessGo.d.ts +20 -1
  15. package/dist/extension/pipeline/headlessGo.js +205 -33
  16. package/dist/extension/pipeline/orchestrator.d.ts +8 -2
  17. package/dist/extension/pipeline/orchestrator.js +249 -68
  18. package/dist/extension/pipeline/personas.d.ts +8 -3
  19. package/dist/extension/pipeline/personas.js +42 -7
  20. package/dist/extension/pipeline/planOutput.d.ts +42 -0
  21. package/dist/extension/pipeline/planOutput.js +94 -0
  22. package/dist/extension/pipeline/stages.d.ts +34 -5
  23. package/dist/extension/pipeline/stages.js +136 -7
  24. package/dist/extension/pipeline/tierCap.d.ts +22 -1
  25. package/dist/extension/pipeline/tierCap.js +48 -1
  26. package/dist/extension/pipeline/types.d.ts +31 -0
  27. package/dist/extension/pipeline/types.js +2 -0
  28. package/dist/extension/pipeline/verify.d.ts +33 -0
  29. package/dist/extension/pipeline/verify.js +101 -9
  30. package/package.json +2 -2
@@ -0,0 +1,205 @@
1
+ /**
2
+ * The one-attempt lane's session contract (spec decisions 2 and 3): what
3
+ * `yagni go --headless --cloud` reads besides its flags, and the `result.json`
4
+ * it leaves for the host.
5
+ *
6
+ * The host (PR A's `startSessionActivity`) writes the input folder beside the
7
+ * ticket file and sets the env below; the session reads them and writes its
8
+ * outputs under `YAGNI_MISSION_OUT_DIR`. The session commits locally and never
9
+ * pushes: the host collects the commits as a bundle and pushes as the GitHub
10
+ * App. `result.json` is the agent's report, written as the session's last act
11
+ * (`completeCloudSession`) and mirrored on the last `pipeline_result` line of
12
+ * the NDJSON stream. headlessGo.ts runs the pipeline between the two halves
13
+ * here: `prepareCloudLane` before it, `completeCloudSession` after it.
14
+ *
15
+ * The extension does not depend on `@yagni/shared`, where the host's parser
16
+ * lives (`packages/shared/src/attempts.ts`): the two meet only at this JSON.
17
+ */
18
+ import type { ContinuationStage } from "./continuation.js";
19
+ import type { VerifyCheckRun, VerifyOutcome } from "./verify.js";
20
+ import type { WorktreeExec } from "./worktree.js";
21
+ import type { Finding, PipelineLane, PipelineResult, StopReason } from "./types.js";
22
+ /** Set on every stage child of a cloud session, so its prompt describes the cloud sandbox. */
23
+ export declare const CLOUD_SESSION_ENV = "YAGNI_GO_CLOUD";
24
+ /** Where the session writes `plan.md`, `plan.json` and `result.json`. */
25
+ export declare const MISSION_OUT_DIR_ENV = "YAGNI_MISSION_OUT_DIR";
26
+ /** Reeve's persona body, beside the ticket, which the plan stage runs with when present. */
27
+ export declare const PLAN_PERSONA_FILE_NAME = "plan-persona.md";
28
+ export declare const RESULT_FILE_NAME = "result.json";
29
+ export declare const PLAN_MARKDOWN_FILE_NAME = "plan.md";
30
+ export declare const PLAN_JSON_FILE_NAME = "plan.json";
31
+ /** Which stage this execution started at: the attempt's record of it. */
32
+ export type CloudStage = "plan" | "implement" | "fix" | "verify";
33
+ /** The execution's identity, echoed verbatim from the host's env. The session never invents it. */
34
+ export interface CloudIdentity {
35
+ attemptId: string;
36
+ executionId: string;
37
+ generation: number;
38
+ }
39
+ /** The identity and the output folder, or what is missing. */
40
+ export declare function readCloudEnv(env: NodeJS.ProcessEnv): {
41
+ identity: CloudIdentity;
42
+ outDir: string;
43
+ } | {
44
+ problem: string;
45
+ };
46
+ /**
47
+ * The stage this execution starts at: a fresh attempt plans; a continue file
48
+ * names its stage; a plan file alone starts implement. A continue file that
49
+ * names review (the legacy spelling of a stop in the loop) re-enters the loop
50
+ * as a fix execution does.
51
+ */
52
+ export declare function cloudStartStage(continuation: {
53
+ stageAtStop: ContinuationStage | null;
54
+ } | undefined, hasPlan: boolean): CloudStage;
55
+ /**
56
+ * The lane a cloud execution runs and the stage it starts at: the host's
57
+ * per-stage tiers applied, and Reeve's persona (`plan-persona.md`) on the plan
58
+ * stage when the host sent one. The host wrote every file here, so one the
59
+ * session cannot use is the host's to fix, never a reason to plan a ticket
60
+ * the attempt is building: that is a `problem`, and nothing runs.
61
+ */
62
+ export declare function prepareCloudLane(input: {
63
+ inputDir: string;
64
+ env: NodeJS.ProcessEnv;
65
+ continueFile?: string;
66
+ continuation?: {
67
+ stageAtStop: ContinuationStage | null;
68
+ };
69
+ hasPlan: boolean;
70
+ readFile: (path: string) => string;
71
+ writeErr: (line: string) => void;
72
+ }): {
73
+ stage: CloudStage;
74
+ lane: PipelineLane;
75
+ } | {
76
+ problem: string;
77
+ };
78
+ /** The stop reasons `result.json` carries. */
79
+ export type ResultStopReason = "clean" | "round_cap" | "no_changes" | "budget_exceeded" | "failed" | "aborted";
80
+ /**
81
+ * The pipeline's stop as the result names it. `partial` (a write stage cut off
82
+ * after it began editing) is `failed` here; its change is still committed, and
83
+ * the result's notes say so.
84
+ */
85
+ export declare function resultStopReason(stop: StopReason): ResultStopReason;
86
+ /** One finding the session's own review left open. */
87
+ export interface OpenFinding {
88
+ file?: string;
89
+ line?: number;
90
+ message: string;
91
+ severity: "blocking" | "advisory";
92
+ }
93
+ /** `result.json`, version 1. */
94
+ export interface AttemptResult extends CloudIdentity {
95
+ version: 1;
96
+ stage: CloudStage;
97
+ headSha: string;
98
+ branch: string | null;
99
+ baseSha: string;
100
+ stopReason: ResultStopReason;
101
+ rounds: number;
102
+ checks: VerifyCheckRun[];
103
+ findingsOpen: OpenFinding[];
104
+ filesChanged: string[];
105
+ commits: string[];
106
+ notes: string[];
107
+ blockingKey: string | null;
108
+ usage: {
109
+ tokens: number;
110
+ costMillicents: number;
111
+ byStage: Array<{
112
+ stage: string;
113
+ tokens: number;
114
+ costMillicents: number;
115
+ }>;
116
+ };
117
+ }
118
+ /** The findings the result reports, blocking first. */
119
+ export declare function openFindings(findings: readonly Finding[]): OpenFinding[];
120
+ /**
121
+ * What the review's blocking findings are, as one key two executions can
122
+ * compare (the no-progress rule, #2669), or null when none blocks. The same
123
+ * fold as `blockingFindingsKey` in the backend's
124
+ * integrations/e2b/missionPipelineStream.ts: each finding's file and the head
125
+ * of its message without any output tail, lowercased; repeats count, and so
126
+ * does the blocking total. A change there is a change here.
127
+ */
128
+ export declare function blockingFindingsKey(findings: readonly Finding[]): string | null;
129
+ /** The run's spend, in all and by stage. */
130
+ export declare function resultUsage(result: Pick<PipelineResult, "stages" | "rounds">): AttemptResult["usage"];
131
+ /** What git says about the execution's work: its head and what it committed since `baseSha`. */
132
+ export interface GitFacts {
133
+ headSha: string;
134
+ commits: string[];
135
+ filesChanged: string[];
136
+ /** One line per git read that failed, so a report of nothing committed is never a silent guess. */
137
+ notes: string[];
138
+ }
139
+ /**
140
+ * The branch the session commits on: the one the host checked out before it
141
+ * started (`yagni/<ticket>`). The session never names or creates a branch.
142
+ * Null on a detached HEAD.
143
+ */
144
+ export declare function currentBranch(cwd: string, exec: WorktreeExec): Promise<string | null>;
145
+ /** The ticket a branch named `yagni/<ticket>` is for, for the commit's provenance trailer. */
146
+ export declare function ticketOfBranch(branch: string | null): string | undefined;
147
+ /**
148
+ * Whether the branch carries a commit this attempt made, in this execution or
149
+ * an earlier one: each carries the attempt's `YAGNI-Run` trailer (finish.ts).
150
+ * Null when git cannot say.
151
+ */
152
+ export declare function attemptCommitted(cwd: string, exec: WorktreeExec, attemptId: string): Promise<boolean | null>;
153
+ /** Read the execution's work off git. Never throws: a read that fails is a note. */
154
+ export declare function gitFacts(cwd: string, exec: WorktreeExec, baseSha: string): Promise<GitFacts>;
155
+ /** What the session's last act needs, gathered once the pipeline has stopped. */
156
+ export interface CloudCompletion {
157
+ identity: CloudIdentity;
158
+ outDir: string;
159
+ stage: CloudStage;
160
+ cwd: string;
161
+ /** The commit the execution started from. */
162
+ baseSha: string;
163
+ /** The tree's pre-run dirt, never committed. */
164
+ preRunDirt: string[];
165
+ /** The approved plan: the commit's title and summary. */
166
+ plan?: string;
167
+ /** Review rounds earlier executions finished. */
168
+ roundsDone: number;
169
+ /** The gate's last verdict, and the tree it judged; absent when it never ran. */
170
+ lastVerify?: {
171
+ outcome: VerifyOutcome;
172
+ tree: string;
173
+ };
174
+ /** The tree as the gate's verdict is compared against it. */
175
+ tree: () => Promise<string>;
176
+ /** The gate taken again on the final tree, or null when the run has no time left for it. */
177
+ reverify?: () => Promise<VerifyOutcome | null>;
178
+ /** The secret and binary scan of what the execution added, or null when git cannot say. */
179
+ scan: () => Promise<Finding[] | null>;
180
+ /** The plan's package folders, read off its text, when its block names none. */
181
+ planPaths: (planText: string) => string[];
182
+ exec: WorktreeExec;
183
+ writeFile: (path: string, text: string) => void;
184
+ }
185
+ /**
186
+ * The cloud session's last act. A plan execution writes `plan.md` and
187
+ * `plan.json`. Any other runs the secret and binary scan on what it added,
188
+ * whatever its stop (it runs no model, so no deadline skips it), takes the
189
+ * checks again when the gate's last verdict judged an earlier tree, and
190
+ * commits its work locally on the branch the host checked out, so nothing is
191
+ * lost when the sandbox goes. `result.json` is written last; the caller mirrors
192
+ * it on the stream. `ok` is a written plan, or a clean candidate whose branch
193
+ * carries this attempt's committed work, from this execution or an earlier
194
+ * one: a clean stop with nothing committed at all delivered nothing.
195
+ */
196
+ export declare function completeCloudSession(c: CloudCompletion, ran: {
197
+ result: PipelineResult;
198
+ } | {
199
+ error: string;
200
+ partial: PipelineResult["stages"];
201
+ }): Promise<{
202
+ attempt: AttemptResult;
203
+ ok: boolean;
204
+ }>;
205
+ //# sourceMappingURL=cloud.d.ts.map
@@ -0,0 +1,382 @@
1
+ /**
2
+ * The one-attempt lane's session contract (spec decisions 2 and 3): what
3
+ * `yagni go --headless --cloud` reads besides its flags, and the `result.json`
4
+ * it leaves for the host.
5
+ *
6
+ * The host (PR A's `startSessionActivity`) writes the input folder beside the
7
+ * ticket file and sets the env below; the session reads them and writes its
8
+ * outputs under `YAGNI_MISSION_OUT_DIR`. The session commits locally and never
9
+ * pushes: the host collects the commits as a bundle and pushes as the GitHub
10
+ * App. `result.json` is the agent's report, written as the session's last act
11
+ * (`completeCloudSession`) and mirrored on the last `pipeline_result` line of
12
+ * the NDJSON stream. headlessGo.ts runs the pipeline between the two halves
13
+ * here: `prepareCloudLane` before it, `completeCloudSession` after it.
14
+ *
15
+ * The extension does not depend on `@yagni/shared`, where the host's parser
16
+ * lives (`packages/shared/src/attempts.ts`): the two meet only at this JSON.
17
+ */
18
+ import { createHash } from "node:crypto";
19
+ import { join } from "node:path";
20
+ import { isBlockingFinding } from "./findings.js";
21
+ import { planTitle, runFinish, verifyTrailerValue } from "./finish.js";
22
+ import { buildPlanFile, planMarkdown } from "./planOutput.js";
23
+ import { cloudLane } from "./stages.js";
24
+ import { applyStageTiers, parseStageTiers, STAGE_TIERS_ENV } from "./tierCap.js";
25
+ /** Set on every stage child of a cloud session, so its prompt describes the cloud sandbox. */
26
+ export const CLOUD_SESSION_ENV = "YAGNI_GO_CLOUD";
27
+ /** Where the session writes `plan.md`, `plan.json` and `result.json`. */
28
+ export const MISSION_OUT_DIR_ENV = "YAGNI_MISSION_OUT_DIR";
29
+ /** Reeve's persona body, beside the ticket, which the plan stage runs with when present. */
30
+ export const PLAN_PERSONA_FILE_NAME = "plan-persona.md";
31
+ export const RESULT_FILE_NAME = "result.json";
32
+ export const PLAN_MARKDOWN_FILE_NAME = "plan.md";
33
+ export const PLAN_JSON_FILE_NAME = "plan.json";
34
+ /** The identity and the output folder, or what is missing. */
35
+ export function readCloudEnv(env) {
36
+ const attemptId = env.YAGNI_ATTEMPT_ID?.trim();
37
+ const executionId = env.YAGNI_EXECUTION_ID?.trim();
38
+ const rawGeneration = env.YAGNI_GENERATION?.trim();
39
+ const generation = rawGeneration && /^\d+$/.test(rawGeneration) ? Number(rawGeneration) : Number.NaN;
40
+ const outDir = env[MISSION_OUT_DIR_ENV]?.trim();
41
+ const missing = [
42
+ ...(attemptId ? [] : ["YAGNI_ATTEMPT_ID"]),
43
+ ...(executionId ? [] : ["YAGNI_EXECUTION_ID"]),
44
+ ...(Number.isSafeInteger(generation) ? [] : ["YAGNI_GENERATION"]),
45
+ ...(outDir ? [] : [MISSION_OUT_DIR_ENV]),
46
+ ];
47
+ if (missing.length > 0 || !attemptId || !executionId || !outDir) {
48
+ return { problem: `A cloud session needs ${missing.join(", ")} from the host.` };
49
+ }
50
+ return { identity: { attemptId, executionId, generation }, outDir };
51
+ }
52
+ /**
53
+ * The stage this execution starts at: a fresh attempt plans; a continue file
54
+ * names its stage; a plan file alone starts implement. A continue file that
55
+ * names review (the legacy spelling of a stop in the loop) re-enters the loop
56
+ * as a fix execution does.
57
+ */
58
+ export function cloudStartStage(continuation, hasPlan) {
59
+ if (continuation) {
60
+ const stage = continuation.stageAtStop;
61
+ if (stage === "review" || stage === "fix")
62
+ return "fix";
63
+ if (stage === "verify")
64
+ return "verify";
65
+ return "implement";
66
+ }
67
+ return hasPlan ? "implement" : "plan";
68
+ }
69
+ /**
70
+ * The lane a cloud execution runs and the stage it starts at: the host's
71
+ * per-stage tiers applied, and Reeve's persona (`plan-persona.md`) on the plan
72
+ * stage when the host sent one. The host wrote every file here, so one the
73
+ * session cannot use is the host's to fix, never a reason to plan a ticket
74
+ * the attempt is building: that is a `problem`, and nothing runs.
75
+ */
76
+ export function prepareCloudLane(input) {
77
+ const stage = cloudStartStage(input.continuation, input.hasPlan);
78
+ if (input.continueFile && !input.continuation) {
79
+ return { problem: `A cloud session cannot run on the continue file ${input.continueFile}.` };
80
+ }
81
+ if (stage === "implement" && !input.hasPlan) {
82
+ return { problem: "A cloud implement execution needs the approved plan (--plan-file)." };
83
+ }
84
+ const { tiers, ignored } = parseStageTiers(input.env[STAGE_TIERS_ENV]);
85
+ if (ignored.length > 0) {
86
+ input.writeErr(`Ignoring ${STAGE_TIERS_ENV} entries that name no stage or tier: ${ignored.join(", ")}.`);
87
+ }
88
+ let persona = "";
89
+ const personaPath = join(input.inputDir, PLAN_PERSONA_FILE_NAME);
90
+ try {
91
+ persona = input.readFile(personaPath).trim();
92
+ }
93
+ catch (err) {
94
+ const code = err?.code;
95
+ if (code !== "ENOENT") {
96
+ input.writeErr(`Could not read ${personaPath} (${err instanceof Error ? err.message : String(err)}); the plan stage runs on its default persona.`);
97
+ }
98
+ }
99
+ const base = cloudLane(input.inputDir);
100
+ return {
101
+ stage,
102
+ lane: {
103
+ ...base,
104
+ stages: applyStageTiers(base.stages, tiers).map((s) => s.id === "plan" && persona ? { ...s, persona } : s),
105
+ },
106
+ };
107
+ }
108
+ /**
109
+ * The pipeline's stop as the result names it. `partial` (a write stage cut off
110
+ * after it began editing) is `failed` here; its change is still committed, and
111
+ * the result's notes say so.
112
+ */
113
+ export function resultStopReason(stop) {
114
+ return stop === "partial" ? "failed" : stop;
115
+ }
116
+ const MAX_OPEN_FINDINGS = 50;
117
+ const MAX_FINDING_MESSAGE = 1000;
118
+ /** The findings the result reports, blocking first. */
119
+ export function openFindings(findings) {
120
+ return [...findings]
121
+ .sort((a, b) => Number(isBlockingFinding(b)) - Number(isBlockingFinding(a)))
122
+ .slice(0, MAX_OPEN_FINDINGS)
123
+ .map((finding) => ({
124
+ ...(finding.file ? { file: finding.file } : {}),
125
+ ...(finding.line !== undefined ? { line: finding.line } : {}),
126
+ message: finding.message.slice(0, MAX_FINDING_MESSAGE),
127
+ severity: isBlockingFinding(finding) ? "blocking" : "advisory",
128
+ }));
129
+ }
130
+ /**
131
+ * What the review's blocking findings are, as one key two executions can
132
+ * compare (the no-progress rule, #2669), or null when none blocks. The same
133
+ * fold as `blockingFindingsKey` in the backend's
134
+ * integrations/e2b/missionPipelineStream.ts: each finding's file and the head
135
+ * of its message without any output tail, lowercased; repeats count, and so
136
+ * does the blocking total. A change there is a change here.
137
+ */
138
+ export function blockingFindingsKey(findings) {
139
+ const parts = findings
140
+ .filter(isBlockingFinding)
141
+ .map((finding) => {
142
+ const head = finding.message.split(/\bOutput tail\b|\n/)[0] ?? "";
143
+ const words = head.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 160);
144
+ return `${finding.file ?? ""}\u0000${words}`;
145
+ })
146
+ .sort();
147
+ if (parts.length === 0)
148
+ return null;
149
+ return createHash("sha256")
150
+ .update(`${parts.length}\u0001${parts.join("\u0001")}`)
151
+ .digest("hex")
152
+ .slice(0, 32);
153
+ }
154
+ const tokensOf = (usage) => usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
155
+ /** Dollars, as the model proxy reports them, to millicents. */
156
+ const millicents = (dollars) => Math.round(dollars * 100_000);
157
+ /** The run's spend, in all and by stage. */
158
+ export function resultUsage(result) {
159
+ const byStage = new Map();
160
+ const rows = [...result.stages, ...result.rounds.flatMap((round) => round.lensResults)];
161
+ for (const row of rows) {
162
+ const entry = byStage.get(row.stageId) ?? { tokens: 0, cost: 0 };
163
+ entry.tokens += tokensOf(row.usage);
164
+ entry.cost += row.usage.cost;
165
+ byStage.set(row.stageId, entry);
166
+ }
167
+ const stages = [...byStage].map(([stage, entry]) => ({
168
+ stage,
169
+ tokens: entry.tokens,
170
+ costMillicents: millicents(entry.cost),
171
+ }));
172
+ return {
173
+ tokens: stages.reduce((sum, entry) => sum + entry.tokens, 0),
174
+ costMillicents: millicents([...byStage.values()].reduce((sum, entry) => sum + entry.cost, 0)),
175
+ byStage: stages,
176
+ };
177
+ }
178
+ /** One git read: its lines, or null when it failed (a non-zero exit or a spawn error). */
179
+ async function gitLines(cwd, exec, args) {
180
+ const result = await exec(["git", ...args], cwd).catch(() => ({
181
+ code: -1,
182
+ output: "",
183
+ }));
184
+ if (result.code !== 0)
185
+ return null;
186
+ return result.output
187
+ .split("\n")
188
+ .map((line) => line.trim())
189
+ .filter(Boolean);
190
+ }
191
+ /**
192
+ * The branch the session commits on: the one the host checked out before it
193
+ * started (`yagni/<ticket>`). The session never names or creates a branch.
194
+ * Null on a detached HEAD.
195
+ */
196
+ export async function currentBranch(cwd, exec) {
197
+ return (await gitLines(cwd, exec, ["branch", "--show-current"]))?.[0] ?? null;
198
+ }
199
+ /** The ticket a branch named `yagni/<ticket>` is for, for the commit's provenance trailer. */
200
+ export function ticketOfBranch(branch) {
201
+ const ticket = branch?.startsWith("yagni/") ? branch.slice("yagni/".length).trim() : "";
202
+ return ticket || undefined;
203
+ }
204
+ /**
205
+ * Whether the branch carries a commit this attempt made, in this execution or
206
+ * an earlier one: each carries the attempt's `YAGNI-Run` trailer (finish.ts).
207
+ * Null when git cannot say.
208
+ */
209
+ export async function attemptCommitted(cwd, exec, attemptId) {
210
+ const found = await gitLines(cwd, exec, [
211
+ "log",
212
+ "-1",
213
+ "--format=%H",
214
+ "--fixed-strings",
215
+ `--grep=YAGNI-Run: ${attemptId}`,
216
+ ]);
217
+ return found === null ? null : found.length > 0;
218
+ }
219
+ /** Read the execution's work off git. Never throws: a read that fails is a note. */
220
+ export async function gitFacts(cwd, exec, baseSha) {
221
+ const notes = [];
222
+ const read = async (args) => {
223
+ const found = await gitLines(cwd, exec, args);
224
+ if (found === null)
225
+ notes.push(`git ${args.join(" ")} failed, so the result cannot say what it lists.`);
226
+ return found ?? [];
227
+ };
228
+ const head = (await read(["rev-parse", "HEAD"]))[0] || baseSha;
229
+ if (!baseSha || head === baseSha)
230
+ return { headSha: head, commits: [], filesChanged: [], notes };
231
+ return {
232
+ headSha: head,
233
+ commits: await read(["rev-list", "--reverse", `${baseSha}..${head}`]),
234
+ filesChanged: await read(["diff", "--name-only", baseSha, head]),
235
+ notes,
236
+ };
237
+ }
238
+ /** A finding as the result dedupes it: where it is and what it says. */
239
+ function findingKey(finding) {
240
+ return `${finding.file ?? ""}:${finding.line ?? ""}:${finding.message}`;
241
+ }
242
+ /**
243
+ * The cloud session's last act. A plan execution writes `plan.md` and
244
+ * `plan.json`. Any other runs the secret and binary scan on what it added,
245
+ * whatever its stop (it runs no model, so no deadline skips it), takes the
246
+ * checks again when the gate's last verdict judged an earlier tree, and
247
+ * commits its work locally on the branch the host checked out, so nothing is
248
+ * lost when the sandbox goes. `result.json` is written last; the caller mirrors
249
+ * it on the stream. `ok` is a written plan, or a clean candidate whose branch
250
+ * carries this attempt's committed work, from this execution or an earlier
251
+ * one: a clean stop with nothing committed at all delivered nothing.
252
+ */
253
+ export async function completeCloudSession(c, ran) {
254
+ const notes = "error" in ran ? [ran.error] : [];
255
+ const writeOut = (name, text) => {
256
+ try {
257
+ c.writeFile(join(c.outDir, name), text);
258
+ return true;
259
+ }
260
+ catch (err) {
261
+ notes.push(`${name} could not be written: ${err instanceof Error ? err.message : String(err)}`);
262
+ return false;
263
+ }
264
+ };
265
+ const result = "result" in ran ? ran.result : undefined;
266
+ let stop = result?.stopReason ?? "failed";
267
+ if (stop === "partial")
268
+ notes.push("A build stage stopped partway; its change is committed as it was left.");
269
+ if (result?.verifyNote)
270
+ notes.push(result.verifyNote);
271
+ let outputsWritten = true;
272
+ let committed = false;
273
+ let checks = c.lastVerify?.outcome;
274
+ const extra = [];
275
+ const branch = await currentBranch(c.cwd, c.exec);
276
+ if (c.stage === "plan") {
277
+ const planText = result?.plan ?? "";
278
+ const plan = buildPlanFile(planText, () => c.planPaths(planText));
279
+ if (planText.trim() && !plan.found) {
280
+ notes.push("The plan carried no plan-json block, even after one re-ask: plan.json holds no questions.");
281
+ }
282
+ if (planText.trim()) {
283
+ outputsWritten =
284
+ writeOut(PLAN_MARKDOWN_FILE_NAME, planMarkdown(planText)) &&
285
+ writeOut(PLAN_JSON_FILE_NAME, `${JSON.stringify(plan.file, null, 2)}\n`);
286
+ }
287
+ }
288
+ else {
289
+ const hits = await c.scan().catch(() => null);
290
+ if (hits === null) {
291
+ notes.push("The secret and binary scan could not read the change before the commit; the host still scans it.");
292
+ }
293
+ else {
294
+ extra.push(...hits);
295
+ }
296
+ if (c.reverify && c.lastVerify?.tree !== (await c.tree())) {
297
+ try {
298
+ const again = await c.reverify();
299
+ if (again === null) {
300
+ notes.push("The checks were not run on the final tree: the run had no time left for them.");
301
+ }
302
+ else {
303
+ checks = again;
304
+ extra.push(...again.findings);
305
+ if (again.reason)
306
+ notes.push(again.reason);
307
+ }
308
+ }
309
+ catch (err) {
310
+ notes.push(`The checks could not run on the final tree: ${err instanceof Error ? err.message : String(err)}`);
311
+ }
312
+ }
313
+ // The loop judged the tree it last verified; what the final scan or checks
314
+ // found blocking in the tree being committed means it did not stop clean.
315
+ if (stop === "clean" && extra.some(isBlockingFinding)) {
316
+ stop = "failed";
317
+ notes.push("The review loop stopped clean, but the final scan or checks found a blocker in what is committed.");
318
+ }
319
+ const title = c.plan ? planTitle(c.plan) : undefined;
320
+ const finish = await runFinish({
321
+ cwd: c.cwd,
322
+ worktreeMode: true,
323
+ hereBaseline: "unknown",
324
+ ticket: ticketOfBranch(branch) ?? c.identity.attemptId,
325
+ ...(c.plan ? { planText: c.plan } : {}),
326
+ ...(title ? { ticketBrief: title } : {}),
327
+ runId: c.identity.attemptId,
328
+ verify: verifyTrailerValue({
329
+ stopReason: stop,
330
+ ...(checks?.ran && checks.command ? { verifyCommand: checks.command } : {}),
331
+ ...(checks?.reason ? { verifyNote: checks.reason } : {}),
332
+ }),
333
+ decisionsRecorded: result?.decisionsRecorded ?? 0,
334
+ rounds: result?.rounds.length ?? 0,
335
+ openPr: false,
336
+ ...(c.preRunDirt.length > 0 ? { baselinePaths: c.preRunDirt } : {}),
337
+ exec: c.exec,
338
+ });
339
+ committed = finish.committed;
340
+ notes.push(...finish.notes);
341
+ }
342
+ let delivered = outputsWritten;
343
+ if (c.stage !== "plan") {
344
+ const onBranch = committed || (await attemptCommitted(c.cwd, c.exec, c.identity.attemptId));
345
+ if (onBranch === null) {
346
+ notes.push("git log failed, so the result cannot say whether the branch carries this attempt's work.");
347
+ }
348
+ delivered = onBranch === true;
349
+ }
350
+ const facts = await gitFacts(c.cwd, c.exec, c.baseSha);
351
+ notes.push(...facts.notes);
352
+ // What the final scan and checks found beside the last round's findings; a
353
+ // finding both report is listed once.
354
+ const seen = new Set((result?.findings ?? []).map(findingKey));
355
+ const findings = [...(result?.findings ?? [])];
356
+ for (const finding of extra) {
357
+ if (seen.has(findingKey(finding)))
358
+ continue;
359
+ seen.add(findingKey(finding));
360
+ findings.push(finding);
361
+ }
362
+ const attempt = {
363
+ version: 1,
364
+ ...c.identity,
365
+ stage: c.stage,
366
+ headSha: c.stage === "plan" ? c.baseSha : facts.headSha,
367
+ branch,
368
+ baseSha: c.baseSha,
369
+ stopReason: outputsWritten ? resultStopReason(stop) : "failed",
370
+ rounds: result?.rounds.at(-1)?.round ?? c.roundsDone,
371
+ checks: checks?.checks ?? [],
372
+ findingsOpen: openFindings(findings),
373
+ filesChanged: c.stage === "plan" ? [] : facts.filesChanged,
374
+ commits: c.stage === "plan" ? [] : facts.commits,
375
+ notes,
376
+ blockingKey: blockingFindingsKey(findings),
377
+ usage: resultUsage(result ?? { stages: "partial" in ran ? ran.partial : [], rounds: [] }),
378
+ };
379
+ const resultWritten = writeOut(RESULT_FILE_NAME, `${JSON.stringify(attempt, null, 2)}\n`);
380
+ return { attempt, ok: resultWritten && attempt.stopReason === "clean" && delivered };
381
+ }
382
+ //# sourceMappingURL=cloud.js.map
@@ -46,16 +46,20 @@ export declare function continuesInLoop(continuation: ContinuationInputs): boole
46
46
  * continuation has no summary, so it says where the change is instead and pins
47
47
  * the same design context.
48
48
  */
49
- export declare function continuationReviewInput(continuation: ContinuationInputs, mission: MissionInputs | undefined): string;
49
+ export declare function continuationReviewInput(continuation: ContinuationInputs, mission: MissionInputs | undefined, committed?: boolean): string;
50
50
  /**
51
51
  * The resume plan that re-enters the review→fix loop for a continuation that
52
52
  * stopped in review, fix or verify; undefined when implement must run again.
53
- * The round continues the earlier attempt's count, capped at the last round so
54
- * a continuation always gets one review.
53
+ * The round continues the earlier attempt's count, capped at the lane's last
54
+ * round so a continuation always gets one review. `committed` is the cloud
55
+ * lane's resume, whose earlier work is on the branch.
55
56
  */
56
57
  export declare function continuationResumePlan(continuation: ContinuationInputs, ctx: {
57
58
  ticket: string;
58
59
  runId?: string;
59
60
  mission?: MissionInputs;
61
+ }, opts?: {
62
+ maxRounds?: number;
63
+ committed?: boolean;
60
64
  }): ResumePlan | undefined;
61
65
  //# sourceMappingURL=continuation.d.ts.map