@yagni-app/code-staging 1.2.6-staging.1987.1 → 1.2.6-staging.1993.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.
@@ -106,6 +106,17 @@ export interface VerifyOutcome {
106
106
  /** Every command the gate ran, with how it exited (the cloud session's `result.json` checks). */
107
107
  checks?: VerifyCheckRun[];
108
108
  }
109
+ /** An approved check's name on the report: its command, cut at 80 characters. */
110
+ export declare function checkLabel(command: string): string;
111
+ /**
112
+ * True when a command changes the checkout's git state: a commit, a branch
113
+ * created or switched, a reset, a stash, a push. `git branch`, `git remote`
114
+ * and `git config` count as reads only in their listing forms. The same rule
115
+ * as `changesGitState` in the backend's attempts/inputs.ts, which leaves such
116
+ * a check out of the session's `checks.json`; both test files read one
117
+ * command corpus (test/fixtures/gitStateCommands.json), so they cannot drift.
118
+ */
119
+ export declare function changesGitState(command: string): boolean;
109
120
  /** One command the gate ran, and how it went. */
110
121
  export interface VerifyCheckRun {
111
122
  name: string;
@@ -107,6 +107,52 @@ export function exitSummary(label, code, output) {
107
107
  }
108
108
  /** The verify lens reuses `does_it_hold` (does the change build/test) to avoid widening ReviewLens. */
109
109
  const VERIFY_LENS = "does_it_hold";
110
+ /** An approved check's name on the report: its command, cut at 80 characters. */
111
+ export function checkLabel(command) {
112
+ return command.length > 80 ? `${command.slice(0, 77)}...` : command;
113
+ }
114
+ /** Git subcommands that only read: a check may run them as often as it likes. */
115
+ const READ_ONLY_GIT = new Set("status diff log show ls-files ls-tree rev-parse rev-list check-ignore grep blame describe cat-file shortlog diff-tree merge-base for-each-ref show-ref name-rev count-objects var version help".split(" "));
116
+ /**
117
+ * One git call in a shell command: what precedes it (a separator, a quote, a
118
+ * `$(`), its global options (`-C dir`, `-c k=v`, `--git-dir=...`,
119
+ * `--no-pager`), its subcommand, and its arguments up to the next separator.
120
+ */
121
+ const GIT_CALL =
122
+ // Each alternative is disjoint from the others (no backtracking blow-up).
123
+ /(?:^|[\s;&|(`!"'/])git((?:\s+(?:-[cC]\s+\S+|--[A-Za-z][\w-]*(?:=\S+)?|-[ABD-Zabd-z]))*)\s+([a-z][\w-]*)((?:[ \t][^;&|)`\n\r]*)?)/g;
124
+ /**
125
+ * True when a command changes the checkout's git state: a commit, a branch
126
+ * created or switched, a reset, a stash, a push. `git branch`, `git remote`
127
+ * and `git config` count as reads only in their listing forms. The same rule
128
+ * as `changesGitState` in the backend's attempts/inputs.ts, which leaves such
129
+ * a check out of the session's `checks.json`; both test files read one
130
+ * command corpus (test/fixtures/gitStateCommands.json), so they cannot drift.
131
+ */
132
+ export function changesGitState(command) {
133
+ const calls = command.matchAll(GIT_CALL);
134
+ for (const call of calls) {
135
+ const sub = call[2] ?? "";
136
+ const args = (call[3] ?? "").replace(/["']/g, "").trim().split(/\s+/).filter(Boolean);
137
+ if (READ_ONLY_GIT.has(sub))
138
+ continue;
139
+ const listing = args.every((arg) => arg.startsWith("-") &&
140
+ !/^-[dDmMcC]$|^--(?:delete|move|copy|set-upstream|unset|add|replace-all|edit)/.test(arg));
141
+ if (sub === "branch" && listing)
142
+ continue;
143
+ if (sub === "remote" &&
144
+ (args.length === 0 || args.every((arg) => arg === "-v" || arg === "--verbose")))
145
+ continue;
146
+ if (sub === "config" &&
147
+ (args.includes("--get") ||
148
+ args.includes("--list") ||
149
+ args.includes("-l") ||
150
+ (args.length === 1 && !args[0]?.startsWith("-"))))
151
+ continue;
152
+ return true;
153
+ }
154
+ return false;
155
+ }
110
156
  /**
111
157
  * Why a command threw instead of exiting, as its check record says it. `run`
112
158
  * is the run's own signal (a /stop or the run's end); `capped` is the one
@@ -434,6 +480,9 @@ const TEST_INFRA_PATTERNS = [
434
480
  "missing environment variable",
435
481
  ],
436
482
  [/\bprocess\.env\.[A-Z0-9_]+ is (?:undefined|not set)\b/, "missing environment variable"],
483
+ // Yarn Berry reading a `${TOKEN}` in .yarnrc.yml that the environment lacks
484
+ // (staging bench 2026-10-23: GITHUB_PACKAGES_TOKEN): the suite never started.
485
+ [/\bEnvironment variable not found \([A-Za-z0-9_]+\)/, "missing environment variable"],
437
486
  [/\bSASL\b|\bSCRAM-SERVER\b/, "database auth failed (missing DB credentials?)"],
438
487
  [/\bpassword authentication failed\b/i, "database auth failed"],
439
488
  [/\brole "[^"]*" does not exist\b/i, "database role does not exist"],
@@ -1151,9 +1200,23 @@ export function makeRunVerify(opts = {}) {
1151
1200
  const reasons = [];
1152
1201
  const labels = [];
1153
1202
  const cannotRun = [];
1203
+ // An approved check the sandbox cannot run is on the report as one with no
1204
+ // verdict, so a session that never ran it is never read as all green
1205
+ // (#2694): the host leaves its pull request a draft.
1206
+ const unrunnable = [];
1154
1207
  for (const check of file.checks) {
1155
- if (check.status === "cannot_run")
1156
- cannotRun.push(`\`${check.command}\`${check.cause ? ` (${check.cause})` : ""}`);
1208
+ if (check.status !== "cannot_run")
1209
+ continue;
1210
+ cannotRun.push(`\`${check.command}\`${check.cause ? ` (${check.cause})` : ""}`);
1211
+ unrunnable.push({
1212
+ name: checkLabel(check.command),
1213
+ command: check.command,
1214
+ cwd: ".",
1215
+ exitCode: null,
1216
+ durationMs: 0,
1217
+ baseline: "cannot_run",
1218
+ note: `could not run in the sandbox${check.cause ? `: ${check.cause}` : ""}`,
1219
+ });
1157
1220
  }
1158
1221
  const paths = await changedPaths(cwd, signal, input.baselinePaths);
1159
1222
  // A job the pull request's CI reported failed runs whatever its paths say.
@@ -1200,12 +1263,12 @@ export function makeRunVerify(opts = {}) {
1200
1263
  const steps = jobs.flatMap((job) => job.steps.map((step) => ({ job, step })));
1201
1264
  if (approved.length === 0 && steps.length === 0) {
1202
1265
  report();
1203
- return { findings, reasons, labels, checks: [] };
1266
+ return { findings, reasons, labels, checks: unrunnable };
1204
1267
  }
1205
1268
  const buildLeft = (opts.repoChecksBuildBudgetMs ?? REPO_CHECKS_BUILD_BUDGET_MS) - repoChecksSpentMs;
1206
1269
  if (buildLeft < REPO_CHECK_MIN_START_MS) {
1207
1270
  reasons.push("approved checks and CI steps not run again: the build's check budget is spent; the host still runs the approved checks after the build");
1208
- return { findings, reasons, labels, checks: [] };
1271
+ return { findings, reasons, labels, checks: unrunnable };
1209
1272
  }
1210
1273
  const callBudget = Math.min(opts.repoChecksCallBudgetMs ?? REPO_CHECKS_CALL_BUDGET_MS, timeoutMs - (now() - input.callStartedAt), buildLeft);
1211
1274
  const halfStartedAt = now();
@@ -1254,7 +1317,7 @@ export function makeRunVerify(opts = {}) {
1254
1317
  if (run.notReached > 0) {
1255
1318
  reasons.push(`${run.notReached} approved check(s) or CI step(s) not reached in the gate's time budget; the host still runs the approved checks after the build`);
1256
1319
  }
1257
- return { findings, reasons, labels, checks: run.checks };
1320
+ return { findings, reasons, labels, checks: [...unrunnable, ...run.checks] };
1258
1321
  }
1259
1322
  /** One repository command's verdict: passed, a note (it could not run), or a failure with its output. */
1260
1323
  async function runRepoCommand(run, command, dir, stepEnv, check) {
@@ -1328,7 +1391,13 @@ export function makeRunVerify(opts = {}) {
1328
1391
  run.notReached += 1;
1329
1392
  continue;
1330
1393
  }
1331
- const label = check.command.length > 80 ? `${check.command.slice(0, 77)}...` : check.command;
1394
+ const label = checkLabel(check.command);
1395
+ if (changesGitState(check.command)) {
1396
+ // A check runs every round: one that commits, branches or resets would
1397
+ // move the session's HEAD under its own work. Never run, and said.
1398
+ run.reasons.push(`approved check \`${label}\` not run: it changes the checkout's git state (a branch, a commit or a reset), which a check must never do`);
1399
+ continue;
1400
+ }
1332
1401
  const verdict = await runRepoCommand(run, check.command, run.cwd, undefined, {
1333
1402
  name: label,
1334
1403
  baseline: check.status,
@@ -1393,6 +1462,14 @@ export function makeRunVerify(opts = {}) {
1393
1462
  continue;
1394
1463
  }
1395
1464
  const where = `CI ${job.name}, step "${step.name}"`;
1465
+ if (changesGitState(step.run)) {
1466
+ // The runner's checkout is its own; here it is the session's branch.
1467
+ const reason = "it changes the checkout's git state";
1468
+ run.reasons.push(`${where} not run: ${reason}`);
1469
+ stoppedJobs.add(job);
1470
+ settle(job, "cannot_run", reason);
1471
+ continue;
1472
+ }
1396
1473
  const key = `${job.workflow}\u0000${job.job}\u0000${step.name}\u0000${step.run}`;
1397
1474
  const dir = step.workingDirectory
1398
1475
  ? join(run.cwd, step.workingDirectory.replace(/^\.\/+/, ""))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.2.6-staging.1987.1",
3
+ "version": "1.2.6-staging.1993.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": "c2d1f347d692ab8adbd5a5007f4a3d5c7b7d7d77"
61
+ "yagniSourceSha": "d016e89ef2032f809b217a8cca48b847f5770774"
62
62
  }