@feigi/fleet-ctl 3.82.12 → 3.82.14
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/agents/fleet-implementer-slow-high.agent.md +1 -1
- package/agents/fleet-implementer-slow-medium.agent.md +1 -1
- package/agents/fleet-implementer-smol-high.agent.md +1 -1
- package/agents/fleet-implementer-task-high.agent.md +1 -1
- package/agents/fleet-implementer-task-max.agent.md +1 -1
- package/commands/review-and-fix.md +1 -0
- package/package.json +1 -1
- package/scripts/fleet-tick.mjs +32 -14
- package/scripts/ledger-grammar.mjs +14 -0
- package/scripts/ledger.mjs +52 -4
- package/scripts/pull.mjs +8 -5
- package/scripts/recipe-check.mjs +260 -0
- package/scripts/report-files.mjs +67 -8
- package/skills/next-ticket/SKILL.md +12 -2
- package/skills/run-team/SKILL.md +41 -520
- package/skills/run-team/briefs/fix-applier.md +13 -3
- package/skills/run-team/briefs/implementer.md +8 -0
- package/skills/run-team/references/isolation.md +1 -1
- package/skills/run-team/references/member-lifecycle.md +46 -0
- package/skills/run-team/references/phase-0.md +2 -2
- package/skills/run-team/references/pull.md +31 -5
- package/skills/run-team/references/reviewers.md +5 -1
- package/skills/run-team/references/shell-traps.md +2 -0
- package/skills/run-team/references/tick-rows.md +37 -5
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Run the Recipe's Check step and the Test entrypoint against the staged tree,
|
|
3
|
+
// and record that both passed — the record `ledger.mjs settle --from-report`
|
|
4
|
+
// requires before it settles an implementer's or a fix-applier's report.
|
|
5
|
+
//
|
|
6
|
+
// recipe-check.mjs --member <name> --testCmd <command> [--reports <dir>]
|
|
7
|
+
//
|
|
8
|
+
// Run from the member's worktree, after `git add` and before the commit and
|
|
9
|
+
// push, and again after any content change or rebase. Reached through the
|
|
10
|
+
// Resolver: `~/.fleet/bin/fleet-run recipe-check.mjs ...`.
|
|
11
|
+
//
|
|
12
|
+
// `--member` is the `impl-<N>[-x]` or `fix-pr-<M>[-x]` name the member was
|
|
13
|
+
// dispatched as. `--testCmd` is the Test entrypoint exactly as the member was
|
|
14
|
+
// handed it, a shell command run from the worktree root: the member's runner
|
|
15
|
+
// already goes through the suite-slot wrapper, so this script adds none of its
|
|
16
|
+
// own and never runs the suite a second time. The Check step is not an
|
|
17
|
+
// argument: it is read off the repository's Recipe cache, through
|
|
18
|
+
// derive-testcmd.sh, the one reader of it. `--reports` is the directory the
|
|
19
|
+
// member's report is written to, `<scratch>/reports`; omitted, the default
|
|
20
|
+
// scratch root's.
|
|
21
|
+
//
|
|
22
|
+
// Steps, in this order:
|
|
23
|
+
//
|
|
24
|
+
// 1 read the Check step and the Test entrypoint off the Recipe cache. A
|
|
25
|
+
// worktree with no usable Recipe has no Check step to run: exit 2,
|
|
26
|
+
// nothing run, nothing written. Settle reads the same cache from the
|
|
27
|
+
// ledger's repository, where derive-testcmd.sh's probes of the Test
|
|
28
|
+
// entrypoint can answer otherwise, so it skips its gate only where its
|
|
29
|
+
// own read refuses.
|
|
30
|
+
// 2 `git write-tree`: the tree id of the staged index. The commands run in
|
|
31
|
+
// the working tree, so it must hold exactly that tree: an unstaged change
|
|
32
|
+
// to a tracked file, or an untracked file no ignore rule covers, is a
|
|
33
|
+
// refusal, exit 2 — a green record would otherwise name a tree that never
|
|
34
|
+
// ran. Then the reports directory is created, before anything runs.
|
|
35
|
+
// 3 the Check step, `sh -c` from the worktree root. A Check step of
|
|
36
|
+
// `true` is recorded as green without being run.
|
|
37
|
+
// 4 the Test entrypoint, the same way — only when the Check step passed.
|
|
38
|
+
//
|
|
39
|
+
// The commands' own output passes through on stdout and stderr as it arrives.
|
|
40
|
+
//
|
|
41
|
+
// THE RECORD is `<reports>/<member>.check.json`, beside the member's report
|
|
42
|
+
// (report-files.mjs names the path and reads it back), written whole and
|
|
43
|
+
// atomically once both commands have settled:
|
|
44
|
+
//
|
|
45
|
+
// { member, tree, check: {command, ran, exitCode, pass, fail},
|
|
46
|
+
// test: {command, ran, exitCode, pass, fail} }
|
|
47
|
+
//
|
|
48
|
+
// tree the `git write-tree` of step 2. Settle compares it with the
|
|
49
|
+
// reported head's own tree, so a message-only amend keeps the
|
|
50
|
+
// record valid and a rebase onto a moved base, or any content
|
|
51
|
+
// change, does not.
|
|
52
|
+
// ran false for a Check step of `true`, and for a command skipped
|
|
53
|
+
// because the Check step failed (its exitCode is then null).
|
|
54
|
+
// exitCode the command's status, a signal's number plus 128.
|
|
55
|
+
// pass, fail the counts the runner's summary printed — a `# pass N` /
|
|
56
|
+
// `ℹ fail N` line, or `N passed`/`N failed`/`N passing`/`N
|
|
57
|
+
// failing` — null where it printed none.
|
|
58
|
+
//
|
|
59
|
+
// A record is written whenever a command ran and failed, so a red run
|
|
60
|
+
// replaces an older record rather than leaving it standing; it records the
|
|
61
|
+
// failure, and settle refuses on it. A record is a claim, no attestation: the
|
|
62
|
+
// member produces it, and nothing here stops a forged one.
|
|
63
|
+
//
|
|
64
|
+
// Exit status: 0 both commands passed and the record is written; 1 a command
|
|
65
|
+
// failed — the record says which, and the script's stderr names it; 2 a
|
|
66
|
+
// refusal that ran nothing and wrote nothing (usage, no usable Recipe, git
|
|
67
|
+
// could not be read, a working tree that is not the staged tree, a reports
|
|
68
|
+
// directory that could not be created); 75 the Test entrypoint printed exit
|
|
69
|
+
// 75 — the suite wrapper found no slot and launched nothing, which is no test
|
|
70
|
+
// result — so no record is written, and the same command is to be run again.
|
|
71
|
+
|
|
72
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
73
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
74
|
+
import { constants as osConstants } from "node:os";
|
|
75
|
+
import { dirname, join } from "node:path";
|
|
76
|
+
import { fileURLToPath } from "node:url";
|
|
77
|
+
import { makeDie, defineFlags, writeAll } from "./arg.mjs";
|
|
78
|
+
import { FleetDirUnresolvable } from "./fleet-dir.mjs";
|
|
79
|
+
import { gitEnv } from "./git-env.mjs";
|
|
80
|
+
import { isCLI } from "./is-cli.mjs";
|
|
81
|
+
import { parseMember } from "./ledger-grammar.mjs";
|
|
82
|
+
import { checkRecordPathIn, defaultScratch, reportsDir } from "./report-files.mjs";
|
|
83
|
+
|
|
84
|
+
const NAME = "recipe-check";
|
|
85
|
+
const USAGE = "usage: recipe-check.mjs --member <name> --testCmd <command> [--reports <dir>]";
|
|
86
|
+
const DERIVE = join(dirname(fileURLToPath(import.meta.url)), "derive-testcmd.sh");
|
|
87
|
+
const GIT_TIMEOUT_MS = 30_000;
|
|
88
|
+
// What the suite wrapper exits with when it found no slot and launched nothing.
|
|
89
|
+
const NO_SLOT = 75;
|
|
90
|
+
// The tail of a command's output kept for the counts: a runner prints its
|
|
91
|
+
// summary last.
|
|
92
|
+
const TAIL_BYTES = 256 * 1024;
|
|
93
|
+
|
|
94
|
+
const ANSI = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
95
|
+
|
|
96
|
+
/** `{pass, fail}` the output's summary printed, each null where it printed no
|
|
97
|
+
* count. The last count of a kind wins: a summary follows the lines it sums. */
|
|
98
|
+
export function parseCounts(output) {
|
|
99
|
+
const text = output.replace(ANSI, "");
|
|
100
|
+
const last = (patterns) => {
|
|
101
|
+
for (const re of patterns) {
|
|
102
|
+
const all = [...text.matchAll(re)];
|
|
103
|
+
if (all.length > 0) return Number(all.at(-1)[1]);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
};
|
|
107
|
+
const count = (word, loose) => last([new RegExp(`^[ \\t]*(?:#|ℹ)[ \\t]+${word}[ \\t]+(\\d+)[ \\t]*$`, "gm"), loose]);
|
|
108
|
+
return {
|
|
109
|
+
pass: count("pass", /\b(\d+)[ \t]+(?:passed|passing)\b/g),
|
|
110
|
+
fail: count("fail", /\b(\d+)[ \t]+(?:failed|failing)\b/g),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function cli() {
|
|
115
|
+
const die = makeDie(NAME);
|
|
116
|
+
const { arg, sweep, stray } = defineFlags(die, { flags: { member: "value", testCmd: "value", reports: "value" } });
|
|
117
|
+
sweep();
|
|
118
|
+
stray();
|
|
119
|
+
const member = arg("member");
|
|
120
|
+
const testCmd = arg("testCmd");
|
|
121
|
+
let reports = arg("reports");
|
|
122
|
+
if (member === null || testCmd === null) die(USAGE);
|
|
123
|
+
const parsed = parseMember(member);
|
|
124
|
+
if (parsed === null || (parsed.family !== "impl" && parsed.family !== "fix-pr")) {
|
|
125
|
+
die(`--member '${member}' is not an impl-<N>[-x] or fix-pr-<M>[-x] name — ${USAGE}`);
|
|
126
|
+
}
|
|
127
|
+
if (testCmd.trim() === "") die(`--testCmd is empty — ${USAGE}`);
|
|
128
|
+
if (reports === null) {
|
|
129
|
+
try {
|
|
130
|
+
reports = reportsDir(defaultScratch());
|
|
131
|
+
} catch (e) {
|
|
132
|
+
if (!(e instanceof FleetDirUnresolvable)) throw e;
|
|
133
|
+
die(`no --reports, and the scratch root could not be resolved — ${e.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const env = gitEnv();
|
|
138
|
+
// A reader that did not answer: the refusal names it, its exit status and
|
|
139
|
+
// its own reason.
|
|
140
|
+
const noAnswer = (what, r) => {
|
|
141
|
+
const why = (r.error?.message ?? r.stderr ?? "").trim();
|
|
142
|
+
die(`${what} did not answer${r.status === null ? "" : ` (exit ${r.status})`}${why ? `: ${why}` : ""}; nothing run, nothing written`);
|
|
143
|
+
};
|
|
144
|
+
const gitOut = (what, args) => {
|
|
145
|
+
const r = spawnSync("git", args, { encoding: "utf8", env, timeout: GIT_TIMEOUT_MS });
|
|
146
|
+
if (r.status !== 0) noAnswer(what, r);
|
|
147
|
+
return r.stdout.trim();
|
|
148
|
+
};
|
|
149
|
+
const top = gitOut("git rev-parse --show-toplevel", ["rev-parse", "--show-toplevel"]);
|
|
150
|
+
|
|
151
|
+
// 1: the Recipe. Exit 1 is derive-testcmd.sh's refusal about the cache; any
|
|
152
|
+
// other failure is a reader that could not run.
|
|
153
|
+
const field = (name) => {
|
|
154
|
+
const r = spawnSync("sh", [DERIVE, top, name], { encoding: "utf8", env, timeout: GIT_TIMEOUT_MS });
|
|
155
|
+
if (r.status === 0) return r.stdout.replace(/\n$/, "");
|
|
156
|
+
if (r.status === 1) {
|
|
157
|
+
die(`this worktree has no usable Recipe, so there is no Check step to run; settle reads the Recipe from the ledger's repository and gates the report wherever that read finds one; nothing run, nothing written — ${(r.stderr ?? "").trim()}`);
|
|
158
|
+
}
|
|
159
|
+
noAnswer(`derive-testcmd.sh ${name}`, r);
|
|
160
|
+
};
|
|
161
|
+
const checkCmd = field("check");
|
|
162
|
+
field("test");
|
|
163
|
+
|
|
164
|
+
// 2: the staged tree, and a working tree that holds exactly it.
|
|
165
|
+
const tree = gitOut("git write-tree", ["write-tree"]);
|
|
166
|
+
const strays = [
|
|
167
|
+
...gitOut("git diff --name-only", ["-C", top, "diff", "--name-only"]).split("\n"),
|
|
168
|
+
...gitOut("git ls-files --others --exclude-standard", ["-C", top, "ls-files", "--others", "--exclude-standard", "--directory"]).split("\n"),
|
|
169
|
+
].filter((p) => p !== "");
|
|
170
|
+
if (strays.length > 0) {
|
|
171
|
+
const shown = strays.slice(0, 10).join(", ") + (strays.length > 10 ? `, and ${strays.length - 10} more` : "");
|
|
172
|
+
die(`the working tree is not the staged tree — unstaged or untracked: ${shown}. The commands run in the working tree and the record names the staged tree, so stage, remove or ignore these first; nothing run, nothing written`);
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
mkdirSync(reports, { recursive: true });
|
|
176
|
+
} catch (e) {
|
|
177
|
+
die(`could not create ${reports}: ${e.code ?? e.message}; nothing run, nothing written`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 3 and 4: the commands. A reader that closes this script's stdout or
|
|
181
|
+
// stderr early stops only the passthrough: the commands still run to their
|
|
182
|
+
// end and the record is still written. Any other stream error stays fatal.
|
|
183
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
184
|
+
stream.on("error", (e) => {
|
|
185
|
+
if (e.code !== "EPIPE") throw e;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const check = checkCmd.trim() === "true"
|
|
189
|
+
? { command: checkCmd, ran: false, exitCode: 0, pass: null, fail: null }
|
|
190
|
+
: await runCommand(checkCmd, top, env);
|
|
191
|
+
const test = check.exitCode === 0
|
|
192
|
+
? await runCommand(testCmd, top, env)
|
|
193
|
+
: { command: testCmd, ran: false, exitCode: null, pass: null, fail: null };
|
|
194
|
+
|
|
195
|
+
// process.exitCode, never process.exit(): the command's own output is still
|
|
196
|
+
// draining through async pipe writes, and exit discards what is queued.
|
|
197
|
+
const say = (line) => writeAll(2, `\n${NAME}: ${line}\n`);
|
|
198
|
+
if (test.exitCode === NO_SLOT) {
|
|
199
|
+
say(`the Test entrypoint exited ${NO_SLOT}: no suite slot was free and nothing was launched — run the same command again; no record written`);
|
|
200
|
+
process.exitCode = NO_SLOT;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const path = checkRecordPathIn(reports, member);
|
|
205
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
206
|
+
try {
|
|
207
|
+
writeFileSync(tmp, `${JSON.stringify({ member, tree, check, test })}\n`);
|
|
208
|
+
renameSync(tmp, path);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
rmSync(tmp, { force: true });
|
|
211
|
+
die(`could not write ${path}: ${e.code ?? e.message}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (check.exitCode !== 0) {
|
|
215
|
+
say(`the Check step exited ${check.exitCode}; red record written to ${path}, so settle refuses it — fix the cause and run again`);
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
} else if (test.exitCode !== 0) {
|
|
218
|
+
say(`the Test entrypoint exited ${test.exitCode}; red record written to ${path}, so settle refuses it — fix the cause and run again`);
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
} else {
|
|
221
|
+
say(`green on tree ${tree}; record written to ${path}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (isCLI(import.meta.url)) await cli();
|
|
226
|
+
|
|
227
|
+
// One command, `sh -c` from `cwd`, its output passed through and its tail kept
|
|
228
|
+
// for the counts. A signal that stops it is its number plus 128.
|
|
229
|
+
function runCommand(command, cwd, env) {
|
|
230
|
+
return new Promise((resolve) => {
|
|
231
|
+
let tail = "";
|
|
232
|
+
const keep = (chunk) => {
|
|
233
|
+
tail = (tail + chunk).slice(-TAIL_BYTES);
|
|
234
|
+
};
|
|
235
|
+
const child = spawn("sh", ["-c", command], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
236
|
+
const handlers = ["SIGTERM", "SIGINT", "SIGHUP"].map((sig) => [sig, () => child.kill(sig)]);
|
|
237
|
+
for (const [sig, h] of handlers) process.on(sig, h);
|
|
238
|
+
child.stdout.setEncoding("utf8");
|
|
239
|
+
child.stderr.setEncoding("utf8");
|
|
240
|
+
child.stdout.on("data", (d) => {
|
|
241
|
+
keep(d);
|
|
242
|
+
process.stdout.write(d);
|
|
243
|
+
});
|
|
244
|
+
child.stderr.on("data", (d) => {
|
|
245
|
+
keep(d);
|
|
246
|
+
process.stderr.write(d);
|
|
247
|
+
});
|
|
248
|
+
const done = (exitCode) => {
|
|
249
|
+
for (const [sig, h] of handlers) process.off(sig, h);
|
|
250
|
+
const { pass, fail } = parseCounts(tail);
|
|
251
|
+
resolve({ command, ran: true, exitCode, pass, fail });
|
|
252
|
+
};
|
|
253
|
+
// A spawn that never started (a missing `sh`) is the command not passing.
|
|
254
|
+
child.on("error", (e) => {
|
|
255
|
+
process.stderr.write(`${NAME}: could not start sh: ${e.message}\n`);
|
|
256
|
+
done(127);
|
|
257
|
+
});
|
|
258
|
+
child.on("close", (code, signal) => done(code ?? 128 + (osConstants.signals[signal] ?? 0)));
|
|
259
|
+
});
|
|
260
|
+
}
|
package/scripts/report-files.mjs
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
// SCHEMAS. The review runner and every member outside those four families
|
|
10
10
|
// have no schema, so no file to look up.
|
|
11
11
|
//
|
|
12
|
+
// The Check-step record beside a report, `<reports>/<member>.check.json`, is
|
|
13
|
+
// looked up here the same way: recipe-check.mjs writes it, and this module
|
|
14
|
+
// only names where it lives and reads it back.
|
|
15
|
+
//
|
|
12
16
|
// This module only looks files up: it writes none, and creates no scratch
|
|
13
17
|
// root.
|
|
14
18
|
|
|
@@ -32,6 +36,66 @@ export const reportsDir = (scratch) => join(scratch, "reports");
|
|
|
32
36
|
|
|
33
37
|
export const reportPath = (scratch, member) => join(reportsDir(scratch), `${member}.json`);
|
|
34
38
|
|
|
39
|
+
/** Where `member`'s Check-step record sits in reports directory `dir`: the
|
|
40
|
+
* sibling of its report, `<dir>/<member>.check.json`. recipe-check.mjs writes
|
|
41
|
+
* it, `ledger.mjs settle --from-report` reads it. */
|
|
42
|
+
export const checkRecordPathIn = (dir, member) => join(dir, `${member}.check.json`);
|
|
43
|
+
|
|
44
|
+
const HEX_TREE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
45
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
46
|
+
|
|
47
|
+
// One command's half of a record: the command string, whether it ran (a Check
|
|
48
|
+
// step of `true` is recorded as green without running), the exit status it
|
|
49
|
+
// ended with — null when it never ran because an earlier one failed — and
|
|
50
|
+
// the pass/fail counts its output printed, null where the runner printed none.
|
|
51
|
+
function commandViolation(path, c) {
|
|
52
|
+
if (!isObject(c)) return `${path}: not an object`;
|
|
53
|
+
if (typeof c.command !== "string" || c.command === "") return `${path}.command: not a non-empty string`;
|
|
54
|
+
if (typeof c.ran !== "boolean") return `${path}.ran: not a boolean`;
|
|
55
|
+
if (c.exitCode !== null && !Number.isInteger(c.exitCode)) return `${path}.exitCode: not an integer or null`;
|
|
56
|
+
for (const k of ["pass", "fail"]) {
|
|
57
|
+
if (c[k] !== null && !(Number.isInteger(c[k]) && c[k] >= 0)) return `${path}.${k}: not a non-negative integer or null`;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A file's text as `{text}`, or the answer both readers below give for a file
|
|
63
|
+
// that is not there (`absent`) or cannot be read (`invalid`).
|
|
64
|
+
function readAt(path) {
|
|
65
|
+
try {
|
|
66
|
+
return { text: readFileSync(path, "utf8") };
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") return { state: "absent", path };
|
|
69
|
+
return { state: "invalid", path, violation: `$: unreadable (${e.code ?? e.message})` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The Check-step record of `member` in reports directory `dir`, the way
|
|
74
|
+
* readReportIn() answers for a report: `{state: "ok", path, record}`,
|
|
75
|
+
* `{state: "invalid", path, violation}` for a file that exists and is
|
|
76
|
+
* unreadable, not JSON, or the wrong shape, or `{state: "absent", path}`.
|
|
77
|
+
* The record is `{member, tree, check, test}`, `tree` the `git write-tree`
|
|
78
|
+
* of the index the two commands ran against and `check`/`test` each
|
|
79
|
+
* `{command, ran, exitCode, pass, fail}`. */
|
|
80
|
+
export function readCheckRecordIn(dir, member) {
|
|
81
|
+
const path = checkRecordPathIn(dir, member);
|
|
82
|
+
const read = readAt(path);
|
|
83
|
+
if (read.text === undefined) return read;
|
|
84
|
+
let record;
|
|
85
|
+
try {
|
|
86
|
+
record = JSON.parse(read.text);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return { state: "invalid", path, violation: `$: not JSON (${e.message})` };
|
|
89
|
+
}
|
|
90
|
+
if (!isObject(record)) return { state: "invalid", path, violation: "$: not an object" };
|
|
91
|
+
if (record.member !== member) return { state: "invalid", path, violation: `$.member: ${JSON.stringify(record.member)} is not ${member}` };
|
|
92
|
+
if (typeof record.tree !== "string" || !HEX_TREE.test(record.tree)) {
|
|
93
|
+
return { state: "invalid", path, violation: "$.tree: not a full lowercase hex tree id" };
|
|
94
|
+
}
|
|
95
|
+
const violation = commandViolation("$.check", record.check) ?? commandViolation("$.test", record.test);
|
|
96
|
+
return violation === null ? { state: "ok", path, record } : { state: "invalid", path, violation };
|
|
97
|
+
}
|
|
98
|
+
|
|
35
99
|
/** The report file of `member` under `scratch`, read and checked against its
|
|
36
100
|
* family's schema: `{state: "ok", path, report}`, `{state: "invalid", path,
|
|
37
101
|
* violation}` for a file that exists and is unreadable, not JSON, or the
|
|
@@ -46,14 +110,9 @@ export function readReportIn(dir, member) {
|
|
|
46
110
|
const family = reportFamily(member);
|
|
47
111
|
if (family === null) throw new Error(`no report schema for '${member}'`);
|
|
48
112
|
const path = join(dir, `${member}.json`);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} catch (e) {
|
|
53
|
-
if (e.code === "ENOENT" || e.code === "ENOTDIR") return { state: "absent", path };
|
|
54
|
-
return { state: "invalid", path, violation: `$: unreadable (${e.code ?? e.message})` };
|
|
55
|
-
}
|
|
56
|
-
const parsed = parseReport(text);
|
|
113
|
+
const read = readAt(path);
|
|
114
|
+
if (read.text === undefined) return read;
|
|
115
|
+
const parsed = parseReport(read.text);
|
|
57
116
|
const violation = parsed.violation ?? validate(SCHEMAS[family], parsed.value);
|
|
58
117
|
return violation === null ? { state: "ok", path, report: parsed.value } : { state: "invalid", path, violation };
|
|
59
118
|
}
|
|
@@ -94,8 +94,7 @@ Come back here. The sizing path hands off; this step always follows.
|
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
git fetch origin && git rebase origin/main # rebase, never merge main in
|
|
97
|
-
<test command>
|
|
98
|
-
<check command> # the Recipe's Check step; re-run after rebasing
|
|
97
|
+
~/.fleet/bin/fleet-run recipe-check.mjs --member <your name> --testCmd "<test command>" --reports <scratch>/reports # re-run after any content change or rebase
|
|
99
98
|
git push --force-with-lease -u origin HEAD
|
|
100
99
|
gh pr create --base main --body "…
|
|
101
100
|
|
|
@@ -103,6 +102,17 @@ Closes #N"
|
|
|
103
102
|
gh pr edit --add-label <patch|minor|major> # own command, own exit status
|
|
104
103
|
```
|
|
105
104
|
|
|
105
|
+
`recipe-check.mjs` runs the Recipe's Check step, then `<test command>` — the
|
|
106
|
+
Recipe's Test entrypoint, as you were handed it, which runs through the suite
|
|
107
|
+
slot already — against the staged tree, and records both results beside your
|
|
108
|
+
report; the controller refuses a report whose head's tree has no green record.
|
|
109
|
+
`<your name>` is the `impl-<N>[-x]` you were dispatched as. The record is keyed
|
|
110
|
+
to the tree, so run it after `git add` and before you commit, and again after
|
|
111
|
+
any content change or rebase: a message-only amend keeps it valid, a changed
|
|
112
|
+
tree does not. A non-zero exit is not a pass, and exit 75 is no test result —
|
|
113
|
+
no slot was free and nothing ran, so run it again. A solo session has no report
|
|
114
|
+
to settle against: it runs the Check step and the Test entrypoint themselves.
|
|
115
|
+
|
|
106
116
|
**A red Check step is yours to judge.** Read what it reports and fix every
|
|
107
117
|
finding in a file the PR changes, including lines you did not edit — CI checks
|
|
108
118
|
whole files. Never edit a file the PR does not otherwise change: a finding there
|