@yagni-app/code-staging 1.2.6-staging.1882.1 → 1.2.6-staging.1886.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/extension/pipeline/headlessGo.d.ts +2 -0
- package/dist/extension/pipeline/headlessGo.js +19 -3
- package/dist/extension/pipeline/missionChecks.d.ts +24 -1
- package/dist/extension/pipeline/missionChecks.js +35 -2
- package/dist/extension/pipeline/verify.d.ts +6 -1
- package/dist/extension/pipeline/verify.js +67 -3
- package/package.json +2 -2
|
@@ -153,6 +153,8 @@ export interface HeadlessGoDeps {
|
|
|
153
153
|
now?: () => number;
|
|
154
154
|
runPipeline?: typeof defaultRunPipeline;
|
|
155
155
|
readFile?: (path: string) => string;
|
|
156
|
+
/** Writes the gate's `ci-results.json`. Defaults to `writeFileSync`; injected in tests. */
|
|
157
|
+
writeFile?: (path: string, text: string) => void;
|
|
156
158
|
/**
|
|
157
159
|
* The verify gate a mission run uses, given its `checks.json` (null when
|
|
158
160
|
* there is none). Defaults to the real gate; injected in tests.
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
* cannot finish, rather than being killed partway through one (see budget.ts). Such a run gets the sandbox lane's
|
|
59
59
|
* ceilings (`SANDBOX_RUN_BUDGET`), not interactive /go's.
|
|
60
60
|
*/
|
|
61
|
-
import { readFileSync } from "node:fs";
|
|
61
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
62
62
|
import { dirname, join } from "node:path";
|
|
63
63
|
import { continuationResumePlan, parseContinuation } from "./continuation.js";
|
|
64
64
|
import { resolveFanoutMode } from "./fanout.js";
|
|
@@ -66,7 +66,7 @@ import { makeFanoutBeats } from "./fanoutBeats.js";
|
|
|
66
66
|
import { isBlockingFinding } from "./findings.js";
|
|
67
67
|
import { MISSION_FIX_ROUND_FILE_NAME, readFixRoundFile } from "./fixRound.js";
|
|
68
68
|
import { normalizeMission } from "./mission.js";
|
|
69
|
-
import { MISSION_CHECKS_FILE_NAME, readMissionChecksFile } from "./missionChecks.js";
|
|
69
|
+
import { MISSION_CHECKS_FILE_NAME, MISSION_CI_RESULTS_FILE_NAME, readMissionChecksFile, renderCiResults, } from "./missionChecks.js";
|
|
70
70
|
import { startTreeSnapshots } from "./treeSnapshot.js";
|
|
71
71
|
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
72
72
|
import { makeRunVerify } from "./verify.js";
|
|
@@ -333,7 +333,23 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
333
333
|
const read = readMissionChecksFile(checksPath, readFile);
|
|
334
334
|
if (read.file) {
|
|
335
335
|
const checks = read.file;
|
|
336
|
-
|
|
336
|
+
// How the failed CI checks' jobs went, for the host, beside the checks
|
|
337
|
+
// file. A write that fails is said once; the host then trusts the
|
|
338
|
+
// round's own account, as before.
|
|
339
|
+
const resultsPath = join(dirname(args.ticketFile), MISSION_CI_RESULTS_FILE_NAME);
|
|
340
|
+
let resultsWriteFailed = false;
|
|
341
|
+
const onFailedChecks = (results) => {
|
|
342
|
+
try {
|
|
343
|
+
(deps.writeFile ?? writeFileSync)(resultsPath, renderCiResults(results));
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
if (resultsWriteFailed)
|
|
347
|
+
return;
|
|
348
|
+
resultsWriteFailed = true;
|
|
349
|
+
writeErr(`Could not write ${resultsPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
runVerify = (deps.makeRunVerify ?? makeRunVerify)({ repoChecks: () => checks, onFailedChecks });
|
|
337
353
|
}
|
|
338
354
|
else if (read.problem !== "missing") {
|
|
339
355
|
const why = read.problem === "malformed" ? "not a version 1 checks file" : "it could not be read";
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* run besides its own detected scripts: the plan's approved checks (each with
|
|
4
4
|
* what the host already knows: it fails on the unchanged code, or the sandbox
|
|
5
5
|
* cannot start it) and the pull request's CI jobs, read from the repository's
|
|
6
|
-
* workflows at build time.
|
|
6
|
+
* workflows at build time. On a fix round, the job the pull request's CI
|
|
7
|
+
* reported failed is marked (`failedCheck`): the gate runs it whatever its
|
|
8
|
+
* `paths` say, and writes how it went to `ci-results.json` beside this file
|
|
9
|
+
* for the host, which reads it with its own parser
|
|
10
|
+
* (`packages/backend/src/agent/runs/mission/roundOutcomes.ts`).
|
|
7
11
|
*
|
|
8
12
|
* The format's producer is `renderMissionChecksJson` in
|
|
9
13
|
* `packages/backend/src/integrations/e2b/missionFiles.ts`; this package does
|
|
@@ -35,12 +39,31 @@ export interface MissionCiJob {
|
|
|
35
39
|
step: string;
|
|
36
40
|
reason: string;
|
|
37
41
|
}>;
|
|
42
|
+
/** The pull request's CI reported this job failed (at `step`): the gate runs it whatever its `paths` say. */
|
|
43
|
+
failedCheck?: {
|
|
44
|
+
name: string;
|
|
45
|
+
step?: string;
|
|
46
|
+
};
|
|
38
47
|
}
|
|
39
48
|
export interface MissionChecksFile {
|
|
40
49
|
version: 1;
|
|
41
50
|
checks: MissionCheck[];
|
|
42
51
|
ciJobs: MissionCiJob[];
|
|
52
|
+
/** Failed CI checks the gate cannot run, with why (no job named like it, or a job that cannot run here). */
|
|
53
|
+
failedChecksCannotRun?: Array<{
|
|
54
|
+
name: string;
|
|
55
|
+
reason: string;
|
|
56
|
+
}>;
|
|
57
|
+
}
|
|
58
|
+
export declare const MISSION_CI_RESULTS_FILE_NAME = "ci-results.json";
|
|
59
|
+
/** How one failed CI check's job went the last time the gate ran it. */
|
|
60
|
+
export interface FailedCheckResult {
|
|
61
|
+
name: string;
|
|
62
|
+
status: "passed" | "failed" | "cannot_run" | "not_reached";
|
|
63
|
+
reason?: string;
|
|
43
64
|
}
|
|
65
|
+
/** `ci-results.json` (version 1), as the host reads it. */
|
|
66
|
+
export declare function renderCiResults(checks: readonly FailedCheckResult[]): string;
|
|
44
67
|
/** The file, or null when the text is not a well-formed version 1 file. */
|
|
45
68
|
export declare function parseMissionChecksFile(text: string): MissionChecksFile | null;
|
|
46
69
|
/**
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* run besides its own detected scripts: the plan's approved checks (each with
|
|
4
4
|
* what the host already knows: it fails on the unchanged code, or the sandbox
|
|
5
5
|
* cannot start it) and the pull request's CI jobs, read from the repository's
|
|
6
|
-
* workflows at build time.
|
|
6
|
+
* workflows at build time. On a fix round, the job the pull request's CI
|
|
7
|
+
* reported failed is marked (`failedCheck`): the gate runs it whatever its
|
|
8
|
+
* `paths` say, and writes how it went to `ci-results.json` beside this file
|
|
9
|
+
* for the host, which reads it with its own parser
|
|
10
|
+
* (`packages/backend/src/agent/runs/mission/roundOutcomes.ts`).
|
|
7
11
|
*
|
|
8
12
|
* The format's producer is `renderMissionChecksJson` in
|
|
9
13
|
* `packages/backend/src/integrations/e2b/missionFiles.ts`; this package does
|
|
@@ -13,6 +17,11 @@
|
|
|
13
17
|
*/
|
|
14
18
|
import { readFileSync } from "node:fs";
|
|
15
19
|
export const MISSION_CHECKS_FILE_NAME = "checks.json";
|
|
20
|
+
export const MISSION_CI_RESULTS_FILE_NAME = "ci-results.json";
|
|
21
|
+
/** `ci-results.json` (version 1), as the host reads it. */
|
|
22
|
+
export function renderCiResults(checks) {
|
|
23
|
+
return `${JSON.stringify({ version: 1, checks }, null, 2)}\n`;
|
|
24
|
+
}
|
|
16
25
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
26
|
const isString = (value) => typeof value === "string";
|
|
18
27
|
function stringList(value) {
|
|
@@ -76,8 +85,27 @@ function parseJob(value) {
|
|
|
76
85
|
return null;
|
|
77
86
|
job[key] = list;
|
|
78
87
|
}
|
|
88
|
+
if (value.failedCheck !== undefined) {
|
|
89
|
+
const failed = value.failedCheck;
|
|
90
|
+
if (!isRecord(failed) || !isString(failed.name))
|
|
91
|
+
return null;
|
|
92
|
+
if (failed.step !== undefined && !isString(failed.step))
|
|
93
|
+
return null;
|
|
94
|
+
job.failedCheck = { name: failed.name, ...(isString(failed.step) ? { step: failed.step } : {}) };
|
|
95
|
+
}
|
|
79
96
|
return job;
|
|
80
97
|
}
|
|
98
|
+
function parseCannotRunChecks(value) {
|
|
99
|
+
if (!Array.isArray(value))
|
|
100
|
+
return null;
|
|
101
|
+
const entries = [];
|
|
102
|
+
for (const raw of value) {
|
|
103
|
+
if (!isRecord(raw) || !isString(raw.name) || !isString(raw.reason))
|
|
104
|
+
return null;
|
|
105
|
+
entries.push({ name: raw.name, reason: raw.reason });
|
|
106
|
+
}
|
|
107
|
+
return entries;
|
|
108
|
+
}
|
|
81
109
|
/** The file, or null when the text is not a well-formed version 1 file. */
|
|
82
110
|
export function parseMissionChecksFile(text) {
|
|
83
111
|
let parsed;
|
|
@@ -112,7 +140,12 @@ export function parseMissionChecksFile(text) {
|
|
|
112
140
|
return null;
|
|
113
141
|
ciJobs.push(job);
|
|
114
142
|
}
|
|
115
|
-
|
|
143
|
+
if (parsed.failedChecksCannotRun === undefined)
|
|
144
|
+
return { version: 1, checks, ciJobs };
|
|
145
|
+
const failedChecksCannotRun = parseCannotRunChecks(parsed.failedChecksCannotRun);
|
|
146
|
+
if (!failedChecksCannotRun)
|
|
147
|
+
return null;
|
|
148
|
+
return { version: 1, checks, ciJobs, failedChecksCannotRun };
|
|
116
149
|
}
|
|
117
150
|
/**
|
|
118
151
|
* Read the file. `missing` (no such file: a host that predates it, or a run
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* findings, handoffs, or run records (§0.9).
|
|
48
48
|
*/
|
|
49
49
|
import { type ToolchainFile } from "../toolchain/spawnHook.js";
|
|
50
|
-
import { type MissionChecksFile } from "./missionChecks.js";
|
|
50
|
+
import { type FailedCheckResult, type MissionChecksFile } from "./missionChecks.js";
|
|
51
51
|
import type { Finding } from "./types.js";
|
|
52
52
|
/**
|
|
53
53
|
* Total wall-clock budget for a single verify run before it is abandoned
|
|
@@ -329,6 +329,11 @@ export interface MakeRunVerifyOpts {
|
|
|
329
329
|
* jobs). Absent or null: no repository-checks half, as before.
|
|
330
330
|
*/
|
|
331
331
|
repoChecks?: () => MissionChecksFile | null;
|
|
332
|
+
/**
|
|
333
|
+
* How each failed CI check's job went (the jobs `checks.json` marks), after
|
|
334
|
+
* every repository-checks half that ran: the host reads the last one.
|
|
335
|
+
*/
|
|
336
|
+
onFailedChecks?: (results: FailedCheckResult[]) => void;
|
|
332
337
|
/**
|
|
333
338
|
* This run's own changed paths (repo-relative), which scope the CI jobs by
|
|
334
339
|
* their `paths` filters. Default: the same `git status` the scope reads,
|
|
@@ -361,6 +361,14 @@ export function classifyVerifyInfraFailure(output, exitCode) {
|
|
|
361
361
|
}
|
|
362
362
|
if (/\bspawn(?:Sync)?\b.*\bENOENT\b/.test(output))
|
|
363
363
|
return "spawn failed (ENOENT)";
|
|
364
|
+
// uv's and make's words for a tool that is not installed (`uv run ruff` with
|
|
365
|
+
// ruff absent from the environment, a Makefile recipe calling it).
|
|
366
|
+
if (/\bFailed to spawn: `[^`\n]+`\s+Caused by: No such file or directory \(os error 2\)/.test(output)) {
|
|
367
|
+
return "command not found (not installed in the environment)";
|
|
368
|
+
}
|
|
369
|
+
if (/^make(?:\[\d+\])?: \*\*\* \[[^\]\n]*\] Error 127$/m.test(output)) {
|
|
370
|
+
return "command not found (a Makefile recipe's tool is not installed)";
|
|
371
|
+
}
|
|
364
372
|
return null;
|
|
365
373
|
}
|
|
366
374
|
/**
|
|
@@ -969,11 +977,31 @@ export function makeRunVerify(opts = {}) {
|
|
|
969
977
|
cannotRun.push(`\`${check.command}\`${check.cause ? ` (${check.cause})` : ""}`);
|
|
970
978
|
}
|
|
971
979
|
const paths = await changedPaths(cwd, signal, input.baselinePaths);
|
|
972
|
-
|
|
980
|
+
// A job the pull request's CI reported failed runs whatever its paths say.
|
|
981
|
+
const jobs = file.ciJobs.filter((job) => job.failedCheck !== undefined || ciJobInScope(job, paths));
|
|
973
982
|
for (const job of jobs) {
|
|
974
983
|
for (const entry of job.cannotRun)
|
|
975
984
|
cannotRun.push(`CI ${job.name}, "${entry.step}" (${entry.reason})`);
|
|
976
985
|
}
|
|
986
|
+
for (const failed of file.failedChecksCannotRun ?? []) {
|
|
987
|
+
cannotRun.push(`failed CI check "${failed.name}" (${failed.reason})`);
|
|
988
|
+
}
|
|
989
|
+
const failedJobs = new Map();
|
|
990
|
+
for (const job of jobs) {
|
|
991
|
+
if (!job.failedCheck)
|
|
992
|
+
continue;
|
|
993
|
+
failedJobs.set(job, job.steps.length > 0
|
|
994
|
+
? { name: job.failedCheck.name, status: "not_reached" }
|
|
995
|
+
: { name: job.failedCheck.name, status: "cannot_run", reason: job.cannotRun[0]?.reason ?? "none of its steps can run in the sandbox" });
|
|
996
|
+
}
|
|
997
|
+
const report = () => {
|
|
998
|
+
if (failedJobs.size === 0 && !file.failedChecksCannotRun?.length)
|
|
999
|
+
return;
|
|
1000
|
+
opts.onFailedChecks?.([
|
|
1001
|
+
...failedJobs.values(),
|
|
1002
|
+
...(file.failedChecksCannotRun ?? []).map((failed) => ({ name: failed.name, status: "cannot_run", reason: failed.reason })),
|
|
1003
|
+
]);
|
|
1004
|
+
};
|
|
977
1005
|
if (cannotRun.length > 0) {
|
|
978
1006
|
const named = cannotRun.slice(0, MAX_NAMED_CANNOT_RUN).join("; ");
|
|
979
1007
|
const more = cannotRun.length > MAX_NAMED_CANNOT_RUN ? `; and ${cannotRun.length - MAX_NAMED_CANNOT_RUN} more` : "";
|
|
@@ -981,8 +1009,10 @@ export function makeRunVerify(opts = {}) {
|
|
|
981
1009
|
}
|
|
982
1010
|
const approved = file.checks.filter((check) => check.status !== "cannot_run");
|
|
983
1011
|
const steps = jobs.flatMap((job) => job.steps.map((step) => ({ job, step })));
|
|
984
|
-
if (approved.length === 0 && steps.length === 0)
|
|
1012
|
+
if (approved.length === 0 && steps.length === 0) {
|
|
1013
|
+
report();
|
|
985
1014
|
return { findings, reasons, labels };
|
|
1015
|
+
}
|
|
986
1016
|
const buildLeft = (opts.repoChecksBuildBudgetMs ?? REPO_CHECKS_BUILD_BUDGET_MS) - repoChecksSpentMs;
|
|
987
1017
|
if (buildLeft < REPO_CHECK_MIN_START_MS) {
|
|
988
1018
|
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");
|
|
@@ -999,6 +1029,8 @@ export function makeRunVerify(opts = {}) {
|
|
|
999
1029
|
reasons,
|
|
1000
1030
|
labels,
|
|
1001
1031
|
notReached: 0,
|
|
1032
|
+
failedJobs,
|
|
1033
|
+
approvedFailed: new Set(),
|
|
1002
1034
|
};
|
|
1003
1035
|
const before = await treeStatus(cwd, signal).catch((err) => {
|
|
1004
1036
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1010,6 +1042,9 @@ export function makeRunVerify(opts = {}) {
|
|
|
1010
1042
|
await runCiSteps(run, steps, new Set(file.checks.map((check) => check.command.trim())));
|
|
1011
1043
|
}
|
|
1012
1044
|
finally {
|
|
1045
|
+
// Whatever ran (an aborted half included) is what the host reads. A half
|
|
1046
|
+
// skipped for the spent budget, above, writes nothing: the last real run stands.
|
|
1047
|
+
report();
|
|
1013
1048
|
repoChecksSpentMs += now() - halfStartedAt;
|
|
1014
1049
|
// What the checks wrote into the tree (a coverage folder, an updated
|
|
1015
1050
|
// snapshot, a build output) is not the build's change: undone, so the
|
|
@@ -1075,6 +1110,8 @@ export function makeRunVerify(opts = {}) {
|
|
|
1075
1110
|
}
|
|
1076
1111
|
const label = check.command.length > 80 ? `${check.command.slice(0, 77)}...` : check.command;
|
|
1077
1112
|
const verdict = await runRepoCommand(run, check.command, run.cwd, undefined);
|
|
1113
|
+
if (verdict.kind === "failed")
|
|
1114
|
+
run.approvedFailed.add(check.command.trim());
|
|
1078
1115
|
if (verdict.kind === "note") {
|
|
1079
1116
|
run.reasons.push(`approved check \`${label}\` ${verdict.reason}`);
|
|
1080
1117
|
}
|
|
@@ -1099,11 +1136,35 @@ export function makeRunVerify(opts = {}) {
|
|
|
1099
1136
|
*/
|
|
1100
1137
|
async function runCiSteps(run, steps, approvedCommands) {
|
|
1101
1138
|
const stoppedJobs = new Set();
|
|
1139
|
+
// A marked job's result: the first step that does not pass decides it;
|
|
1140
|
+
// every step passing (or being an approved check that passed) is a pass.
|
|
1141
|
+
const passedSteps = new Map();
|
|
1142
|
+
const settle = (job, status, reason) => {
|
|
1143
|
+
const result = run.failedJobs.get(job);
|
|
1144
|
+
if (!result || result.status === "failed" || result.status === "cannot_run")
|
|
1145
|
+
return;
|
|
1146
|
+
result.status = status;
|
|
1147
|
+
if (reason)
|
|
1148
|
+
result.reason = reason;
|
|
1149
|
+
};
|
|
1150
|
+
const stepPassed = (job) => {
|
|
1151
|
+
const count = (passedSteps.get(job) ?? 0) + 1;
|
|
1152
|
+
passedSteps.set(job, count);
|
|
1153
|
+
if (count === job.steps.length)
|
|
1154
|
+
settle(job, "passed");
|
|
1155
|
+
};
|
|
1102
1156
|
for (const { job, step } of steps) {
|
|
1103
1157
|
if (run.signal?.aborted)
|
|
1104
1158
|
return;
|
|
1105
|
-
if (stoppedJobs.has(job)
|
|
1159
|
+
if (stoppedJobs.has(job))
|
|
1106
1160
|
continue;
|
|
1161
|
+
if (approvedCommands.has(step.run.trim())) {
|
|
1162
|
+
if (run.approvedFailed.has(step.run.trim()))
|
|
1163
|
+
settle(job, "failed");
|
|
1164
|
+
else
|
|
1165
|
+
stepPassed(job);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1107
1168
|
if (run.left() < REPO_CHECK_MIN_START_MS) {
|
|
1108
1169
|
run.notReached += 1;
|
|
1109
1170
|
continue;
|
|
@@ -1115,13 +1176,16 @@ export function makeRunVerify(opts = {}) {
|
|
|
1115
1176
|
if (verdict.kind === "passed") {
|
|
1116
1177
|
lastCiFailure.delete(key);
|
|
1117
1178
|
run.labels.push(`CI ${job.name}: ${step.name}`);
|
|
1179
|
+
stepPassed(job);
|
|
1118
1180
|
continue;
|
|
1119
1181
|
}
|
|
1120
1182
|
stoppedJobs.add(job);
|
|
1121
1183
|
if (verdict.kind === "note") {
|
|
1122
1184
|
run.reasons.push(`${where} ${verdict.reason}`);
|
|
1185
|
+
settle(job, "cannot_run", `step "${step.name}" ${verdict.reason}`);
|
|
1123
1186
|
continue;
|
|
1124
1187
|
}
|
|
1188
|
+
settle(job, "failed");
|
|
1125
1189
|
const stepFindings = repoFailureFindings(run, verdict, step.name, where, dir);
|
|
1126
1190
|
const keys = new Set(stepFindings.map((f) => f.message));
|
|
1127
1191
|
const previous = lastCiFailure.get(key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.2.6-staging.
|
|
3
|
+
"version": "1.2.6-staging.1886.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": "8d2eb70b49e6d282e8587ac8458423f8249a4953"
|
|
62
62
|
}
|