@yagni-app/code-staging 1.2.7-staging.2069.1 → 1.2.8-staging.2076.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.
@@ -16,7 +16,7 @@
16
16
  * lives (`packages/shared/src/attempts.ts`): the two meet only at this JSON.
17
17
  */
18
18
  import type { ContinuationStage } from "./continuation.js";
19
- import type { VerifyCheckRun, VerifyOutcome } from "./verify.js";
19
+ import { type VerifyCheckRun, type VerifyOutcome } from "./verify.js";
20
20
  import type { WorktreeExec } from "./worktree.js";
21
21
  import type { Finding, PipelineLane, PipelineResult, StopReason } from "./types.js";
22
22
  /** Set on every stage child of a cloud session, so its prompt describes the cloud sandbox. */
@@ -152,6 +152,15 @@ export declare function ticketOfBranch(branch: string | null): string | undefine
152
152
  export declare function attemptCommitted(cwd: string, exec: WorktreeExec, attemptId: string): Promise<boolean | null>;
153
153
  /** Read the execution's work off git. Never throws: a read that fails is a note. */
154
154
  export declare function gitFacts(cwd: string, exec: WorktreeExec, baseSha: string): Promise<GitFacts>;
155
+ /**
156
+ * The checks that gave no verdict because the sandbox could not run them,
157
+ * each with why, as one sentence for the person who reads the result: no
158
+ * change to the code makes them run, so the loop never held the change on
159
+ * them. A check only not run on this tree (its budget spent, the build
160
+ * stopped first, its note {@link CHECK_NOT_RUN_NOTE}) is not one: another run
161
+ * could give it a verdict. Null when no check could not run. Pure.
162
+ */
163
+ export declare function cannotRunNote(checks: readonly VerifyCheckRun[]): string | null;
155
164
  /** What the session's last act needs, gathered once the pipeline has stopped. */
156
165
  export interface CloudCompletion {
157
166
  identity: CloudIdentity;
@@ -22,6 +22,7 @@ import { planTitle, runFinish, verifyTrailerValue } from "./finish.js";
22
22
  import { buildPlanFile, planMarkdown } from "./planOutput.js";
23
23
  import { cloudLane } from "./stages.js";
24
24
  import { applyStageTiers, parseStageTiers, STAGE_TIERS_ENV } from "./tierCap.js";
25
+ import { CHECK_NOT_RUN_NOTE } from "./verify.js";
25
26
  /** Set on every stage child of a cloud session, so its prompt describes the cloud sandbox. */
26
27
  export const CLOUD_SESSION_ENV = "YAGNI_GO_CLOUD";
27
28
  /** Where the session writes `plan.md`, `plan.json` and `result.json`. */
@@ -235,6 +236,32 @@ export async function gitFacts(cwd, exec, baseSha) {
235
236
  notes,
236
237
  };
237
238
  }
239
+ /** Checks the cannot-run note names before "and N more". */
240
+ const MAX_NAMED_CANNOT_RUN = 5;
241
+ /**
242
+ * The checks that gave no verdict because the sandbox could not run them,
243
+ * each with why, as one sentence for the person who reads the result: no
244
+ * change to the code makes them run, so the loop never held the change on
245
+ * them. A check only not run on this tree (its budget spent, the build
246
+ * stopped first, its note {@link CHECK_NOT_RUN_NOTE}) is not one: another run
247
+ * could give it a verdict. Null when no check could not run. Pure.
248
+ */
249
+ export function cannotRunNote(checks) {
250
+ const unrun = checks.filter((check) => check.baseline === "cannot_run" &&
251
+ check.exitCode === null &&
252
+ !(check.note ?? "").startsWith(CHECK_NOT_RUN_NOTE));
253
+ if (unrun.length === 0)
254
+ return null;
255
+ const named = unrun.slice(0, MAX_NAMED_CANNOT_RUN).map((check) => {
256
+ const cause = (check.note ?? "")
257
+ .split("; `")[0]
258
+ .replace(/^could not run(?: in the sandbox)?:?\s*/, "")
259
+ .trim();
260
+ return `\`${check.name}\` (${cause || "could not run"})`;
261
+ });
262
+ const more = unrun.length - named.length;
263
+ return `No change to the code can make ${unrun.length === 1 ? "this check" : "these checks"} run here, so ${unrun.length === 1 ? "it was" : "they were"} never a finding to fix and gave no verdict on this change: ${named.join("; ")}${more > 0 ? `; and ${more} more` : ""}.`;
264
+ }
238
265
  /** A finding as the result dedupes it: where it is and what it says. */
239
266
  function findingKey(finding) {
240
267
  return `${finding.file ?? ""}:${finding.line ?? ""}:${finding.message}`;
@@ -312,6 +339,9 @@ export async function completeCloudSession(c, ran) {
312
339
  notes.push(`The checks could not run on the final tree: ${err instanceof Error ? err.message : String(err)}`);
313
340
  }
314
341
  }
342
+ const unrun = cannotRunNote(checks?.checks ?? []);
343
+ if (unrun)
344
+ notes.push(unrun);
315
345
  // The loop judged the tree it last verified; what the final scan or checks
316
346
  // found blocking in the tree being committed means it did not stop clean.
317
347
  if (stop === "clean" && extra.some(isBlockingFinding)) {
@@ -10,9 +10,10 @@
10
10
  * run's own change (an implement that only confirms finished work is not
11
11
  * `no_changes`, and the verify gate scopes to the whole diff against HEAD);
12
12
  * - an attempt that stopped in review, fix or verify re-enters the review→fix
13
- * loop at the next round, through the same `resume_loop` seam a resumed
14
- * interactive run uses. An attempt that stopped in implement (or before)
15
- * runs implement again on the approved plan.
13
+ * loop at the next round, but never on the lane's last, so its review is
14
+ * always followed by a fix pass, through the same `resume_loop` seam a
15
+ * resumed interactive run uses. An attempt that stopped in implement (or
16
+ * before) runs implement again on the approved plan.
16
17
  *
17
18
  * The file is the host's contract, so anything unreadable is refused here and
18
19
  * the caller runs a normal mission instead of crashing.
@@ -58,11 +59,21 @@ export declare function continuesInLoop(continuation: ContinuationInputs): boole
58
59
  * the same design context.
59
60
  */
60
61
  export declare function continuationReviewInput(continuation: ContinuationInputs, mission: MissionInputs | undefined, committed?: boolean): string;
62
+ /**
63
+ * The round a continuation re-enters the loop at: the earlier attempt's count
64
+ * goes on, but never onto the lane's last round, so the review it starts
65
+ * with is always followed by one fix pass before the cap can stop it. A
66
+ * resume onto the cap reviewed, hit the cap and stopped `round_cap` without a
67
+ * fix: COREX-1291's eight fix sessions (continue.json `roundsDone: 4` on the
68
+ * cloud lane's four rounds) each ran one review and changed nothing. A lane of
69
+ * one round has no fix pass to give. Pure.
70
+ */
71
+ export declare function continuationRound(reviewRoundsDone: number, maxRounds: number): number;
61
72
  /**
62
73
  * The resume plan that re-enters the review→fix loop for a continuation that
63
74
  * stopped in review, fix or verify; undefined when implement must run again.
64
- * The round continues the earlier attempt's count, capped at the lane's last
65
- * round so a continuation always gets one review. `committed` is the cloud
75
+ * Its round is {@link continuationRound}'s, so a continuation always gets a
76
+ * review and a fix pass for what that review finds. `committed` is the cloud
66
77
  * lane's resume, whose earlier work is on the branch.
67
78
  */
68
79
  export declare function continuationResumePlan(continuation: ContinuationInputs, ctx: {
@@ -10,9 +10,10 @@
10
10
  * run's own change (an implement that only confirms finished work is not
11
11
  * `no_changes`, and the verify gate scopes to the whole diff against HEAD);
12
12
  * - an attempt that stopped in review, fix or verify re-enters the review→fix
13
- * loop at the next round, through the same `resume_loop` seam a resumed
14
- * interactive run uses. An attempt that stopped in implement (or before)
15
- * runs implement again on the approved plan.
13
+ * loop at the next round, but never on the lane's last, so its review is
14
+ * always followed by a fix pass, through the same `resume_loop` seam a
15
+ * resumed interactive run uses. An attempt that stopped in implement (or
16
+ * before) runs implement again on the approved plan.
16
17
  *
17
18
  * The file is the host's contract, so anything unreadable is refused here and
18
19
  * the caller runs a normal mission instead of crashing.
@@ -95,11 +96,23 @@ export function continuationReviewInput(continuation, mission, committed = false
95
96
  const design = missionDesignContext(mission);
96
97
  return design ? `${summary}\n\n${design}` : summary;
97
98
  }
99
+ /**
100
+ * The round a continuation re-enters the loop at: the earlier attempt's count
101
+ * goes on, but never onto the lane's last round, so the review it starts
102
+ * with is always followed by one fix pass before the cap can stop it. A
103
+ * resume onto the cap reviewed, hit the cap and stopped `round_cap` without a
104
+ * fix: COREX-1291's eight fix sessions (continue.json `roundsDone: 4` on the
105
+ * cloud lane's four rounds) each ran one review and changed nothing. A lane of
106
+ * one round has no fix pass to give. Pure.
107
+ */
108
+ export function continuationRound(reviewRoundsDone, maxRounds) {
109
+ return Math.max(1, Math.min(reviewRoundsDone + 1, maxRounds - 1));
110
+ }
98
111
  /**
99
112
  * The resume plan that re-enters the review→fix loop for a continuation that
100
113
  * stopped in review, fix or verify; undefined when implement must run again.
101
- * The round continues the earlier attempt's count, capped at the lane's last
102
- * round so a continuation always gets one review. `committed` is the cloud
114
+ * Its round is {@link continuationRound}'s, so a continuation always gets a
115
+ * review and a fix pass for what that review finds. `committed` is the cloud
103
116
  * lane's resume, whose earlier work is on the branch.
104
117
  */
105
118
  export function continuationResumePlan(continuation, ctx, opts = {}) {
@@ -107,7 +120,7 @@ export function continuationResumePlan(continuation, ctx, opts = {}) {
107
120
  return undefined;
108
121
  return {
109
122
  mode: "resume_loop",
110
- round: Math.min(continuation.reviewRoundsDone + 1, opts.maxRounds ?? MAX_REVIEW_ROUNDS),
123
+ round: continuationRound(continuation.reviewRoundsDone, opts.maxRounds ?? MAX_REVIEW_ROUNDS),
111
124
  reviewInput: continuationReviewInput(continuation, ctx.mission, opts.committed),
112
125
  priorRounds: [],
113
126
  ticket: ctx.ticket,
@@ -300,6 +300,25 @@ export declare function detectVerifyTestCommands(cwd: string, opts?: DetectOpts)
300
300
  * verify failure.
301
301
  */
302
302
  export declare function classifyVerifyInfraFailure(output: string, exitCode: number): string | null;
303
+ /**
304
+ * Why a repository check gave no verdict when the paths it reads are not
305
+ * there: `diff` against a log an earlier plan step writes, `grep` in a folder
306
+ * named from the wrong root (COREX-1291: `diff: /tmp/fym-baseline-tsc.log:
307
+ * No such file or directory`, exit 2, eight fix sessions burned on it). The
308
+ * comparison never ran, so the check says nothing of the code, and a fix
309
+ * turn cannot make it run. Null when the output names no such path, when the
310
+ * tool that said so is not one the command runs (a test's own output), when
311
+ * a missing path is one this change touched (a file it deleted, a folder it
312
+ * made): that is the change's to put back, or when the rest of the output
313
+ * carries a located diagnostic (a compound check whose other half failed for
314
+ * real): the missing path is then not the whole story, and the check stays
315
+ * the change's failure. Pure.
316
+ */
317
+ export declare function classifyMissingInput(command: string, output: string, at: {
318
+ repoRoot: string;
319
+ dir: string;
320
+ changedPaths?: readonly string[];
321
+ }): string | null;
303
322
  /**
304
323
  * What a run could not start without, when its output says so in a way the
305
324
  * gate recognises: a credential or variable the environment lacks, or a
@@ -541,6 +541,87 @@ export function classifyVerifyInfraFailure(output, exitCode) {
541
541
  }
542
542
  return null;
543
543
  }
544
+ /**
545
+ * Tools whose "No such file or directory" names a path the check reads (or,
546
+ * for `tee`, writes into): the comparison or search never ran.
547
+ */
548
+ const PATH_READING_TOOLS = new Set("awk cat cmp comm cut diff egrep fgrep find grep head jq ls rg sed sort stat tail tee wc".split(" "));
549
+ /**
550
+ * A tool's own line for a path that is not there, GNU's and BSD's: `diff:
551
+ * /tmp/a.log: No such file or directory`, `ls: cannot access 'x': No such
552
+ * file or directory`, `awk: cannot open x (No such file or directory)`.
553
+ */
554
+ const MISSING_PATH_LINE = /^(?:\S*\/)?([a-z][\w-]*):\s+(.*?)\s*[:(]?\s*No such file or directory\b/;
555
+ /** What a tool says before the path it could not open. */
556
+ const MISSING_PATH_VERB = /^(?:(?:error|fatal):\s*)?(?:(?:cannot|can't|could not|couldn't|unable to)\s+(?:access|open|read|stat|statx)(?:\s+file)?:?\s+|IO error for operation on\s+)/i;
557
+ /** Missing paths a cause names before "and N more". */
558
+ const MAX_NAMED_MISSING_PATHS = 3;
559
+ /** The path in what a tool said about it, or null when it names none plainly. */
560
+ function missingPathIn(what) {
561
+ const quoted = [...what.matchAll(/[`'"‘]([^`'"’\n]+)[`'"’]/g)].at(-1)?.[1]?.trim();
562
+ if (quoted)
563
+ return quoted;
564
+ for (const segment of what.replace(MISSING_PATH_VERB, "").split(/:\s+/)) {
565
+ const path = segment.replace(/\s+for reading$/, "").trim();
566
+ if (path && !/\s/.test(path))
567
+ return path;
568
+ }
569
+ return null;
570
+ }
571
+ /** True when `tool` is a command the shell line runs, not a word in an argument. */
572
+ function commandRuns(command, tool) {
573
+ return new RegExp(`(?:^|[\\s;&|(!{\`])(?:\\S*/)?${tool}(?=[\\s;&|)]|$)`).test(command);
574
+ }
575
+ /** Two repo-relative paths are one, or one holds the other. */
576
+ function pathsOverlap(a, b) {
577
+ const x = a.replace(/\/+$/, "");
578
+ const y = b.replace(/\/+$/, "");
579
+ return x === y || x.startsWith(`${y}/`) || y.startsWith(`${x}/`);
580
+ }
581
+ /**
582
+ * Why a repository check gave no verdict when the paths it reads are not
583
+ * there: `diff` against a log an earlier plan step writes, `grep` in a folder
584
+ * named from the wrong root (COREX-1291: `diff: /tmp/fym-baseline-tsc.log:
585
+ * No such file or directory`, exit 2, eight fix sessions burned on it). The
586
+ * comparison never ran, so the check says nothing of the code, and a fix
587
+ * turn cannot make it run. Null when the output names no such path, when the
588
+ * tool that said so is not one the command runs (a test's own output), when
589
+ * a missing path is one this change touched (a file it deleted, a folder it
590
+ * made): that is the change's to put back, or when the rest of the output
591
+ * carries a located diagnostic (a compound check whose other half failed for
592
+ * real): the missing path is then not the whole story, and the check stays
593
+ * the change's failure. Pure.
594
+ */
595
+ export function classifyMissingInput(command, output, at) {
596
+ const missing = new Map();
597
+ const rest = [];
598
+ for (const raw of output.split("\n")) {
599
+ const match = MISSING_PATH_LINE.exec(raw.trim());
600
+ const tool = match?.[1];
601
+ if (!match || !tool || !PATH_READING_TOOLS.has(tool) || !commandRuns(command, tool)) {
602
+ rest.push(raw);
603
+ continue;
604
+ }
605
+ const path = missingPathIn(match[2] ?? "");
606
+ if (!path)
607
+ continue;
608
+ const repoPath = relative(at.repoRoot, isAbsolutePath(path) ? path : join(at.dir, path)) || ".";
609
+ const inRepo = repoPath !== ".." && !repoPath.startsWith("../") && !isAbsolutePath(repoPath);
610
+ if (inRepo && (at.changedPaths ?? []).some((changed) => pathsOverlap(changed, repoPath))) {
611
+ return null;
612
+ }
613
+ missing.set(inRepo ? repoPath : path, inRepo);
614
+ }
615
+ if (missing.size === 0)
616
+ return null;
617
+ if (parseVerifyFailures(rest.join("\n"), 1, command).some((finding) => finding.file))
618
+ return null;
619
+ const named = [...missing]
620
+ .slice(0, MAX_NAMED_MISSING_PATHS)
621
+ .map(([path, inRepo]) => (inRepo ? path : `${path} (outside the repository)`));
622
+ const more = missing.size - named.length;
623
+ return `${missing.size === 1 ? "a path it reads does not exist" : "paths it reads do not exist"}: ${named.join(", ")}${more > 0 ? ` and ${more} more` : ""}`;
624
+ }
544
625
  /**
545
626
  * Test-half infra/environment patterns (spec §3d): failure shapes that mean the
546
627
  * ENVIRONMENT could not run the suite — a DB that is not up, a missing env var,
@@ -1502,7 +1583,8 @@ export function makeRunVerify(opts = {}) {
1502
1583
  * The repository-checks half: the plan's approved checks, then the pull
1503
1584
  * request's CI steps its changed paths trigger. A failure becomes `critical`
1504
1585
  * findings the fix turn works on, except where it can never clear: a check
1505
- * the sandbox cannot start, an infra failure, an approved check that already
1586
+ * the sandbox cannot start, an infra failure, a path the check reads that is
1587
+ * not there ({@link classifyMissingInput}), an approved check that already
1506
1588
  * failed on the unchanged code (the host compares it after the build), and a
1507
1589
  * CI step that fails exactly as it did before a fix turn (it may fail on the
1508
1590
  * base too, and the pull request's CI shows it). Those are notes.
@@ -1599,6 +1681,7 @@ export function makeRunVerify(opts = {}) {
1599
1681
  approvedFailed: new Set(),
1600
1682
  checks: [],
1601
1683
  approvedRuns: new Map(),
1684
+ changedPaths: paths,
1602
1685
  };
1603
1686
  const before = await treeStatus(cwd, signal).catch((err) => {
1604
1687
  const msg = err instanceof Error ? err.message : String(err);
@@ -1688,8 +1771,21 @@ export function makeRunVerify(opts = {}) {
1688
1771
  }
1689
1772
  const output = scrubSecrets(rawOutput);
1690
1773
  const cls = classifyTestFailure(output, code, MAX_LOCATED_FINDINGS, env);
1691
- if (cls.kind === "infra" && !(cls.needs && check.passedOnBase)) {
1692
- const reason = `could not run: ${cls.reason}`;
1774
+ // A path the check reads that is not there (a log an earlier plan step
1775
+ // writes, a folder named from the wrong root) gave no verdict either,
1776
+ // unless the check passed on the unchanged code: then the change lost it.
1777
+ // One already failing on the base is a note as it is, its record kept.
1778
+ const cause = cls.kind === "infra" && !(cls.needs && check.passedOnBase)
1779
+ ? cls.reason
1780
+ : cls.kind === "uncertain" && check.baseline === "runnable" && !check.passedOnBase
1781
+ ? classifyMissingInput(command, output, {
1782
+ repoRoot: run.cwd,
1783
+ dir,
1784
+ changedPaths: run.changedPaths,
1785
+ })
1786
+ : null;
1787
+ if (cause) {
1788
+ const reason = `could not run: ${cause}`;
1693
1789
  const entry = record("cannot_run", `${reason}; ${exitSummary(check.name, code, output)}`);
1694
1790
  opts.onCheck?.(checkEnded(entry, output));
1695
1791
  return { kind: "note", reason };
@@ -2370,8 +2466,13 @@ export function makeMeasureChecksOnBase(opts = {}) {
2370
2466
  }
2371
2467
  const output = scrubSecrets(rawOutput);
2372
2468
  const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY, env);
2373
- if (cls.kind === "infra") {
2374
- result.notes.push(`\`${label}\`: could not run: ${cls.reason}`);
2469
+ const cause = cls.kind === "infra"
2470
+ ? cls.reason
2471
+ : cls.kind === "uncertain"
2472
+ ? classifyMissingInput(check.command, output, { repoRoot: cwd, dir: cwd })
2473
+ : null;
2474
+ if (cause) {
2475
+ result.notes.push(`\`${label}\`: could not run: ${cause}`);
2375
2476
  continue;
2376
2477
  }
2377
2478
  const found = repoCommandFindings({ output, code, env }, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.2.7-staging.2069.1",
3
+ "version": "1.2.8-staging.2076.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": "618a0d09ec56651efeb5669dc92e21a1ce8699ef"
61
+ "yagniSourceSha": "54b2ac0b0df83d33d1882e399cd60c4954d673d1"
62
62
  }