@yagni-app/code-staging 1.2.13-staging.2176.1 → 1.2.13-staging.2196.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.
package/dist/cli.js CHANGED
@@ -32,6 +32,7 @@ import { buildLaunch } from "./launch.js";
32
32
  import { resolveOtelLaunchWithWorkspace } from "./otel.js";
33
33
  import { parseOutputFormat, parseJsonEvents, buildResultObject, readGuardianEvents, } from "./outputFormat.js";
34
34
  import { feedbackCommand } from "./feedback.js";
35
+ import { envCommand } from "./env.js";
35
36
  import { runDoctor } from "./doctor.js";
36
37
  import { installProcessCrashHandlers } from "./crashReport.js";
37
38
  import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
@@ -705,6 +706,10 @@ export const HELP_TEXT = [
705
706
  " add, install, uninstall, update, list — run",
706
707
  " `yagni plugin` for details. Skills, commands,",
707
708
  " agents, and bundled MCP servers load on launch.",
709
+ " yagni env check [--repo owner/name] Validate the repository's environment",
710
+ " as it stands, without repair, printing each step.",
711
+ " The repository defaults to the origin remote.",
712
+ " Exits 0 ready, 1 needs attention, 2 request error.",
708
713
  " yagni token Output the active environment's API token (for helpers).",
709
714
  " yagni use <name> Switch the active environment (sticky).",
710
715
  " Presets: prod, local. Others need --base-url <url>.",
@@ -866,6 +871,9 @@ export async function main(argv) {
866
871
  if (command === "feedback") {
867
872
  return feedbackCommand(rest, {}, cliVersion());
868
873
  }
874
+ if (command === "env") {
875
+ return envCommand(rest, {}, cliVersion());
876
+ }
869
877
  if (command === "token") {
870
878
  return tokenCommand({}, rest);
871
879
  }
package/dist/env.d.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * `yagni env check [--repo owner/name]` (YAG-1109): validate a repository's
3
+ * environment as it stands, without repair, and print each step as it runs.
4
+ *
5
+ * The repository defaults to the current directory's `origin` remote. The
6
+ * command starts the check (`POST /api/yagni-code/environment/check`), then
7
+ * polls the run (`GET /api/yagni-code/environment/runs/:revisionId`) every
8
+ * 3 s. Exit codes: 0 ready, 1 needs attention, 2 a request or usage error.
9
+ *
10
+ * The remote parser, the step formatter and the poll loop are pure, with
11
+ * `fetch` and `sleep` injected, so they test without a network.
12
+ */
13
+ export type EnvRunStatus = "validating" | "ready" | "needs_attention";
14
+ export type EnvStepState = "pending" | "running" | "done" | "failed" | "skipped";
15
+ /** One step of the run, as the server returns it (`QaRunStep`). */
16
+ export interface EnvRunStep {
17
+ index: number;
18
+ kind?: string;
19
+ label: string;
20
+ path?: string;
21
+ line?: number;
22
+ state: EnvStepState;
23
+ durationMs?: number;
24
+ exitCode?: number;
25
+ outputTail?: string;
26
+ }
27
+ export interface EnvRun {
28
+ status: EnvRunStatus;
29
+ message: string;
30
+ steps: EnvRunStep[];
31
+ notes: string[];
32
+ outputTail?: string;
33
+ /** The declared system the run validated in, or why it ran alone. */
34
+ system?: EnvRunSystem;
35
+ }
36
+ export interface EnvRunSystem {
37
+ name: string;
38
+ applied?: boolean;
39
+ reason?: "not_member" | "unreadable" | "missing";
40
+ }
41
+ /** `in system gpx`, or `ran alone: <why>` when the system was not applied. */
42
+ export declare function formatSystem(system: EnvRunSystem): string;
43
+ export declare const ENV_POLL_INTERVAL_MS = 3000;
44
+ export declare const ENV_USAGE: string;
45
+ /**
46
+ * `owner/name` from a git remote URL: https (`https://github.com/o/n.git`),
47
+ * ssh (`ssh://git@github.com/o/n`) or scp-like (`git@github.com:o/n.git`).
48
+ * The `.git` suffix and a trailing slash are optional. Undefined otherwise.
49
+ */
50
+ export declare function parseRemoteRepo(url: string): string | undefined;
51
+ /**
52
+ * A remote URL safe to print: the username and password (a token, often)
53
+ * are dropped from a URL remote, and the user from an scp-like one.
54
+ */
55
+ export declare function redactRemote(url: string): string;
56
+ /** A validated `owner/name` given on the command line, `.git` allowed. */
57
+ export declare function parseRepoArg(raw: string): string | undefined;
58
+ /**
59
+ * A step's time as a person reads it: `41 s`, `11 m 20 s`, `2 h 5 m`. Mirrors
60
+ * `qaStepDuration` in `@yagni/shared` (`packages/shared/src/qa.ts`), which this
61
+ * self-contained package cannot import; keep the two in step.
62
+ */
63
+ export declare function formatDuration(ms: number): string;
64
+ /**
65
+ * The lines one step prints when it reaches a state. `pending` prints
66
+ * nothing; a failed step prints its output tail, indented.
67
+ */
68
+ export declare function formatStep(step: EnvRunStep): string[];
69
+ /**
70
+ * The lines this read adds: every step whose state moved since the last
71
+ * read. `seen` is updated in place.
72
+ */
73
+ export declare function stepTransitions(steps: EnvRunStep[], seen: Map<number, EnvStepState>): string[];
74
+ export interface EnvRequest {
75
+ baseUrl: string;
76
+ token: string;
77
+ fetchImpl: typeof fetch;
78
+ }
79
+ /** Start the check: the run's revision id, or the reason it did not start. */
80
+ export declare function startCheck(req: EnvRequest, repo: string): Promise<{
81
+ revisionId: string;
82
+ } | {
83
+ error: string;
84
+ }>;
85
+ export interface PollOptions extends EnvRequest {
86
+ revisionId: string;
87
+ sleep: (ms: number) => Promise<void>;
88
+ writeOut: (line: string) => void;
89
+ writeErr: (line: string) => void;
90
+ intervalMs?: number;
91
+ }
92
+ /** Poll the run until it settles; the command's exit code. */
93
+ export declare function pollRun(opts: PollOptions): Promise<number>;
94
+ export interface EnvDeps {
95
+ loadCredentials?: () => Promise<{
96
+ token?: string;
97
+ baseUrl: string;
98
+ name: string;
99
+ }>;
100
+ fetchImpl?: typeof fetch;
101
+ sleep?: (ms: number) => Promise<void>;
102
+ /** The current directory's `origin` remote URL, or undefined. */
103
+ originUrl?: (cwd: string) => Promise<string | undefined>;
104
+ cwd?: string;
105
+ env?: NodeJS.ProcessEnv;
106
+ writeOut?: (line: string) => void;
107
+ writeErr?: (line: string) => void;
108
+ }
109
+ /** `--repo owner/name` or `--repo=owner/name`; `error` names a bad argument. */
110
+ export declare function parseEnvCheckArgs(args: string[]): {
111
+ repo?: string;
112
+ error?: string;
113
+ };
114
+ /** `yagni env <subcommand>`; only `check` exists. */
115
+ export declare function envCommand(args: string[], deps?: EnvDeps, _cliVersion?: string): Promise<number>;
116
+ //# sourceMappingURL=env.d.ts.map
package/dist/env.js ADDED
@@ -0,0 +1,332 @@
1
+ /**
2
+ * `yagni env check [--repo owner/name]` (YAG-1109): validate a repository's
3
+ * environment as it stands, without repair, and print each step as it runs.
4
+ *
5
+ * The repository defaults to the current directory's `origin` remote. The
6
+ * command starts the check (`POST /api/yagni-code/environment/check`), then
7
+ * polls the run (`GET /api/yagni-code/environment/runs/:revisionId`) every
8
+ * 3 s. Exit codes: 0 ready, 1 needs attention, 2 a request or usage error.
9
+ *
10
+ * The remote parser, the step formatter and the poll loop are pure, with
11
+ * `fetch` and `sleep` injected, so they test without a network.
12
+ */
13
+ import { execFile } from "node:child_process";
14
+ import { credentialsFromProfile, readActiveProfile } from "./profiles.js";
15
+ /** `in system gpx`, or `ran alone: <why>` when the system was not applied. */
16
+ export function formatSystem(system) {
17
+ if (system.applied !== false)
18
+ return `in system ${system.name}`;
19
+ const why = system.reason === "not_member"
20
+ ? `not a member of system ${system.name}`
21
+ : system.reason === "unreadable"
22
+ ? `system ${system.name} could not be read`
23
+ : system.reason === "missing"
24
+ ? `system ${system.name} no longer exists`
25
+ : `system ${system.name} was not applied`;
26
+ return `ran alone: ${why}`;
27
+ }
28
+ export const ENV_POLL_INTERVAL_MS = 3_000;
29
+ /** Consecutive failed reads of the run before the command gives up. */
30
+ const MAX_READ_FAILURES = 3;
31
+ export const ENV_USAGE = [
32
+ "Usage: yagni env check [--repo owner/name]",
33
+ "",
34
+ "Validates the repository's environment as it stands, without repair.",
35
+ "The repository defaults to this directory's origin remote.",
36
+ "Exits 0 when ready, 1 when it needs attention, 2 on a request error.",
37
+ ].join("\n");
38
+ /**
39
+ * `owner/name` from a git remote URL: https (`https://github.com/o/n.git`),
40
+ * ssh (`ssh://git@github.com/o/n`) or scp-like (`git@github.com:o/n.git`).
41
+ * The `.git` suffix and a trailing slash are optional. Undefined otherwise.
42
+ */
43
+ export function parseRemoteRepo(url) {
44
+ const trimmed = url
45
+ .trim()
46
+ .replace(/\/+$/, "")
47
+ .replace(/\.git$/, "");
48
+ let path;
49
+ if (trimmed.includes("://")) {
50
+ try {
51
+ path = new URL(trimmed).pathname;
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ else {
58
+ const scp = /^(?:[^@/\s]+@)?[^:/\s]+:(.+)$/.exec(trimmed);
59
+ if (!scp)
60
+ return undefined;
61
+ path = scp[1];
62
+ }
63
+ // Exactly owner and name: a longer path (an Azure DevOps `_git` path, a
64
+ // nested group) is never guessed at.
65
+ const segments = path.split("/").filter(Boolean);
66
+ if (segments.length !== 2)
67
+ return undefined;
68
+ const [owner, name] = segments;
69
+ const part = /^[A-Za-z0-9._-]+$/;
70
+ return part.test(owner) && part.test(name) ? `${owner}/${name}` : undefined;
71
+ }
72
+ /**
73
+ * A remote URL safe to print: the username and password (a token, often)
74
+ * are dropped from a URL remote, and the user from an scp-like one.
75
+ */
76
+ export function redactRemote(url) {
77
+ const trimmed = url.trim();
78
+ if (trimmed.includes("://")) {
79
+ try {
80
+ const parsed = new URL(trimmed);
81
+ parsed.username = "";
82
+ parsed.password = "";
83
+ return parsed.toString();
84
+ }
85
+ catch {
86
+ return trimmed.replace(/^([^:/]+:\/\/)[^/]*@/, "$1");
87
+ }
88
+ }
89
+ return trimmed.replace(/^[^/\s]*@/, "");
90
+ }
91
+ /** A validated `owner/name` given on the command line, `.git` allowed. */
92
+ export function parseRepoArg(raw) {
93
+ const repo = raw.trim().replace(/\.git$/, "");
94
+ return /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo) ? repo : undefined;
95
+ }
96
+ /**
97
+ * A step's time as a person reads it: `41 s`, `11 m 20 s`, `2 h 5 m`. Mirrors
98
+ * `qaStepDuration` in `@yagni/shared` (`packages/shared/src/qa.ts`), which this
99
+ * self-contained package cannot import; keep the two in step.
100
+ */
101
+ export function formatDuration(ms) {
102
+ const seconds = Math.max(0, Math.round(ms / 1000));
103
+ if (seconds < 60)
104
+ return `${seconds} s`;
105
+ const minutes = Math.floor(seconds / 60);
106
+ if (minutes < 60)
107
+ return seconds % 60 ? `${minutes} m ${seconds % 60} s` : `${minutes} m`;
108
+ const hours = Math.floor(minutes / 60);
109
+ return minutes % 60 ? `${hours} h ${minutes % 60} m` : `${hours} h`;
110
+ }
111
+ /**
112
+ * The lines one step prints when it reaches a state. `pending` prints
113
+ * nothing; a failed step prints its output tail, indented.
114
+ */
115
+ export function formatStep(step) {
116
+ const line = step.line !== undefined ? ` (line ${step.line})` : "";
117
+ const took = step.durationMs !== undefined ? ` ${formatDuration(step.durationMs)}` : "";
118
+ switch (step.state) {
119
+ case "running":
120
+ return [`… ${step.label}`];
121
+ case "done":
122
+ return [`✓ ${step.label}${took}`];
123
+ case "skipped":
124
+ return [`- ${step.label} skipped`];
125
+ case "failed": {
126
+ const tail = (step.outputTail ?? "")
127
+ .split("\n")
128
+ .filter((l) => l.trim().length > 0)
129
+ .map((l) => ` ${l}`);
130
+ return [`✗ ${step.label}${line}${took}`, ...tail];
131
+ }
132
+ default:
133
+ return [];
134
+ }
135
+ }
136
+ /**
137
+ * The lines this read adds: every step whose state moved since the last
138
+ * read. `seen` is updated in place.
139
+ */
140
+ export function stepTransitions(steps, seen) {
141
+ const out = [];
142
+ for (const step of [...steps].sort((a, b) => a.index - b.index)) {
143
+ if (seen.get(step.index) === step.state)
144
+ continue;
145
+ seen.set(step.index, step.state);
146
+ out.push(...formatStep(step));
147
+ }
148
+ return out;
149
+ }
150
+ function apiUrl(baseUrl, path) {
151
+ return `${baseUrl.replace(/\/$/, "")}/api/yagni-code${path}`;
152
+ }
153
+ async function errorText(res) {
154
+ const body = (await res.json().catch(() => ({})));
155
+ const detail = typeof body.error === "string" ? body.error : "";
156
+ if (res.status === 401)
157
+ return "Your login has expired. Run `yagni login` and try again.";
158
+ return detail ? `${detail} (HTTP ${res.status})` : `The server answered HTTP ${res.status}.`;
159
+ }
160
+ /** Start the check: the run's revision id, or the reason it did not start. */
161
+ export async function startCheck(req, repo) {
162
+ try {
163
+ const res = await req.fetchImpl(apiUrl(req.baseUrl, "/environment/check"), {
164
+ method: "POST",
165
+ headers: { "content-type": "application/json", authorization: `Bearer ${req.token}` },
166
+ body: JSON.stringify({ repo }),
167
+ signal: AbortSignal.timeout(60_000),
168
+ });
169
+ if (!res.ok)
170
+ return { error: await errorText(res) };
171
+ const body = (await res.json());
172
+ return typeof body.revisionId === "string"
173
+ ? { revisionId: body.revisionId }
174
+ : { error: "The server did not say which run started." };
175
+ }
176
+ catch (err) {
177
+ return { error: err instanceof Error ? err.message : String(err) };
178
+ }
179
+ }
180
+ async function readRun(req, revisionId) {
181
+ try {
182
+ const res = await req.fetchImpl(apiUrl(req.baseUrl, `/environment/runs/${encodeURIComponent(revisionId)}`), {
183
+ headers: { authorization: `Bearer ${req.token}` },
184
+ signal: AbortSignal.timeout(30_000),
185
+ });
186
+ if (!res.ok)
187
+ return { error: await errorText(res) };
188
+ const body = (await res.json());
189
+ return {
190
+ status: body.status ?? "validating",
191
+ message: body.message ?? "",
192
+ steps: Array.isArray(body.steps) ? body.steps : [],
193
+ notes: Array.isArray(body.notes) ? body.notes : [],
194
+ ...(body.outputTail ? { outputTail: body.outputTail } : {}),
195
+ ...(body.system && typeof body.system.name === "string" ? { system: body.system } : {}),
196
+ };
197
+ }
198
+ catch (err) {
199
+ return { error: err instanceof Error ? err.message : String(err) };
200
+ }
201
+ }
202
+ /** Poll the run until it settles; the command's exit code. */
203
+ export async function pollRun(opts) {
204
+ const seen = new Map();
205
+ let failures = 0;
206
+ let systemSaid = false;
207
+ for (;;) {
208
+ const run = await readRun(opts, opts.revisionId);
209
+ if ("error" in run) {
210
+ failures += 1;
211
+ if (failures >= MAX_READ_FAILURES) {
212
+ opts.writeErr(`Could not read the run: ${run.error}`);
213
+ return 2;
214
+ }
215
+ }
216
+ else {
217
+ failures = 0;
218
+ if (run.system && !systemSaid) {
219
+ systemSaid = true;
220
+ opts.writeOut(formatSystem(run.system));
221
+ }
222
+ for (const line of stepTransitions(run.steps, seen))
223
+ opts.writeOut(line);
224
+ if (run.status !== "validating") {
225
+ const failedTail = run.steps.some((s) => s.state === "failed" && s.outputTail);
226
+ if (run.status === "needs_attention" && run.outputTail && !failedTail) {
227
+ for (const line of run.outputTail.split("\n").filter((l) => l.trim()))
228
+ opts.writeOut(` ${line}`);
229
+ }
230
+ for (const note of run.notes)
231
+ opts.writeOut(`note: ${note}`);
232
+ if (run.status === "ready") {
233
+ opts.writeOut(run.message ? `Ready. ${run.message}` : "Ready.");
234
+ return 0;
235
+ }
236
+ opts.writeOut(run.message ? `Needs attention: ${run.message}` : "Needs attention.");
237
+ return 1;
238
+ }
239
+ }
240
+ await opts.sleep(opts.intervalMs ?? ENV_POLL_INTERVAL_MS);
241
+ }
242
+ }
243
+ function gitOriginUrl(cwd) {
244
+ return new Promise((resolve) => {
245
+ execFile("git", ["remote", "get-url", "origin"], { cwd, timeout: 10_000 }, (err, stdout) => resolve(err ? undefined : stdout.trim() || undefined));
246
+ });
247
+ }
248
+ /** `--repo owner/name` or `--repo=owner/name`; `error` names a bad argument. */
249
+ export function parseEnvCheckArgs(args) {
250
+ let repo;
251
+ for (let i = 0; i < args.length; i++) {
252
+ const a = args[i];
253
+ if (a === "--repo") {
254
+ const value = args[++i];
255
+ if (!value)
256
+ return { error: "--repo needs owner/name" };
257
+ repo = value;
258
+ }
259
+ else if (a.startsWith("--repo=")) {
260
+ repo = a.slice("--repo=".length);
261
+ }
262
+ else {
263
+ return { error: `Unknown argument: ${a}` };
264
+ }
265
+ }
266
+ if (repo === undefined)
267
+ return {};
268
+ const parsed = parseRepoArg(repo);
269
+ return parsed ? { repo: parsed } : { error: `Not a repository: ${repo} (expected owner/name)` };
270
+ }
271
+ /** `yagni env <subcommand>`; only `check` exists. */
272
+ export async function envCommand(args, deps = {}, _cliVersion) {
273
+ const writeOut = deps.writeOut ?? ((line) => void process.stdout.write(`${line}\n`));
274
+ const writeErr = deps.writeErr ?? ((line) => void process.stderr.write(`${line}\n`));
275
+ const [sub, ...rest] = args;
276
+ if (sub !== "check") {
277
+ (sub === undefined ? writeOut : writeErr)(ENV_USAGE);
278
+ return sub === undefined ? 0 : 2;
279
+ }
280
+ const parsed = parseEnvCheckArgs(rest);
281
+ if (parsed.error) {
282
+ writeErr(parsed.error);
283
+ writeErr(ENV_USAGE);
284
+ return 2;
285
+ }
286
+ const cwd = deps.cwd ?? process.cwd();
287
+ let repo = parsed.repo;
288
+ if (!repo) {
289
+ const url = await (deps.originUrl ?? gitOriginUrl)(cwd);
290
+ repo = url ? parseRemoteRepo(url) : undefined;
291
+ if (!repo) {
292
+ writeErr(url
293
+ ? `Could not read the repository from the origin remote (${redactRemote(url)}). Pass --repo owner/name.`
294
+ : "This directory has no origin remote. Pass --repo owner/name.");
295
+ return 2;
296
+ }
297
+ }
298
+ const env = deps.env ?? process.env;
299
+ const profile = await (deps.loadCredentials ??
300
+ (async () => {
301
+ const active = await readActiveProfile(env);
302
+ const creds = credentialsFromProfile(active);
303
+ return {
304
+ name: active.name,
305
+ baseUrl: active.baseUrl,
306
+ ...(creds?.token ? { token: creds.token } : {}),
307
+ };
308
+ }))();
309
+ if (!profile.token) {
310
+ writeErr(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`yagni login\` first.`);
311
+ return 2;
312
+ }
313
+ const req = {
314
+ baseUrl: profile.baseUrl,
315
+ token: profile.token,
316
+ fetchImpl: deps.fetchImpl ?? fetch,
317
+ };
318
+ const started = await startCheck(req, repo);
319
+ if ("error" in started) {
320
+ writeErr(`Could not start the check for ${repo}: ${started.error}`);
321
+ return 2;
322
+ }
323
+ writeOut(`Validating ${repo} as it stands, without repair.`);
324
+ return pollRun({
325
+ ...req,
326
+ revisionId: started.revisionId,
327
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
328
+ writeOut,
329
+ writeErr,
330
+ });
331
+ }
332
+ //# sourceMappingURL=env.js.map
@@ -979,7 +979,11 @@ export async function runPipeline(ticket, deps) {
979
979
  reviewInput: written.finalOutput,
980
980
  usage: currentUsage(),
981
981
  }));
982
- return done(open.length > 0 ? "round_cap" : "clean", open);
982
+ // The review loop's own rule (`shouldStop`): a cap is spent only on a
983
+ // finding that blocks. Advisory findings left when the fix turns ran out
984
+ // travel with the result for the pull request to name, never a stop
985
+ // (ENG-3028, YAG-1073).
986
+ return done(open.some(isBlockingFinding) ? "round_cap" : "clean", open);
983
987
  };
984
988
  const fixRound = !resume && deps.fixRound && !deps.fixRound.replan ? deps.fixRound : undefined;
985
989
  if (deps.fixRound?.replan)
@@ -50,8 +50,14 @@ export declare function reviewStage(): PipelineStage;
50
50
  export declare function selectStages(_lane?: "full"): PipelineStage[];
51
51
  /** Interactive /go and mission builds: the diamond, three lenses, two rounds. */
52
52
  export declare const FULL_LANE: PipelineLane;
53
- /** The cloud lane's cap: one reviewer per round, so more rounds than the full lane. */
54
- export declare const CLOUD_MAX_REVIEW_ROUNDS = 4;
53
+ /**
54
+ * The cloud lane's cap: one reviewer per round, so more rounds than the full
55
+ * lane. Five since YAG-1106 (promised to GPX on 2026-10-30): a cap never
56
+ * ends a build without its pull request, so the rounds past the fourth buy
57
+ * one more fix at a blocking finding before the pull request names what is
58
+ * still open. Never uncapped: each round is peak-tier output tokens.
59
+ */
60
+ export declare const CLOUD_MAX_REVIEW_ROUNDS = 5;
55
61
  /** The tool every cloud plan stage calls for the decisions a person already made on this repository. */
56
62
  export declare const DECISIONS_READ_TOOL = "yagni_decisions_read_for_repository";
57
63
  /**
@@ -134,8 +134,14 @@ export const FULL_LANE = {
134
134
  stopAfterPlan: false,
135
135
  skillsEveryStage: false,
136
136
  };
137
- /** The cloud lane's cap: one reviewer per round, so more rounds than the full lane. */
138
- export const CLOUD_MAX_REVIEW_ROUNDS = 4;
137
+ /**
138
+ * The cloud lane's cap: one reviewer per round, so more rounds than the full
139
+ * lane. Five since YAG-1106 (promised to GPX on 2026-10-30): a cap never
140
+ * ends a build without its pull request, so the rounds past the fourth buy
141
+ * one more fix at a blocking finding before the pull request names what is
142
+ * still open. Never uncapped: each round is peak-tier output tokens.
143
+ */
144
+ export const CLOUD_MAX_REVIEW_ROUNDS = 5;
139
145
  /** The tool every cloud plan stage calls for the decisions a person already made on this repository. */
140
146
  export const DECISIONS_READ_TOOL = "yagni_decisions_read_for_repository";
141
147
  /**
@@ -551,13 +551,17 @@ export type PipelineProgress = {
551
551
  * - Review rounds are peak-tier output tokens, the most expensive tokens a
552
552
  * build buys, and the in-sandbox review is not the last reviewer: Proctor
553
553
  * reviews the pull request and its fix rounds act on what it finds.
554
- * - A third round never helped: every build that reached it ended with as many
555
- * findings or more, and hit the cap anyway.
556
- * - Round two runs only when round one found a blocker (`shouldStop`); what is
557
- * still open after the cap is written into the pull request body under
558
- * "Left for review" for Proctor, never a third round.
559
- * This is the `full` lane's cap. The cloud lane carries its own
560
- * (`PipelineLane.maxReviewRounds`): one reviewer per round, so four rounds.
554
+ * - The cap is a bound on spend, never a verdict: a build that hits it still
555
+ * opens its pull request, with what its own review left open named under
556
+ * "Left for review" for Proctor and the person reviewing (YAG-1106). A cap
557
+ * that ended a build with no pull request was the worst outcome a ticket
558
+ * could have (ENG-3028).
559
+ * - A round past the first runs only while a blocking finding is open
560
+ * (`shouldStop`, and the fix-mode path alike); nits, needs-person and
561
+ * advisory findings never spend a round and never stop a build.
562
+ * This is the `full` lane's cap, two rounds. The cloud lane carries its own
563
+ * (`PipelineLane.maxReviewRounds`): one reviewer per round, so five rounds
564
+ * (`CLOUD_MAX_REVIEW_ROUNDS`), never uncapped.
561
565
  */
562
566
  export declare const MAX_REVIEW_ROUNDS = 2;
563
567
  /**
@@ -12,13 +12,17 @@
12
12
  * - Review rounds are peak-tier output tokens, the most expensive tokens a
13
13
  * build buys, and the in-sandbox review is not the last reviewer: Proctor
14
14
  * reviews the pull request and its fix rounds act on what it finds.
15
- * - A third round never helped: every build that reached it ended with as many
16
- * findings or more, and hit the cap anyway.
17
- * - Round two runs only when round one found a blocker (`shouldStop`); what is
18
- * still open after the cap is written into the pull request body under
19
- * "Left for review" for Proctor, never a third round.
20
- * This is the `full` lane's cap. The cloud lane carries its own
21
- * (`PipelineLane.maxReviewRounds`): one reviewer per round, so four rounds.
15
+ * - The cap is a bound on spend, never a verdict: a build that hits it still
16
+ * opens its pull request, with what its own review left open named under
17
+ * "Left for review" for Proctor and the person reviewing (YAG-1106). A cap
18
+ * that ended a build with no pull request was the worst outcome a ticket
19
+ * could have (ENG-3028).
20
+ * - A round past the first runs only while a blocking finding is open
21
+ * (`shouldStop`, and the fix-mode path alike); nits, needs-person and
22
+ * advisory findings never spend a round and never stop a build.
23
+ * This is the `full` lane's cap, two rounds. The cloud lane carries its own
24
+ * (`PipelineLane.maxReviewRounds`): one reviewer per round, so five rounds
25
+ * (`CLOUD_MAX_REVIEW_ROUNDS`), never uncapped.
22
26
  */
23
27
  export const MAX_REVIEW_ROUNDS = 2;
24
28
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.2.13-staging.2176.1",
3
+ "version": "1.2.13-staging.2196.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -58,5 +58,5 @@
58
58
  "turndown": "^7.2.4",
59
59
  "typebox": "^1.3.15"
60
60
  },
61
- "yagniSourceSha": "aae5dcff2b6e88c794f1213893df2c24ec7be19e"
61
+ "yagniSourceSha": "c0c70b535de32727cd574cd8c4eedc460601ff76"
62
62
  }