@yagni-app/code-staging 1.2.7-staging.2033.1 → 1.2.7-staging.2056.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.
|
@@ -456,6 +456,13 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
456
456
|
if (stream)
|
|
457
457
|
emit({ type: "pipeline_log", event, data });
|
|
458
458
|
};
|
|
459
|
+
// Each command the gate runs, as it starts and ends: a check runs for
|
|
460
|
+
// minutes with no turn of the model's, and the host's ticket went quiet
|
|
461
|
+
// through it (YAG-1031). The host shows the command and its verdict.
|
|
462
|
+
const onCheck = stream
|
|
463
|
+
? (event) => logPipeline("verify_check", event)
|
|
464
|
+
: undefined;
|
|
465
|
+
const heard = onCheck ? { onCheck } : {};
|
|
459
466
|
// A measurement before the build (the baseline, the plan's checks on the
|
|
460
467
|
// base) gets at most a sixth of the run's time left when it starts, and
|
|
461
468
|
// none below a minute: the build itself must still fit. No deadline, no cap.
|
|
@@ -480,7 +487,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
480
487
|
}
|
|
481
488
|
else if (dirs.length > 0) {
|
|
482
489
|
try {
|
|
483
|
-
const taken = await (deps.takeVerifyBaseline ?? makeTakeVerifyBaseline())(cwd, dirs, deps.signal, share.shareMs);
|
|
490
|
+
const taken = await (deps.takeVerifyBaseline ?? makeTakeVerifyBaseline(heard))(cwd, dirs, deps.signal, share.shareMs);
|
|
484
491
|
logPipeline("verify_baseline", {
|
|
485
492
|
ran: taken.ran,
|
|
486
493
|
failures: taken.failures,
|
|
@@ -518,7 +525,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
518
525
|
}
|
|
519
526
|
else {
|
|
520
527
|
try {
|
|
521
|
-
const measured = await (deps.measureChecksOnBase ?? makeMeasureChecksOnBase())(cwd, measureFile, deps.signal, share.shareMs);
|
|
528
|
+
const measured = await (deps.measureChecksOnBase ?? makeMeasureChecksOnBase(heard))(cwd, measureFile, deps.signal, share.shareMs);
|
|
522
529
|
logPipeline("verify_checks_on_base", {
|
|
523
530
|
ran: measured.ran,
|
|
524
531
|
passed: [...measured.passed],
|
|
@@ -558,6 +565,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
558
565
|
...(checksOnBase ? { checksOnBase } : {}),
|
|
559
566
|
...(scan ? { scanChange: scan } : {}),
|
|
560
567
|
...(cloud ? { unreadTestFailureIsFinding: true } : {}),
|
|
568
|
+
...heard,
|
|
561
569
|
});
|
|
562
570
|
gate = made;
|
|
563
571
|
runVerify = async (verifyCwd, signal, baselinePaths) => {
|
|
@@ -158,6 +158,38 @@ export interface VerifyCheckRun {
|
|
|
158
158
|
*/
|
|
159
159
|
note?: string;
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* How the note of an approved check this gate run did not run starts (its
|
|
163
|
+
* budget was spent, it was not reached, the build stopped first, the
|
|
164
|
+
* typecheck half failed first). The host's pull request body reads these
|
|
165
|
+
* words (`CHECK_NOT_RUN_NOTE` in @yagni/shared's attempts.ts); a change here
|
|
166
|
+
* is a change there.
|
|
167
|
+
*/
|
|
168
|
+
export declare const CHECK_NOT_RUN_NOTE = "not run on this tree";
|
|
169
|
+
/**
|
|
170
|
+
* One command the gate starts or ends, for the session's stream (headlessGo's
|
|
171
|
+
* `verify_check` line), so the ticket says what runs instead of going quiet
|
|
172
|
+
* for minutes (YAG-1031). `base` is a run on the unchanged code before the
|
|
173
|
+
* build. An end that did not pass carries the last few hundred characters
|
|
174
|
+
* of its output, scrubbed: the host logs them and never shows them.
|
|
175
|
+
*/
|
|
176
|
+
export type VerifyCheckEvent = {
|
|
177
|
+
phase: "start";
|
|
178
|
+
name: string;
|
|
179
|
+
command: string;
|
|
180
|
+
cwd: string;
|
|
181
|
+
base?: true;
|
|
182
|
+
} | {
|
|
183
|
+
phase: "end";
|
|
184
|
+
name: string;
|
|
185
|
+
command: string;
|
|
186
|
+
cwd: string;
|
|
187
|
+
exitCode: number | null;
|
|
188
|
+
durationMs: number;
|
|
189
|
+
baseline?: VerifyCheckRun["baseline"];
|
|
190
|
+
base?: true;
|
|
191
|
+
tail?: string;
|
|
192
|
+
};
|
|
161
193
|
/**
|
|
162
194
|
* Why a command threw instead of exiting, as its check record says it. `run`
|
|
163
195
|
* is the run's own signal (a /stop or the run's end); `capped` is the one
|
|
@@ -535,6 +567,8 @@ export interface MakeRunVerifyOpts {
|
|
|
535
567
|
* removes. Absent: no scan half, as before (the cloud session sets it).
|
|
536
568
|
*/
|
|
537
569
|
scanChange?: (cwd: string, signal?: AbortSignal, baselinePaths?: string[]) => Promise<Finding[] | null>;
|
|
570
|
+
/** Told as each command starts and ends (the session's stream). Absent: nobody hears it. */
|
|
571
|
+
onCheck?: (event: VerifyCheckEvent) => void;
|
|
538
572
|
}
|
|
539
573
|
/**
|
|
540
574
|
* One workstream as the scoped checker sees it: the name its findings are
|
|
@@ -636,6 +670,8 @@ export interface MakeTakeVerifyBaselineOpts {
|
|
|
636
670
|
now?: () => number;
|
|
637
671
|
treeStatus?: (cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
638
672
|
undoTreeWrites?: (cwd: string, entries: TreeEntry[]) => Promise<void>;
|
|
673
|
+
/** Told as each command starts and ends on the clean tree (the session's stream). */
|
|
674
|
+
onCheck?: (event: VerifyCheckEvent) => void;
|
|
639
675
|
}
|
|
640
676
|
/** What the baseline run found, for the gate and for the run's log. */
|
|
641
677
|
export interface VerifyBaselineResult {
|
|
@@ -698,6 +734,8 @@ export interface MakeMeasureChecksOnBaseOpts {
|
|
|
698
734
|
now?: () => number;
|
|
699
735
|
treeStatus?: (cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
700
736
|
undoTreeWrites?: (cwd: string, entries: TreeEntry[]) => Promise<void>;
|
|
737
|
+
/** Told as each check starts and ends on the unchanged code (the session's stream). */
|
|
738
|
+
onCheck?: (event: VerifyCheckEvent) => void;
|
|
701
739
|
}
|
|
702
740
|
/**
|
|
703
741
|
* Run each approved check `checks.json` marks `measureOnBase` once on the
|
|
@@ -195,6 +195,44 @@ export function changesGitState(command) {
|
|
|
195
195
|
}
|
|
196
196
|
return false;
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* How the note of an approved check this gate run did not run starts (its
|
|
200
|
+
* budget was spent, it was not reached, the build stopped first, the
|
|
201
|
+
* typecheck half failed first). The host's pull request body reads these
|
|
202
|
+
* words (`CHECK_NOT_RUN_NOTE` in @yagni/shared's attempts.ts); a change here
|
|
203
|
+
* is a change there.
|
|
204
|
+
*/
|
|
205
|
+
export const CHECK_NOT_RUN_NOTE = "not run on this tree";
|
|
206
|
+
/** How much of a failed command's output its end carries, for the host's own log. */
|
|
207
|
+
const CHECK_TAIL_CHARS = 400;
|
|
208
|
+
/** How a check went the last time it ran, after its not-run note; nothing when it never ran. */
|
|
209
|
+
function earlierRunWords(last) {
|
|
210
|
+
if (last === undefined)
|
|
211
|
+
return "";
|
|
212
|
+
if (last.exitCode === null)
|
|
213
|
+
return "; it gave no verdict on an earlier tree";
|
|
214
|
+
if (last.exitCode === 0)
|
|
215
|
+
return "; it passed on an earlier tree";
|
|
216
|
+
return `; it failed on an earlier tree (exit ${last.exitCode})`;
|
|
217
|
+
}
|
|
218
|
+
/** The end of a command's output, scrubbed, as a check's end carries it. */
|
|
219
|
+
function checkTail(output) {
|
|
220
|
+
return scrubSecrets(output).trimEnd().slice(-CHECK_TAIL_CHARS);
|
|
221
|
+
}
|
|
222
|
+
/** A recorded run's end, as the stream hears it: the output's tail only when it did not pass. */
|
|
223
|
+
function checkEnded(record, output) {
|
|
224
|
+
const tail = output && record.exitCode !== 0 ? checkTail(output) : "";
|
|
225
|
+
return {
|
|
226
|
+
phase: "end",
|
|
227
|
+
name: record.name,
|
|
228
|
+
command: record.command,
|
|
229
|
+
cwd: record.cwd,
|
|
230
|
+
exitCode: record.exitCode,
|
|
231
|
+
durationMs: record.durationMs,
|
|
232
|
+
baseline: record.baseline,
|
|
233
|
+
...(tail ? { tail } : {}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
198
236
|
/**
|
|
199
237
|
* Why a command threw instead of exiting, as its check record says it. `run`
|
|
200
238
|
* is the run's own signal (a /stop or the run's end); `capped` is the one
|
|
@@ -1401,6 +1439,54 @@ export function makeRunVerify(opts = {}) {
|
|
|
1401
1439
|
// keys from the last call it failed in.
|
|
1402
1440
|
let repoChecksSpentMs = 0;
|
|
1403
1441
|
const lastCiFailure = new Map();
|
|
1442
|
+
// Each approved check's last run in this build, by command (trimmed).
|
|
1443
|
+
const lastApproved = new Map();
|
|
1444
|
+
/**
|
|
1445
|
+
* The approved checks as one call reports them, in `checks.json`'s order:
|
|
1446
|
+
* one the sandbox cannot run with no verdict (#2694), one this call ran
|
|
1447
|
+
* with its record, and any other with no verdict on this tree, why, and how
|
|
1448
|
+
* it went the last time it ran. The cloud session's report is the last
|
|
1449
|
+
* call's: one that ran none of them (the build's check budget spent, the
|
|
1450
|
+
* typecheck half failing first) used to drop every one, and a pull request
|
|
1451
|
+
* whose plan named six checks said the session reported none (YAG-1031,
|
|
1452
|
+
* production 2026-10-25). A check that changes the checkout's git state is
|
|
1453
|
+
* never run and never listed.
|
|
1454
|
+
*/
|
|
1455
|
+
function approvedRecords(file, ran, why) {
|
|
1456
|
+
return file.checks.flatMap((check) => {
|
|
1457
|
+
if (check.status === "cannot_run") {
|
|
1458
|
+
return [
|
|
1459
|
+
{
|
|
1460
|
+
name: checkLabel(check.command),
|
|
1461
|
+
command: check.command,
|
|
1462
|
+
cwd: ".",
|
|
1463
|
+
exitCode: null,
|
|
1464
|
+
durationMs: 0,
|
|
1465
|
+
baseline: "cannot_run",
|
|
1466
|
+
note: `could not run in the sandbox${check.cause ? `: ${check.cause}` : ""}`,
|
|
1467
|
+
},
|
|
1468
|
+
];
|
|
1469
|
+
}
|
|
1470
|
+
if (changesGitState(check.command))
|
|
1471
|
+
return [];
|
|
1472
|
+
const key = check.command.trim();
|
|
1473
|
+
const record = ran.get(key);
|
|
1474
|
+
if (record)
|
|
1475
|
+
return [record];
|
|
1476
|
+
const last = lastApproved.get(key);
|
|
1477
|
+
return [
|
|
1478
|
+
{
|
|
1479
|
+
name: checkLabel(check.command),
|
|
1480
|
+
command: check.command,
|
|
1481
|
+
cwd: ".",
|
|
1482
|
+
exitCode: null,
|
|
1483
|
+
durationMs: 0,
|
|
1484
|
+
baseline: last?.baseline ?? measuredBaseline(opts.checksOnBase, check),
|
|
1485
|
+
note: `${CHECK_NOT_RUN_NOTE}: ${why}${earlierRunWords(last)}`,
|
|
1486
|
+
},
|
|
1487
|
+
];
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1404
1490
|
/**
|
|
1405
1491
|
* The repository-checks half: the plan's approved checks, then the pull
|
|
1406
1492
|
* request's CI steps its changed paths trigger. A failure becomes `critical`
|
|
@@ -1417,22 +1503,12 @@ export function makeRunVerify(opts = {}) {
|
|
|
1417
1503
|
const labels = [];
|
|
1418
1504
|
const cannotRun = [];
|
|
1419
1505
|
// An approved check the sandbox cannot run is on the report as one with no
|
|
1420
|
-
// verdict, so a session that never ran it is never
|
|
1421
|
-
// (#2694): the host leaves its pull request a draft.
|
|
1422
|
-
const unrunnable = [];
|
|
1506
|
+
// verdict (`approvedRecords`), so a session that never ran it is never
|
|
1507
|
+
// read as all green (#2694): the host leaves its pull request a draft.
|
|
1423
1508
|
for (const check of file.checks) {
|
|
1424
1509
|
if (check.status !== "cannot_run")
|
|
1425
1510
|
continue;
|
|
1426
1511
|
cannotRun.push(`\`${check.command}\`${check.cause ? ` (${check.cause})` : ""}`);
|
|
1427
|
-
unrunnable.push({
|
|
1428
|
-
name: checkLabel(check.command),
|
|
1429
|
-
command: check.command,
|
|
1430
|
-
cwd: ".",
|
|
1431
|
-
exitCode: null,
|
|
1432
|
-
durationMs: 0,
|
|
1433
|
-
baseline: "cannot_run",
|
|
1434
|
-
note: `could not run in the sandbox${check.cause ? `: ${check.cause}` : ""}`,
|
|
1435
|
-
});
|
|
1436
1512
|
}
|
|
1437
1513
|
const paths = await changedPaths(cwd, signal, input.baselinePaths);
|
|
1438
1514
|
// A job the pull request's CI reported failed runs whatever its paths say.
|
|
@@ -1479,12 +1555,23 @@ export function makeRunVerify(opts = {}) {
|
|
|
1479
1555
|
const steps = jobs.flatMap((job) => job.steps.map((step) => ({ job, step })));
|
|
1480
1556
|
if (approved.length === 0 && steps.length === 0) {
|
|
1481
1557
|
report();
|
|
1482
|
-
|
|
1558
|
+
// Every approved check here is one the sandbox cannot run: none is "not run".
|
|
1559
|
+
return {
|
|
1560
|
+
findings,
|
|
1561
|
+
reasons,
|
|
1562
|
+
labels,
|
|
1563
|
+
checks: approvedRecords(file, new Map(), "the sandbox cannot run it"),
|
|
1564
|
+
};
|
|
1483
1565
|
}
|
|
1484
1566
|
const buildLeft = (opts.repoChecksBuildBudgetMs ?? REPO_CHECKS_BUILD_BUDGET_MS) - repoChecksSpentMs;
|
|
1485
1567
|
if (buildLeft < REPO_CHECK_MIN_START_MS) {
|
|
1486
1568
|
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");
|
|
1487
|
-
return {
|
|
1569
|
+
return {
|
|
1570
|
+
findings,
|
|
1571
|
+
reasons,
|
|
1572
|
+
labels,
|
|
1573
|
+
checks: approvedRecords(file, new Map(), "the build's check budget is spent"),
|
|
1574
|
+
};
|
|
1488
1575
|
}
|
|
1489
1576
|
const callBudget = Math.min(opts.repoChecksCallBudgetMs ?? REPO_CHECKS_CALL_BUDGET_MS, timeoutMs - (now() - input.callStartedAt), buildLeft);
|
|
1490
1577
|
const halfStartedAt = now();
|
|
@@ -1500,6 +1587,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1500
1587
|
failedJobs,
|
|
1501
1588
|
approvedFailed: new Set(),
|
|
1502
1589
|
checks: [],
|
|
1590
|
+
approvedRuns: new Map(),
|
|
1503
1591
|
};
|
|
1504
1592
|
const before = await treeStatus(cwd, signal).catch((err) => {
|
|
1505
1593
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1533,7 +1621,19 @@ export function makeRunVerify(opts = {}) {
|
|
|
1533
1621
|
if (run.notReached > 0) {
|
|
1534
1622
|
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`);
|
|
1535
1623
|
}
|
|
1536
|
-
|
|
1624
|
+
const approvedRan = new Set(run.approvedRuns.values());
|
|
1625
|
+
const whyNotRun = signal?.aborted
|
|
1626
|
+
? "the build stopped first"
|
|
1627
|
+
: "not reached in the gate's time budget";
|
|
1628
|
+
return {
|
|
1629
|
+
findings,
|
|
1630
|
+
reasons,
|
|
1631
|
+
labels,
|
|
1632
|
+
checks: [
|
|
1633
|
+
...approvedRecords(file, run.approvedRuns, whyNotRun),
|
|
1634
|
+
...run.checks.filter((record) => !approvedRan.has(record)),
|
|
1635
|
+
],
|
|
1636
|
+
};
|
|
1537
1637
|
}
|
|
1538
1638
|
/**
|
|
1539
1639
|
* One repository command's verdict: passed, a note (it could not run), or a
|
|
@@ -1543,6 +1643,8 @@ export function makeRunVerify(opts = {}) {
|
|
|
1543
1643
|
* (`needs` says what). Only the session that measured knows it passed: a
|
|
1544
1644
|
* resumed session's host carries forward what failed on the base, never
|
|
1545
1645
|
* what passed, so there the same check is could not run, failing open.
|
|
1646
|
+
* The stream hears the command start, and its end here unless it failed:
|
|
1647
|
+
* a failure's caller tells its end once it decided whose the failure is.
|
|
1546
1648
|
*/
|
|
1547
1649
|
async function runRepoCommand(run, command, dir, stepEnv, check) {
|
|
1548
1650
|
const capMs = Math.min(opts.repoCheckTimeoutMs ?? REPO_CHECK_TIMEOUT_MS, run.left());
|
|
@@ -1550,6 +1652,8 @@ export function makeRunVerify(opts = {}) {
|
|
|
1550
1652
|
const env = opts.repoChecksEnv
|
|
1551
1653
|
? { ...opts.repoChecksEnv, ...(stepEnv ?? {}) }
|
|
1552
1654
|
: buildRepoCheckEnv(process.env, run.toolchainFile, stepEnv);
|
|
1655
|
+
const cwd = relative(run.cwd, dir) || ".";
|
|
1656
|
+
opts.onCheck?.({ phase: "start", name: check.name, command, cwd });
|
|
1553
1657
|
const startedAt = now();
|
|
1554
1658
|
try {
|
|
1555
1659
|
const { code, output: rawOutput } = await exec(repoCheckArgv(command), dir, composed, env);
|
|
@@ -1557,7 +1661,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1557
1661
|
const entry = {
|
|
1558
1662
|
name: check.name,
|
|
1559
1663
|
command,
|
|
1560
|
-
cwd
|
|
1664
|
+
cwd,
|
|
1561
1665
|
exitCode: note ? null : code,
|
|
1562
1666
|
durationMs: now() - startedAt,
|
|
1563
1667
|
baseline,
|
|
@@ -1567,14 +1671,16 @@ export function makeRunVerify(opts = {}) {
|
|
|
1567
1671
|
return entry;
|
|
1568
1672
|
};
|
|
1569
1673
|
if (code === 0) {
|
|
1570
|
-
record(check.baseline);
|
|
1674
|
+
const entry = record(check.baseline);
|
|
1675
|
+
opts.onCheck?.(checkEnded(entry));
|
|
1571
1676
|
return { kind: "passed" };
|
|
1572
1677
|
}
|
|
1573
1678
|
const output = scrubSecrets(rawOutput);
|
|
1574
1679
|
const cls = classifyTestFailure(output, code, MAX_LOCATED_FINDINGS, env);
|
|
1575
1680
|
if (cls.kind === "infra" && !(cls.needs && check.passedOnBase)) {
|
|
1576
1681
|
const reason = `could not run: ${cls.reason}`;
|
|
1577
|
-
record("cannot_run", `${reason}; ${exitSummary(check.name, code, output)}`);
|
|
1682
|
+
const entry = record("cannot_run", `${reason}; ${exitSummary(check.name, code, output)}`);
|
|
1683
|
+
opts.onCheck?.(checkEnded(entry, output));
|
|
1578
1684
|
return { kind: "note", reason };
|
|
1579
1685
|
}
|
|
1580
1686
|
return {
|
|
@@ -1587,15 +1693,17 @@ export function makeRunVerify(opts = {}) {
|
|
|
1587
1693
|
};
|
|
1588
1694
|
}
|
|
1589
1695
|
catch (err) {
|
|
1590
|
-
|
|
1696
|
+
const entry = {
|
|
1591
1697
|
name: check.name,
|
|
1592
1698
|
command,
|
|
1593
|
-
cwd
|
|
1699
|
+
cwd,
|
|
1594
1700
|
exitCode: null,
|
|
1595
1701
|
durationMs: now() - startedAt,
|
|
1596
1702
|
baseline: check.baseline,
|
|
1597
1703
|
note: unexitedNote(err, run.signal, composed),
|
|
1598
|
-
}
|
|
1704
|
+
};
|
|
1705
|
+
run.checks.push(entry);
|
|
1706
|
+
opts.onCheck?.(checkEnded(entry));
|
|
1599
1707
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1600
1708
|
return { kind: "note", reason: `could not run: ${scrubSecrets(msg)}` };
|
|
1601
1709
|
}
|
|
@@ -1628,11 +1736,18 @@ export function makeRunVerify(opts = {}) {
|
|
|
1628
1736
|
// The failures it had there, when the session measured it failing.
|
|
1629
1737
|
const failedOnBase = opts.checksOnBase?.failed.get(check.command.trim());
|
|
1630
1738
|
const status = measuredBaseline(opts.checksOnBase, check);
|
|
1739
|
+
const recorded = run.checks.length;
|
|
1631
1740
|
const verdict = await runRepoCommand(run, check.command, run.cwd, undefined, {
|
|
1632
1741
|
name: label,
|
|
1633
1742
|
baseline: status,
|
|
1634
1743
|
passedOnBase: opts.checksOnBase?.passed.has(check.command.trim()) ?? false,
|
|
1635
1744
|
});
|
|
1745
|
+
// `runRepoCommand` records every run once, whatever its verdict.
|
|
1746
|
+
const ran = run.checks[recorded];
|
|
1747
|
+
if (ran) {
|
|
1748
|
+
run.approvedRuns.set(check.command.trim(), ran);
|
|
1749
|
+
lastApproved.set(check.command.trim(), ran);
|
|
1750
|
+
}
|
|
1636
1751
|
if (verdict.kind === "failed")
|
|
1637
1752
|
run.approvedFailed.add(canonicalCheckCommand(check.command));
|
|
1638
1753
|
if (verdict.kind === "failed" && verdict.needs) {
|
|
@@ -1668,6 +1783,8 @@ export function makeRunVerify(opts = {}) {
|
|
|
1668
1783
|
run.findings.push(...found.slice(0, findingRoom(run)));
|
|
1669
1784
|
}
|
|
1670
1785
|
}
|
|
1786
|
+
if (verdict.kind === "failed")
|
|
1787
|
+
opts.onCheck?.(checkEnded(verdict.record, verdict.output));
|
|
1671
1788
|
}
|
|
1672
1789
|
}
|
|
1673
1790
|
/**
|
|
@@ -1742,6 +1859,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1742
1859
|
continue;
|
|
1743
1860
|
}
|
|
1744
1861
|
settle(job, "failed");
|
|
1862
|
+
opts.onCheck?.(checkEnded(verdict.record, verdict.output));
|
|
1745
1863
|
const stepFindings = repoFailureFindings(run, verdict, step.name, where, dir);
|
|
1746
1864
|
const keys = new Set(stepFindings.map((f) => f.message));
|
|
1747
1865
|
const previous = lastCiFailure.get(key);
|
|
@@ -1791,15 +1909,27 @@ export function makeRunVerify(opts = {}) {
|
|
|
1791
1909
|
const counts = opts.baseline?.get(verifyCommandKey(command));
|
|
1792
1910
|
return counts && [...counts.values()].some((n) => n > 0) ? "fails_on_base" : "runnable";
|
|
1793
1911
|
};
|
|
1794
|
-
|
|
1912
|
+
/** A detected command starting, as the stream hears it. */
|
|
1913
|
+
const commandStarted = (command) => opts.onCheck?.({
|
|
1914
|
+
phase: "start",
|
|
1795
1915
|
name: command.label,
|
|
1796
1916
|
command: command.argv.join(" "),
|
|
1797
1917
|
cwd: relative(cwd, command.cwd) || ".",
|
|
1798
|
-
exitCode: code,
|
|
1799
|
-
durationMs: now() - startedAt,
|
|
1800
|
-
baseline,
|
|
1801
|
-
...(note ? { note } : {}),
|
|
1802
1918
|
});
|
|
1919
|
+
/** A detected command's record, its end told to the stream with its output's tail. */
|
|
1920
|
+
const recordCommand = (command, code, startedAt, baseline, more = {}) => {
|
|
1921
|
+
const entry = {
|
|
1922
|
+
name: command.label,
|
|
1923
|
+
command: command.argv.join(" "),
|
|
1924
|
+
cwd: relative(cwd, command.cwd) || ".",
|
|
1925
|
+
exitCode: code,
|
|
1926
|
+
durationMs: now() - startedAt,
|
|
1927
|
+
baseline,
|
|
1928
|
+
...(more.note ? { note: more.note } : {}),
|
|
1929
|
+
};
|
|
1930
|
+
checks.push(entry);
|
|
1931
|
+
opts.onCheck?.(checkEnded(entry, more.output));
|
|
1932
|
+
};
|
|
1803
1933
|
// TYPECHECK HALF (frozen behavior, §0.2): run every changed package's build
|
|
1804
1934
|
// check; union the findings. One package that cannot spawn fails OPEN (a
|
|
1805
1935
|
// recorded reason, no finding) without sinking the verdict for the packages
|
|
@@ -1808,6 +1938,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1808
1938
|
const typecheckEnv = buildGateVerifyEnv(process.env, toolchainFile);
|
|
1809
1939
|
for (const command of commands) {
|
|
1810
1940
|
const composed = composeAbortSignal(signal, AbortSignal.timeout(timeoutMs));
|
|
1941
|
+
commandStarted(command);
|
|
1811
1942
|
const startedAt = now();
|
|
1812
1943
|
try {
|
|
1813
1944
|
const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed, typecheckEnv);
|
|
@@ -1820,11 +1951,14 @@ export function makeRunVerify(opts = {}) {
|
|
|
1820
1951
|
const infra = classifyVerifyInfraFailure(output, code);
|
|
1821
1952
|
if (infra) {
|
|
1822
1953
|
reasons.push(`verify (${command.label}) could not run: ${infra}`);
|
|
1823
|
-
recordCommand(command, null, startedAt, "cannot_run",
|
|
1954
|
+
recordCommand(command, null, startedAt, "cannot_run", {
|
|
1955
|
+
note: `could not run: ${infra}; ${exitSummary(command.label, code, output)}`,
|
|
1956
|
+
output,
|
|
1957
|
+
});
|
|
1824
1958
|
continue;
|
|
1825
1959
|
}
|
|
1826
1960
|
}
|
|
1827
|
-
recordCommand(command, code, startedAt, standing(command));
|
|
1961
|
+
recordCommand(command, code, startedAt, standing(command), { output });
|
|
1828
1962
|
anyRan = true;
|
|
1829
1963
|
typecheckLabels.push(command.label);
|
|
1830
1964
|
if (code !== 0) {
|
|
@@ -1835,7 +1969,9 @@ export function makeRunVerify(opts = {}) {
|
|
|
1835
1969
|
}
|
|
1836
1970
|
}
|
|
1837
1971
|
catch (err) {
|
|
1838
|
-
recordCommand(command, null, startedAt, standing(command),
|
|
1972
|
+
recordCommand(command, null, startedAt, standing(command), {
|
|
1973
|
+
note: unexitedNote(err, signal, composed),
|
|
1974
|
+
});
|
|
1839
1975
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1840
1976
|
reasons.push(`verify (${command.label}) could not run: ${scrubSecrets(msg)}`);
|
|
1841
1977
|
}
|
|
@@ -1871,13 +2007,17 @@ export function makeRunVerify(opts = {}) {
|
|
|
1871
2007
|
continue;
|
|
1872
2008
|
}
|
|
1873
2009
|
const composed = composeAbortSignal(signal, AbortSignal.timeout(testTimeoutMs));
|
|
2010
|
+
commandStarted(command);
|
|
1874
2011
|
const startedAt = now();
|
|
1875
2012
|
try {
|
|
1876
2013
|
const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed, testEnv);
|
|
1877
2014
|
const output = scrubSecrets(rawOutput);
|
|
1878
2015
|
const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY, testEnv);
|
|
1879
2016
|
if (cls.kind === "infra") {
|
|
1880
|
-
recordCommand(command, null, startedAt, "cannot_run",
|
|
2017
|
+
recordCommand(command, null, startedAt, "cannot_run", {
|
|
2018
|
+
note: `could not run: ${cls.reason}; ${exitSummary(command.label, code, output)}`,
|
|
2019
|
+
output,
|
|
2020
|
+
});
|
|
1881
2021
|
reasons.push(`tests (${where}) could not run: ${cls.reason}; 'clean' reflects typecheck + review only`);
|
|
1882
2022
|
continue;
|
|
1883
2023
|
}
|
|
@@ -1887,19 +2027,22 @@ export function makeRunVerify(opts = {}) {
|
|
|
1887
2027
|
testLabels.push(command.label);
|
|
1888
2028
|
if (base === "fails_on_base") {
|
|
1889
2029
|
const summary = exitSummary(command.label, code, output);
|
|
1890
|
-
recordCommand(command, code, startedAt, base,
|
|
2030
|
+
recordCommand(command, code, startedAt, base, {
|
|
2031
|
+
note: `failed on the unchanged code too: ${summary}`,
|
|
2032
|
+
output,
|
|
2033
|
+
});
|
|
1891
2034
|
reasons.push(`tests (${where}) failed on the unchanged code too, so not a finding: ${summary}`);
|
|
1892
2035
|
}
|
|
1893
2036
|
else {
|
|
1894
2037
|
// One finding per command, past the cap too: it is the only
|
|
1895
2038
|
// word of this command's failure.
|
|
1896
|
-
recordCommand(command, code, startedAt, base);
|
|
2039
|
+
recordCommand(command, code, startedAt, base, { output });
|
|
1897
2040
|
testFindingCount += 1;
|
|
1898
2041
|
findings.push(tailFinding(command.label, code, output));
|
|
1899
2042
|
}
|
|
1900
2043
|
continue;
|
|
1901
2044
|
}
|
|
1902
|
-
recordCommand(command, code, startedAt, base);
|
|
2045
|
+
recordCommand(command, code, startedAt, base, { output });
|
|
1903
2046
|
if (cls.kind === "uncertain") {
|
|
1904
2047
|
// Failing OPEN on an uninterpretable failure (§3d): an honest note,
|
|
1905
2048
|
// never a guessed finding the fix stage cannot clear.
|
|
@@ -1918,7 +2061,9 @@ export function makeRunVerify(opts = {}) {
|
|
|
1918
2061
|
}
|
|
1919
2062
|
}
|
|
1920
2063
|
catch (err) {
|
|
1921
|
-
recordCommand(command, null, startedAt, standing(command),
|
|
2064
|
+
recordCommand(command, null, startedAt, standing(command), {
|
|
2065
|
+
note: unexitedNote(err, signal, composed),
|
|
2066
|
+
});
|
|
1922
2067
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1923
2068
|
reasons.push(`tests (${where}) could not run: ${scrubSecrets(msg)}`);
|
|
1924
2069
|
}
|
|
@@ -1932,6 +2077,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1932
2077
|
if (repoFile) {
|
|
1933
2078
|
if (typecheckFailed) {
|
|
1934
2079
|
reasons.push("approved checks and CI steps skipped: the typecheck half failed; a later round runs them");
|
|
2080
|
+
checks.push(...approvedRecords(repoFile, new Map(), "the typecheck half failed first"));
|
|
1935
2081
|
}
|
|
1936
2082
|
else {
|
|
1937
2083
|
const repo = await runRepoChecks({
|
|
@@ -2056,7 +2202,8 @@ export function makeTakeVerifyBaseline(opts = {}) {
|
|
|
2056
2202
|
for (const { command, test } of runs) {
|
|
2057
2203
|
if (signal?.aborted)
|
|
2058
2204
|
break;
|
|
2059
|
-
const
|
|
2205
|
+
const commandStartedAt = now();
|
|
2206
|
+
const left = budgetMs - (commandStartedAt - startedAt);
|
|
2060
2207
|
if (left < REPO_CHECK_MIN_START_MS) {
|
|
2061
2208
|
result.notes.push(`${command.label} (${command.cwd}): not reached in the baseline budget`);
|
|
2062
2209
|
continue;
|
|
@@ -2065,8 +2212,23 @@ export function makeTakeVerifyBaseline(opts = {}) {
|
|
|
2065
2212
|
? (opts.testTimeoutMs ?? VERIFY_TEST_TIMEOUT_MS)
|
|
2066
2213
|
: (opts.timeoutMs ?? VERIFY_TIMEOUT_MS), left);
|
|
2067
2214
|
const composed = composeAbortSignal(signal, AbortSignal.timeout(cap));
|
|
2215
|
+
const heard = {
|
|
2216
|
+
name: command.label,
|
|
2217
|
+
command: command.argv.join(" "),
|
|
2218
|
+
cwd: relative(cwd, command.cwd) || ".",
|
|
2219
|
+
base: true,
|
|
2220
|
+
};
|
|
2221
|
+
opts.onCheck?.({ phase: "start", ...heard });
|
|
2222
|
+
const ended = (exitCode, output) => opts.onCheck?.({
|
|
2223
|
+
phase: "end",
|
|
2224
|
+
...heard,
|
|
2225
|
+
exitCode,
|
|
2226
|
+
durationMs: now() - commandStartedAt,
|
|
2227
|
+
...(output && exitCode !== 0 ? { tail: checkTail(output) } : {}),
|
|
2228
|
+
});
|
|
2068
2229
|
try {
|
|
2069
2230
|
const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed, test ? testEnv : verifyEnv);
|
|
2231
|
+
ended(code, rawOutput);
|
|
2070
2232
|
const output = scrubSecrets(rawOutput);
|
|
2071
2233
|
let failed;
|
|
2072
2234
|
if (test) {
|
|
@@ -2095,6 +2257,7 @@ export function makeTakeVerifyBaseline(opts = {}) {
|
|
|
2095
2257
|
result.failures += n;
|
|
2096
2258
|
}
|
|
2097
2259
|
catch (err) {
|
|
2260
|
+
ended(null);
|
|
2098
2261
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2099
2262
|
result.notes.push(`${command.label} (${command.cwd}): could not run: ${scrubSecrets(msg)}`);
|
|
2100
2263
|
}
|
|
@@ -2170,14 +2333,25 @@ export function makeMeasureChecksOnBase(opts = {}) {
|
|
|
2170
2333
|
break;
|
|
2171
2334
|
const command = check.command.trim();
|
|
2172
2335
|
const label = checkLabel(check.command);
|
|
2173
|
-
const
|
|
2336
|
+
const checkStartedAt = now();
|
|
2337
|
+
const left = budgetMs - (checkStartedAt - startedAt);
|
|
2174
2338
|
if (left < REPO_CHECK_MIN_START_MS) {
|
|
2175
2339
|
result.notes.push(`\`${label}\`: not reached in the budget`);
|
|
2176
2340
|
continue;
|
|
2177
2341
|
}
|
|
2178
2342
|
const composed = composeAbortSignal(signal, AbortSignal.timeout(Math.min(opts.timeoutMs ?? REPO_CHECK_TIMEOUT_MS, left)));
|
|
2343
|
+
const heard = { name: label, command: check.command, cwd: ".", base: true };
|
|
2344
|
+
opts.onCheck?.({ phase: "start", ...heard });
|
|
2345
|
+
const ended = (exitCode, output) => opts.onCheck?.({
|
|
2346
|
+
phase: "end",
|
|
2347
|
+
...heard,
|
|
2348
|
+
exitCode,
|
|
2349
|
+
durationMs: now() - checkStartedAt,
|
|
2350
|
+
...(output && exitCode !== 0 ? { tail: checkTail(output) } : {}),
|
|
2351
|
+
});
|
|
2179
2352
|
try {
|
|
2180
2353
|
const { code, output: rawOutput } = await exec(repoCheckArgv(check.command), cwd, composed, env);
|
|
2354
|
+
ended(code, rawOutput);
|
|
2181
2355
|
if (code === 0) {
|
|
2182
2356
|
passed.add(command);
|
|
2183
2357
|
result.ran.push(label);
|
|
@@ -2206,6 +2380,7 @@ export function makeMeasureChecksOnBase(opts = {}) {
|
|
|
2206
2380
|
result.ran.push(label);
|
|
2207
2381
|
}
|
|
2208
2382
|
catch (err) {
|
|
2383
|
+
ended(null);
|
|
2209
2384
|
result.notes.push(`\`${label}\`: ${unexitedNote(err, signal, composed)}`);
|
|
2210
2385
|
}
|
|
2211
2386
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.2.7-staging.
|
|
3
|
+
"version": "1.2.7-staging.2056.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": "
|
|
61
|
+
"yagniSourceSha": "76a36c6b94db739ea0fb0544cbe9a26279e6881c"
|
|
62
62
|
}
|