@yagni-app/code-staging 1.2.6-staging.1884.1 → 1.2.6-staging.1892.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 +59 -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,
|
|
@@ -977,11 +977,31 @@ export function makeRunVerify(opts = {}) {
|
|
|
977
977
|
cannotRun.push(`\`${check.command}\`${check.cause ? ` (${check.cause})` : ""}`);
|
|
978
978
|
}
|
|
979
979
|
const paths = await changedPaths(cwd, signal, input.baselinePaths);
|
|
980
|
-
|
|
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));
|
|
981
982
|
for (const job of jobs) {
|
|
982
983
|
for (const entry of job.cannotRun)
|
|
983
984
|
cannotRun.push(`CI ${job.name}, "${entry.step}" (${entry.reason})`);
|
|
984
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
|
+
};
|
|
985
1005
|
if (cannotRun.length > 0) {
|
|
986
1006
|
const named = cannotRun.slice(0, MAX_NAMED_CANNOT_RUN).join("; ");
|
|
987
1007
|
const more = cannotRun.length > MAX_NAMED_CANNOT_RUN ? `; and ${cannotRun.length - MAX_NAMED_CANNOT_RUN} more` : "";
|
|
@@ -989,8 +1009,10 @@ export function makeRunVerify(opts = {}) {
|
|
|
989
1009
|
}
|
|
990
1010
|
const approved = file.checks.filter((check) => check.status !== "cannot_run");
|
|
991
1011
|
const steps = jobs.flatMap((job) => job.steps.map((step) => ({ job, step })));
|
|
992
|
-
if (approved.length === 0 && steps.length === 0)
|
|
1012
|
+
if (approved.length === 0 && steps.length === 0) {
|
|
1013
|
+
report();
|
|
993
1014
|
return { findings, reasons, labels };
|
|
1015
|
+
}
|
|
994
1016
|
const buildLeft = (opts.repoChecksBuildBudgetMs ?? REPO_CHECKS_BUILD_BUDGET_MS) - repoChecksSpentMs;
|
|
995
1017
|
if (buildLeft < REPO_CHECK_MIN_START_MS) {
|
|
996
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");
|
|
@@ -1007,6 +1029,8 @@ export function makeRunVerify(opts = {}) {
|
|
|
1007
1029
|
reasons,
|
|
1008
1030
|
labels,
|
|
1009
1031
|
notReached: 0,
|
|
1032
|
+
failedJobs,
|
|
1033
|
+
approvedFailed: new Set(),
|
|
1010
1034
|
};
|
|
1011
1035
|
const before = await treeStatus(cwd, signal).catch((err) => {
|
|
1012
1036
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1018,6 +1042,9 @@ export function makeRunVerify(opts = {}) {
|
|
|
1018
1042
|
await runCiSteps(run, steps, new Set(file.checks.map((check) => check.command.trim())));
|
|
1019
1043
|
}
|
|
1020
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();
|
|
1021
1048
|
repoChecksSpentMs += now() - halfStartedAt;
|
|
1022
1049
|
// What the checks wrote into the tree (a coverage folder, an updated
|
|
1023
1050
|
// snapshot, a build output) is not the build's change: undone, so the
|
|
@@ -1083,6 +1110,8 @@ export function makeRunVerify(opts = {}) {
|
|
|
1083
1110
|
}
|
|
1084
1111
|
const label = check.command.length > 80 ? `${check.command.slice(0, 77)}...` : check.command;
|
|
1085
1112
|
const verdict = await runRepoCommand(run, check.command, run.cwd, undefined);
|
|
1113
|
+
if (verdict.kind === "failed")
|
|
1114
|
+
run.approvedFailed.add(check.command.trim());
|
|
1086
1115
|
if (verdict.kind === "note") {
|
|
1087
1116
|
run.reasons.push(`approved check \`${label}\` ${verdict.reason}`);
|
|
1088
1117
|
}
|
|
@@ -1107,11 +1136,35 @@ export function makeRunVerify(opts = {}) {
|
|
|
1107
1136
|
*/
|
|
1108
1137
|
async function runCiSteps(run, steps, approvedCommands) {
|
|
1109
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
|
+
};
|
|
1110
1156
|
for (const { job, step } of steps) {
|
|
1111
1157
|
if (run.signal?.aborted)
|
|
1112
1158
|
return;
|
|
1113
|
-
if (stoppedJobs.has(job)
|
|
1159
|
+
if (stoppedJobs.has(job))
|
|
1114
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
|
+
}
|
|
1115
1168
|
if (run.left() < REPO_CHECK_MIN_START_MS) {
|
|
1116
1169
|
run.notReached += 1;
|
|
1117
1170
|
continue;
|
|
@@ -1123,13 +1176,16 @@ export function makeRunVerify(opts = {}) {
|
|
|
1123
1176
|
if (verdict.kind === "passed") {
|
|
1124
1177
|
lastCiFailure.delete(key);
|
|
1125
1178
|
run.labels.push(`CI ${job.name}: ${step.name}`);
|
|
1179
|
+
stepPassed(job);
|
|
1126
1180
|
continue;
|
|
1127
1181
|
}
|
|
1128
1182
|
stoppedJobs.add(job);
|
|
1129
1183
|
if (verdict.kind === "note") {
|
|
1130
1184
|
run.reasons.push(`${where} ${verdict.reason}`);
|
|
1185
|
+
settle(job, "cannot_run", `step "${step.name}" ${verdict.reason}`);
|
|
1131
1186
|
continue;
|
|
1132
1187
|
}
|
|
1188
|
+
settle(job, "failed");
|
|
1133
1189
|
const stepFindings = repoFailureFindings(run, verdict, step.name, where, dir);
|
|
1134
1190
|
const keys = new Set(stepFindings.map((f) => f.message));
|
|
1135
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.1892.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": "f786e1dcc19af7fdd50dcb8acbac4cc44f60920d"
|
|
62
62
|
}
|