@yagni-app/code-staging 1.2.6-staging.1952.1 → 1.2.6-staging.1956.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 +20 -0
  2. package/dist/extension/branding.js +49 -8
  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
@@ -67,6 +67,33 @@ export interface PipelineStage {
67
67
  * claimed files, sibling claims, and the shared-worktree discipline rules.
68
68
  */
69
69
  personaClause?: string;
70
+ /**
71
+ * The persona body this stage runs with, in place of its agent's. The cloud
72
+ * plan stage takes the host's `plan-persona.md` (Reeve's persona) here.
73
+ */
74
+ persona?: string;
75
+ }
76
+ /**
77
+ * A stage list and the loop rules that go with it. The `full` lane is what
78
+ * interactive /go and mission builds run; the `cloud` lane is the one-attempt
79
+ * session (`yagni go --headless --cloud`).
80
+ */
81
+ export interface PipelineLane {
82
+ stages: PipelineStage[];
83
+ /** The review angles fanned out each round; empty is one reviewer covering every angle. */
84
+ reviewLenses: readonly ReviewLens[];
85
+ /** Review rounds before the loop stops `round_cap`. */
86
+ maxReviewRounds: number;
87
+ /** Whether implement may fan out into parallel builders (the diamond). */
88
+ fanout: boolean;
89
+ /** The plan stage ends the run: the plan goes to its gate before anything is built. */
90
+ stopAfterPlan: boolean;
91
+ /**
92
+ * Every stage loads the Team's skills (the launcher's `--skill` /
93
+ * `--prompt-template` flags), not only the ones that write code: the
94
+ * cloud session plans and reviews with them too (spec decision 4).
95
+ */
96
+ skillsEveryStage: boolean;
70
97
  }
71
98
  /** A single structured review finding parsed from a reviewer's output. */
72
99
  export interface Finding {
@@ -210,6 +237,8 @@ export interface PipelineResult {
210
237
  * orchestrator never sets it — commit/push live outside the LLM pipeline).
211
238
  */
212
239
  finish?: FinishInfo;
240
+ /** The plan the run wrote, on a lane whose plan stage ends the run (`stopAfterPlan`). */
241
+ plan?: string;
213
242
  }
214
243
  /**
215
244
  * The append-only resilience journal (spec: 2026-06-29-yagni-code-go-resilience).
@@ -512,6 +541,8 @@ export type PipelineProgress = {
512
541
  * - Round two runs only when round one found a blocker (`shouldStop`); what is
513
542
  * still open after the cap is written into the pull request body under
514
543
  * "Left for review" for Proctor, never a third round.
544
+ * This is the `full` lane's cap. The cloud lane carries its own
545
+ * (`PipelineLane.maxReviewRounds`): one reviewer per round, so four rounds.
515
546
  */
516
547
  export declare const MAX_REVIEW_ROUNDS = 2;
517
548
  /**
@@ -17,6 +17,8 @@
17
17
  * - Round two runs only when round one found a blocker (`shouldStop`); what is
18
18
  * still open after the cap is written into the pull request body under
19
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.
20
22
  */
21
23
  export const MAX_REVIEW_ROUNDS = 2;
22
24
  /**
@@ -96,7 +96,33 @@ export interface VerifyOutcome {
96
96
  findings: Finding[];
97
97
  /** Failures the clean tree already had (YAG-1003): named in `reason`, never findings. */
98
98
  preexisting?: number;
99
+ /** Every command the gate ran, with how it exited (the cloud session's `result.json` checks). */
100
+ checks?: VerifyCheckRun[];
99
101
  }
102
+ /** One command the gate ran, and how it went. */
103
+ export interface VerifyCheckRun {
104
+ name: string;
105
+ command: string;
106
+ /** Where it ran, repo-relative ("." for the root). */
107
+ cwd: string;
108
+ /** Null when the command never exited: it timed out, was aborted, or was not found. */
109
+ exitCode: number | null;
110
+ durationMs: number;
111
+ /**
112
+ * How the check stands on the unchanged code: it already failed there
113
+ * (the YAG-1003 baseline, or `checks.json`), the sandbox cannot run it, or
114
+ * neither.
115
+ */
116
+ baseline: "runnable" | "fails_on_base" | "cannot_run";
117
+ /** Why it has no exit code: "timed out", "aborted", "not found" or "could not run". */
118
+ note?: string;
119
+ }
120
+ /**
121
+ * Why a command threw instead of exiting, as its check record says it. `run`
122
+ * is the run's own signal (a /stop or the run's end); `capped` is the one
123
+ * the command ran under, which also carries its time cap.
124
+ */
125
+ export declare function unexitedNote(err: unknown, run?: AbortSignal, capped?: AbortSignal): string;
100
126
  /** Injectable fs seam so detection is unit-testable without a real repo. */
101
127
  export interface DetectOpts {
102
128
  readFile?: (path: string) => string;
@@ -404,6 +430,13 @@ export interface MakeRunVerifyOpts {
404
430
  * findings. Absent: every failure is a finding, as before.
405
431
  */
406
432
  baseline?: VerifyBaseline | null;
433
+ /**
434
+ * The host's secret and binary scans over what this run added
435
+ * (changeScan.ts, YAG-1000), given the tree's pre-run dirt to leave out;
436
+ * null when git could not say what changed. A hit is a finding the fix turn
437
+ * removes. Absent: no scan half, as before (the cloud session sets it).
438
+ */
439
+ scanChange?: (cwd: string, signal?: AbortSignal, baselinePaths?: string[]) => Promise<Finding[] | null>;
407
440
  }
408
441
  /**
409
442
  * One workstream as the scoped checker sees it: the name its findings are
@@ -53,7 +53,7 @@ import { readFileSync } from "node:fs";
53
53
  // APIs accept forward-slash paths — while win32 join/dirname would emit
54
54
  // backslashed strings that no longer compare equal to the git-derived repoRoot
55
55
  // and would break the walk-up termination check.
56
- import { basename, dirname, join } from "node:path/posix";
56
+ import { basename, dirname, join, relative } from "node:path/posix";
57
57
  import { readToolchainFile } from "../toolchain/spawnHook.js";
58
58
  import { killTreeSync, trackChild } from "./childRegistry.js";
59
59
  import { claimCovers } from "./fanout.js";
@@ -89,6 +89,23 @@ const MAX_LOCATED_FINDINGS = 25;
89
89
  const MAX_TAIL_CHARS = 1000;
90
90
  /** The verify lens reuses `does_it_hold` (does the change build/test) to avoid widening ReviewLens. */
91
91
  const VERIFY_LENS = "does_it_hold";
92
+ /**
93
+ * Why a command threw instead of exiting, as its check record says it. `run`
94
+ * is the run's own signal (a /stop or the run's end); `capped` is the one
95
+ * the command ran under, which also carries its time cap.
96
+ */
97
+ export function unexitedNote(err, run, capped) {
98
+ if (run?.aborted)
99
+ return "aborted";
100
+ if (capped?.aborted)
101
+ return "timed out";
102
+ const shape = err;
103
+ if (shape?.code === "ENOENT")
104
+ return "not found";
105
+ if (shape?.name === "AbortError" || shape?.name === "TimeoutError")
106
+ return "timed out";
107
+ return "could not run";
108
+ }
92
109
  /**
93
110
  * The priority order of package.json scripts we treat as a verify command.
94
111
  * Deliberately a SECRET-INDEPENDENT build check (`typecheck`/`lint`), NEVER the
@@ -1168,12 +1185,12 @@ export function makeRunVerify(opts = {}) {
1168
1185
  const steps = jobs.flatMap((job) => job.steps.map((step) => ({ job, step })));
1169
1186
  if (approved.length === 0 && steps.length === 0) {
1170
1187
  report();
1171
- return { findings, reasons, labels };
1188
+ return { findings, reasons, labels, checks: [] };
1172
1189
  }
1173
1190
  const buildLeft = (opts.repoChecksBuildBudgetMs ?? REPO_CHECKS_BUILD_BUDGET_MS) - repoChecksSpentMs;
1174
1191
  if (buildLeft < REPO_CHECK_MIN_START_MS) {
1175
1192
  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");
1176
- return { findings, reasons, labels };
1193
+ return { findings, reasons, labels, checks: [] };
1177
1194
  }
1178
1195
  const callBudget = Math.min(opts.repoChecksCallBudgetMs ?? REPO_CHECKS_CALL_BUDGET_MS, timeoutMs - (now() - input.callStartedAt), buildLeft);
1179
1196
  const halfStartedAt = now();
@@ -1188,6 +1205,7 @@ export function makeRunVerify(opts = {}) {
1188
1205
  notReached: 0,
1189
1206
  failedJobs,
1190
1207
  approvedFailed: new Set(),
1208
+ checks: [],
1191
1209
  };
1192
1210
  const before = await treeStatus(cwd, signal).catch((err) => {
1193
1211
  const msg = err instanceof Error ? err.message : String(err);
@@ -1221,26 +1239,47 @@ export function makeRunVerify(opts = {}) {
1221
1239
  if (run.notReached > 0) {
1222
1240
  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`);
1223
1241
  }
1224
- return { findings, reasons, labels };
1242
+ return { findings, reasons, labels, checks: run.checks };
1225
1243
  }
1226
1244
  /** One repository command's verdict: passed, a note (it could not run), or a failure with its output. */
1227
- async function runRepoCommand(run, command, dir, stepEnv) {
1245
+ async function runRepoCommand(run, command, dir, stepEnv, check) {
1228
1246
  const capMs = Math.min(opts.repoCheckTimeoutMs ?? REPO_CHECK_TIMEOUT_MS, run.left());
1229
1247
  const composed = composeAbortSignal(run.signal, AbortSignal.timeout(Math.max(1, capMs)));
1230
1248
  const env = opts.repoChecksEnv
1231
1249
  ? { ...opts.repoChecksEnv, ...(stepEnv ?? {}) }
1232
1250
  : buildRepoCheckEnv(process.env, run.toolchainFile, stepEnv);
1251
+ const startedAt = now();
1233
1252
  try {
1234
1253
  const { code, output: rawOutput } = await exec(repoCheckArgv(command), dir, composed, env);
1235
- if (code === 0)
1254
+ const record = (baseline) => run.checks.push({
1255
+ name: check.name,
1256
+ command,
1257
+ cwd: relative(run.cwd, dir) || ".",
1258
+ exitCode: code,
1259
+ durationMs: now() - startedAt,
1260
+ baseline,
1261
+ });
1262
+ if (code === 0) {
1263
+ record(check.baseline);
1236
1264
  return { kind: "passed" };
1265
+ }
1237
1266
  const output = scrubSecrets(rawOutput);
1238
1267
  const cls = classifyTestFailure(output, code);
1268
+ record(cls.kind === "infra" ? "cannot_run" : check.baseline);
1239
1269
  if (cls.kind === "infra")
1240
1270
  return { kind: "note", reason: `could not run: ${cls.reason}` };
1241
1271
  return { kind: "failed", output, code };
1242
1272
  }
1243
1273
  catch (err) {
1274
+ run.checks.push({
1275
+ name: check.name,
1276
+ command,
1277
+ cwd: relative(run.cwd, dir) || ".",
1278
+ exitCode: null,
1279
+ durationMs: now() - startedAt,
1280
+ baseline: check.baseline,
1281
+ note: unexitedNote(err, run.signal, composed),
1282
+ });
1244
1283
  const msg = err instanceof Error ? err.message : String(err);
1245
1284
  return { kind: "note", reason: `could not run: ${scrubSecrets(msg)}` };
1246
1285
  }
@@ -1271,7 +1310,10 @@ export function makeRunVerify(opts = {}) {
1271
1310
  continue;
1272
1311
  }
1273
1312
  const label = check.command.length > 80 ? `${check.command.slice(0, 77)}...` : check.command;
1274
- const verdict = await runRepoCommand(run, check.command, run.cwd, undefined);
1313
+ const verdict = await runRepoCommand(run, check.command, run.cwd, undefined, {
1314
+ name: label,
1315
+ baseline: check.status,
1316
+ });
1275
1317
  if (verdict.kind === "failed")
1276
1318
  run.approvedFailed.add(check.command.trim());
1277
1319
  if (verdict.kind === "note") {
@@ -1336,7 +1378,10 @@ export function makeRunVerify(opts = {}) {
1336
1378
  const dir = step.workingDirectory
1337
1379
  ? join(run.cwd, step.workingDirectory.replace(/^\.\/+/, ""))
1338
1380
  : run.cwd;
1339
- const verdict = await runRepoCommand(run, step.run, dir, step.env);
1381
+ const verdict = await runRepoCommand(run, step.run, dir, step.env, {
1382
+ name: `CI ${job.name}: ${step.name}`,
1383
+ baseline: "runnable",
1384
+ });
1340
1385
  if (verdict.kind === "passed") {
1341
1386
  lastCiFailure.delete(key);
1342
1387
  run.labels.push(`CI ${job.name}: ${step.name}`);
@@ -1374,7 +1419,7 @@ export function makeRunVerify(opts = {}) {
1374
1419
  // The approved checks carry the plan's approval, not the repository's own
1375
1420
  // choice of how to verify, so an override never takes them out of the loop.
1376
1421
  const repoFile = opts.repoChecks?.() ?? null;
1377
- if (commands.length === 0 && testCommands.length === 0 && !repoFile) {
1422
+ if (commands.length === 0 && testCommands.length === 0 && !repoFile && !opts.scanChange) {
1378
1423
  return {
1379
1424
  ran: false,
1380
1425
  ok: true,
@@ -1391,6 +1436,21 @@ export function makeRunVerify(opts = {}) {
1391
1436
  if (n > 0)
1392
1437
  preexisting.set(label, (preexisting.get(label) ?? 0) + n);
1393
1438
  };
1439
+ const checks = [];
1440
+ // A detected command's standing on the clean tree, from the baseline run.
1441
+ const standing = (command) => {
1442
+ const counts = opts.baseline?.get(verifyCommandKey(command));
1443
+ return counts && [...counts.values()].some((n) => n > 0) ? "fails_on_base" : "runnable";
1444
+ };
1445
+ const recordCommand = (command, code, startedAt, baseline, note) => checks.push({
1446
+ name: command.label,
1447
+ command: command.argv.join(" "),
1448
+ cwd: relative(cwd, command.cwd) || ".",
1449
+ exitCode: code,
1450
+ durationMs: now() - startedAt,
1451
+ baseline,
1452
+ ...(note ? { note } : {}),
1453
+ });
1394
1454
  // TYPECHECK HALF (frozen behavior, §0.2): run every changed package's build
1395
1455
  // check; union the findings. One package that cannot spawn fails OPEN (a
1396
1456
  // recorded reason, no finding) without sinking the verdict for the packages
@@ -1398,6 +1458,7 @@ export function makeRunVerify(opts = {}) {
1398
1458
  const typecheckLabels = [];
1399
1459
  for (const command of commands) {
1400
1460
  const composed = composeAbortSignal(signal, AbortSignal.timeout(timeoutMs));
1461
+ const startedAt = now();
1401
1462
  try {
1402
1463
  const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed);
1403
1464
  const output = scrubSecrets(rawOutput);
@@ -1409,9 +1470,11 @@ export function makeRunVerify(opts = {}) {
1409
1470
  const infra = classifyVerifyInfraFailure(output, code);
1410
1471
  if (infra) {
1411
1472
  reasons.push(`verify (${command.label}) could not run: ${infra}`);
1473
+ recordCommand(command, code, startedAt, "cannot_run");
1412
1474
  continue;
1413
1475
  }
1414
1476
  }
1477
+ recordCommand(command, code, startedAt, standing(command));
1415
1478
  anyRan = true;
1416
1479
  typecheckLabels.push(command.label);
1417
1480
  if (code !== 0) {
@@ -1422,6 +1485,7 @@ export function makeRunVerify(opts = {}) {
1422
1485
  }
1423
1486
  }
1424
1487
  catch (err) {
1488
+ recordCommand(command, null, startedAt, standing(command), unexitedNote(err, signal, composed));
1425
1489
  const msg = err instanceof Error ? err.message : String(err);
1426
1490
  reasons.push(`verify (${command.label}) could not run: ${scrubSecrets(msg)}`);
1427
1491
  }
@@ -1445,10 +1509,12 @@ export function makeRunVerify(opts = {}) {
1445
1509
  for (const command of testCommands) {
1446
1510
  const where = basename(command.cwd) || command.cwd;
1447
1511
  const composed = composeAbortSignal(signal, AbortSignal.timeout(testTimeoutMs));
1512
+ const startedAt = now();
1448
1513
  try {
1449
1514
  const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed, testEnv);
1450
1515
  const output = scrubSecrets(rawOutput);
1451
1516
  const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY);
1517
+ recordCommand(command, code, startedAt, cls.kind === "infra" ? "cannot_run" : standing(command));
1452
1518
  if (cls.kind === "infra") {
1453
1519
  reasons.push(`tests (${where}) could not run: ${cls.reason}; 'clean' reflects typecheck + review only`);
1454
1520
  continue;
@@ -1471,6 +1537,7 @@ export function makeRunVerify(opts = {}) {
1471
1537
  }
1472
1538
  }
1473
1539
  catch (err) {
1540
+ recordCommand(command, null, startedAt, standing(command), unexitedNote(err, signal, composed));
1474
1541
  const msg = err instanceof Error ? err.message : String(err);
1475
1542
  reasons.push(`tests (${where}) could not run: ${scrubSecrets(msg)}`);
1476
1543
  }
@@ -1495,11 +1562,33 @@ export function makeRunVerify(opts = {}) {
1495
1562
  });
1496
1563
  reasons.push(...repo.reasons);
1497
1564
  repoLabels.push(...repo.labels);
1565
+ checks.push(...repo.checks);
1498
1566
  if (repo.labels.length > 0)
1499
1567
  anyRan = true;
1500
1568
  findings.push(...repo.findings);
1501
1569
  }
1502
1570
  }
1571
+ // SCAN HALF (YAG-1000): what this run added, against the host's own stop
1572
+ // rules, whatever the other halves found: a credential is never the
1573
+ // compile error's root cause, and it must not reach the host.
1574
+ const scanLabels = [];
1575
+ if (opts.scanChange) {
1576
+ try {
1577
+ const hits = await opts.scanChange(cwd, signal, baselinePaths);
1578
+ if (hits === null) {
1579
+ reasons.push("the secret and binary scan could not read the change; the host still scans it");
1580
+ }
1581
+ else {
1582
+ anyRan = true;
1583
+ scanLabels.push("secret and binary scan");
1584
+ findings.push(...hits.slice(0, MAX_LOCATED_FINDINGS));
1585
+ }
1586
+ }
1587
+ catch (err) {
1588
+ const msg = err instanceof Error ? err.message : String(err);
1589
+ reasons.push(`the secret and binary scan could not run: ${scrubSecrets(msg)}; the host still scans the change`);
1590
+ }
1591
+ }
1503
1592
  const note = preexistingNote(preexisting);
1504
1593
  if (note)
1505
1594
  reasons.push(note);
@@ -1511,6 +1600,7 @@ export function makeRunVerify(opts = {}) {
1511
1600
  ...new Set(typecheckLabels),
1512
1601
  ...new Set(testLabels),
1513
1602
  ...new Set(repoLabels),
1603
+ ...scanLabels,
1514
1604
  ].join(", ");
1515
1605
  if (!anyRan) {
1516
1606
  return {
@@ -1519,6 +1609,7 @@ export function makeRunVerify(opts = {}) {
1519
1609
  command,
1520
1610
  reason: reasons.join("; ") || "verify skipped",
1521
1611
  findings: [],
1612
+ checks,
1522
1613
  };
1523
1614
  }
1524
1615
  return {
@@ -1528,6 +1619,7 @@ export function makeRunVerify(opts = {}) {
1528
1619
  findings,
1529
1620
  ...(reasons.length ? { reason: reasons.join("; ") } : {}),
1530
1621
  ...(preexistingTotal > 0 ? { preexisting: preexistingTotal } : {}),
1622
+ checks,
1531
1623
  };
1532
1624
  };
1533
1625
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.2.6-staging.1952.1",
3
+ "version": "1.2.6-staging.1956.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": "b25d4f380aa71b29777808fe149c71d8d0e3c4ce"
61
+ "yagniSourceSha": "bb058b7c375b03862aa2899c11e3c00b40d0b4f4"
62
62
  }