@dougharness/cli 1.3.0 → 1.4.0
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/bin.js +13 -4
- package/dist/bin.js.map +1 -1
- package/dist/board-page.d.ts +5 -1
- package/dist/board-page.js +19 -5
- package/dist/board-page.js.map +1 -1
- package/dist/board-serve.js +34 -10
- package/dist/board-serve.js.map +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +160 -13
- package/dist/board.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/hooks/register.tsx +18 -2
- package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +94 -7
- package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +31 -4
- package/node_modules/@dougharness/flow/lib/board.mjs +366 -21
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +194 -20
- package/node_modules/@dougharness/flow/lib/cost.mjs +9 -0
- package/node_modules/@dougharness/flow/lib/memory.mjs +24 -1
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +193 -12
- package/node_modules/@dougharness/flow/scripts/board.mjs +122 -13
- package/node_modules/@dougharness/flow/scripts/cost.mjs +6 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +14 -7
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +11 -4
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +9 -2
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +2 -2
- package/node_modules/@dougharness/flow/workflows/card-queen.js +195 -21
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +21 -18
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/baseline.mjs +23 -0
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +41 -3
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +6 -2
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +91 -18
- package/package.json +3 -3
- package/templates/board-app.js +240 -30
|
@@ -32,6 +32,8 @@ const STOP_REASONS = [
|
|
|
32
32
|
"reviewer-blocker-open",
|
|
33
33
|
"reviewer-major-open",
|
|
34
34
|
"gate-red-twice",
|
|
35
|
+
"gate-timeout",
|
|
36
|
+
"commit-tree-mismatch",
|
|
35
37
|
];
|
|
36
38
|
// The Models-table row each seat runs on. setup, gate and commit have no sourced row; cheap is a choice.
|
|
37
39
|
const SEAT_ROWS = {
|
|
@@ -60,10 +62,18 @@ const BRIEF_RULES = {
|
|
|
60
62
|
text: "If a hook blocks a step, do not work around it: report it in hookBlocked with the hook's message.",
|
|
61
63
|
seats: ALL,
|
|
62
64
|
},
|
|
65
|
+
"relayed-message-outside-card": {
|
|
66
|
+
text: "Your task is the card in this brief. A relayed user message about other work is outside this seat's card: note it in leftOut and carry on; it is never a reason to stop or to ask a designQuestion.",
|
|
67
|
+
seats: ALL,
|
|
68
|
+
},
|
|
63
69
|
"stay-in-goal": {
|
|
64
70
|
text: "Do only what the goal asks. A defect found on the way goes in leftOut, not into the change.",
|
|
65
71
|
seats: ["tester", "coder", "reviewer"],
|
|
66
72
|
},
|
|
73
|
+
"no-cpu-burners": {
|
|
74
|
+
text: "A burner is any process started only to load the machine (yes > /dev/null, stress, a busy loop), bounded or not; concurrent pnpm test:unit runs are not burners. Never start a burner without the user's yes: a seat that wants one stops and returns a designQuestion naming the load it would start (the process, how many, the timeout, and why), and starts it only after an answer from the user says yes. Before hand-back, confirm with ps that no load process you started survives, orphans reparented to PID 1 included, and kill any that does.",
|
|
75
|
+
seats: ["tester", "coder", "reviewer"],
|
|
76
|
+
},
|
|
67
77
|
"lessons-verbatim": {
|
|
68
78
|
text: "Return the memory.mjs brief-lessons block for this card verbatim in lessons; never paraphrase it.",
|
|
69
79
|
seats: ["lead"],
|
|
@@ -73,11 +83,20 @@ const BRIEF_RULES = {
|
|
|
73
83
|
seats: ["lead"],
|
|
74
84
|
},
|
|
75
85
|
"mutation-per-case": {
|
|
76
|
-
text: "List one mutation per case the goal names, each with a unique id and the test that must fail.",
|
|
86
|
+
text: "List one mutation per case the goal names, each with a unique id of the form M<n> (M1, M2, ...) and the test that must fail.",
|
|
87
|
+
seats: ["lead"],
|
|
88
|
+
},
|
|
89
|
+
"mutation-ids-verbatim": {
|
|
90
|
+
text: "Report each listed mutation in mutationResults under its exact listed id from MUTATION IDS; never renumber, shorten, or rename an id (a split mutation adds one letter: M8a, M8b).",
|
|
91
|
+
seats: ["tester", "coder", "reviewer"],
|
|
92
|
+
when: "mutations",
|
|
93
|
+
},
|
|
94
|
+
"mutation-is-code-change": {
|
|
95
|
+
text: "A mutation is a code change to a file in the card worktree that a test catches; a check that is not a code mutation (a git diff, a file-absence check) goes in reviewerChecks, never in mutations.",
|
|
77
96
|
seats: ["lead"],
|
|
78
97
|
},
|
|
79
98
|
"red-claim": {
|
|
80
|
-
text: "Tests that are red by design are named in tests_red_by_design; without the claim the card stops.",
|
|
99
|
+
text: "Tests that are red by design are named in tests_red_by_design; without the claim the card stops. If the new tests pass at baseline (pins), return tests_red_by_design [] and report every listed mutation in mutationResults, run in a scratch copy; the card goes on only when each one failed.",
|
|
81
100
|
seats: ["tester"],
|
|
82
101
|
when: "full",
|
|
83
102
|
},
|
|
@@ -119,6 +138,22 @@ const BRIEF_RULES = {
|
|
|
119
138
|
text: "Run the gate commands only. Never run git add, git commit, or any other command that writes to git; the commit seat is the only seat that commits.",
|
|
120
139
|
seats: ["gate"],
|
|
121
140
|
},
|
|
141
|
+
"gate-timeout-is-load": {
|
|
142
|
+
text: "When verify.mjs prints `exit timeout` for a command, report that command with timedOut: true; a timeout is load, not a red or a block, so leave blocked unset.",
|
|
143
|
+
seats: ["gate"],
|
|
144
|
+
},
|
|
145
|
+
"commit-no-file-edits": {
|
|
146
|
+
text: "Never edit, create, delete, or format a file: commit exactly the tree the gate seat passed (GATED TREE). A change the commit seems to need goes in blocked, never into the tree.",
|
|
147
|
+
seats: ["commit"],
|
|
148
|
+
},
|
|
149
|
+
"commit-reports-tree": {
|
|
150
|
+
text: "After git commit returns, run node .doug/hooks/lib/verified-tree.mjs tree with the card worktree path as its argument and report its output as tree; a tree that differs from GATED TREE stops the card.",
|
|
151
|
+
seats: ["commit"],
|
|
152
|
+
},
|
|
153
|
+
"gate-reports-tree": {
|
|
154
|
+
text: "After the gate commands, run node .doug/hooks/lib/verified-tree.mjs tree with the card worktree path as its argument and report its output as tree.",
|
|
155
|
+
seats: ["gate"],
|
|
156
|
+
},
|
|
122
157
|
"leave-plan-anchor-out": {
|
|
123
158
|
text: "Leave .doug/plan.json and .doug/anchor.md out of the commit unless the card is about them.",
|
|
124
159
|
seats: ["commit"],
|
|
@@ -129,11 +164,13 @@ const BRIEF_RULES = {
|
|
|
129
164
|
|
|
130
165
|
const S = { type: "string" };
|
|
131
166
|
const NES = { type: "string", minLength: 1 };
|
|
167
|
+
const MUTATION_ID_PATTERN = "^M[1-9][0-9]*$";
|
|
168
|
+
const TREE = { type: "string", pattern: "^([0-9a-f]{40}|[0-9a-f]{64})$" };
|
|
132
169
|
const INT_OR_NULL = { type: ["integer", "null"] };
|
|
133
170
|
const STRS = { type: "array", items: S };
|
|
134
171
|
const CMD = {
|
|
135
172
|
type: "object",
|
|
136
|
-
properties: { command: S, exitCode: INT_OR_NULL },
|
|
173
|
+
properties: { command: S, exitCode: INT_OR_NULL, timedOut: { type: "boolean" } },
|
|
137
174
|
required: ["command", "exitCode"],
|
|
138
175
|
};
|
|
139
176
|
const MUT_RESULT = {
|
|
@@ -141,6 +178,11 @@ const MUT_RESULT = {
|
|
|
141
178
|
properties: { id: S, failed: { type: "boolean" }, test: S, assertion: S, own: { type: "boolean" } },
|
|
142
179
|
required: ["id", "failed", "test"],
|
|
143
180
|
};
|
|
181
|
+
const CODER_MUT_RESULT = {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: { id: S, failed: { type: "boolean" }, test: S, assertion: S, own: { type: "boolean" }, notRun: NES },
|
|
184
|
+
required: ["id"],
|
|
185
|
+
};
|
|
144
186
|
const ENVELOPE = {
|
|
145
187
|
seat: { type: "string", enum: SEATS },
|
|
146
188
|
hookBlocked: { type: "object", properties: { message: NES }, required: ["message"] },
|
|
@@ -172,11 +214,12 @@ const HAND_BACK_SCHEMAS = {
|
|
|
172
214
|
},
|
|
173
215
|
unverified: STRS,
|
|
174
216
|
testFiles: STRS,
|
|
217
|
+
reviewerChecks: STRS,
|
|
175
218
|
mutations: {
|
|
176
219
|
type: "array",
|
|
177
220
|
items: {
|
|
178
221
|
type: "object",
|
|
179
|
-
properties: { id:
|
|
222
|
+
properties: { id: { type: "string", minLength: 1, pattern: MUTATION_ID_PATTERN }, case: S, mutation: S, test: S },
|
|
180
223
|
required: ["id", "case", "mutation", "test"],
|
|
181
224
|
},
|
|
182
225
|
},
|
|
@@ -201,7 +244,7 @@ const HAND_BACK_SCHEMAS = {
|
|
|
201
244
|
type: "array",
|
|
202
245
|
items: { type: "object", properties: { file: S, reason: NES }, required: ["file", "reason"] },
|
|
203
246
|
},
|
|
204
|
-
mutationResults: { type: "array", items:
|
|
247
|
+
mutationResults: { type: "array", items: CODER_MUT_RESULT },
|
|
205
248
|
},
|
|
206
249
|
["filesChanged", "commandsRun", "testsEdited"],
|
|
207
250
|
),
|
|
@@ -230,12 +273,25 @@ const HAND_BACK_SCHEMAS = {
|
|
|
230
273
|
),
|
|
231
274
|
gate: schema(
|
|
232
275
|
"gate",
|
|
233
|
-
{ commands: { type: "array", items: CMD, minItems: 1 }, gateLine: NES, lockWaitMs: INT_OR_NULL },
|
|
234
|
-
["commands", "gateLine"],
|
|
276
|
+
{ commands: { type: "array", items: CMD, minItems: 1 }, gateLine: NES, lockWaitMs: INT_OR_NULL, tree: TREE },
|
|
277
|
+
["commands", "gateLine", "tree"],
|
|
235
278
|
),
|
|
236
|
-
commit: schema("commit", { sha: { type: "string", pattern: "^[0-9a-f]{7,40}$" }, branch: S }, ["sha", "branch"]),
|
|
279
|
+
commit: schema("commit", { sha: { type: "string", pattern: "^[0-9a-f]{7,40}$" }, branch: S, tree: TREE }, ["sha", "branch", "tree"]),
|
|
237
280
|
};
|
|
238
281
|
|
|
282
|
+
// The schema a seat is handed on a lane. Only the tests-only tester may report notRun (validateHandBack's tester
|
|
283
|
+
// case), so only there are its mutationResults items CODER_MUT_RESULT; the plain table is the full-lane contract.
|
|
284
|
+
function handBackSchema(seat, lane) {
|
|
285
|
+
if (seat === "tester" && lane === "tests-only") {
|
|
286
|
+
const base = HAND_BACK_SCHEMAS.tester;
|
|
287
|
+
return {
|
|
288
|
+
...base,
|
|
289
|
+
properties: { ...base.properties, mutationResults: { type: "array", items: CODER_MUT_RESULT } },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return HAND_BACK_SCHEMAS[seat];
|
|
293
|
+
}
|
|
294
|
+
|
|
239
295
|
// ---- small helpers ----
|
|
240
296
|
|
|
241
297
|
const cp = (x) => JSON.parse(JSON.stringify(x));
|
|
@@ -259,14 +315,25 @@ function checkCmds(list, path, errs, needOne) {
|
|
|
259
315
|
});
|
|
260
316
|
}
|
|
261
317
|
|
|
262
|
-
|
|
318
|
+
// A reported id covers listed id L when it is L or L plus one lowercase letter (a split such as M1a, M1b).
|
|
319
|
+
function coversId(reported, listed) {
|
|
320
|
+
if (reported === listed) return true;
|
|
321
|
+
return typeof reported === "string" && reported.length === listed.length + 1 && reported.startsWith(listed) && /[a-z]$/.test(reported);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function checkMutResults(list, path, errs, allowNotRun = false) {
|
|
263
325
|
if (!Array.isArray(list)) {
|
|
264
326
|
errs.push(path + " must be an array");
|
|
265
327
|
return false;
|
|
266
328
|
}
|
|
267
329
|
let good = true;
|
|
268
330
|
list.forEach((m, i) => {
|
|
269
|
-
if (
|
|
331
|
+
if (allowNotRun && isObj(m) && m.notRun !== undefined) {
|
|
332
|
+
if (!nonBlank(m.id) || !nonBlank(m.notRun)) {
|
|
333
|
+
errs.push(`${path}[${i}] notRun needs id and a non-empty reason string`);
|
|
334
|
+
good = false;
|
|
335
|
+
}
|
|
336
|
+
} else if (!isObj(m) || !nonBlank(m.id) || typeof m.failed !== "boolean" || !isStr(m.test)) {
|
|
270
337
|
errs.push(`${path}[${i}] needs id, failed (boolean) and test`);
|
|
271
338
|
good = false;
|
|
272
339
|
}
|
|
@@ -283,6 +350,21 @@ function laneFor(card) {
|
|
|
283
350
|
return "full";
|
|
284
351
|
}
|
|
285
352
|
|
|
353
|
+
// options.answers: the user's answers to design questions, { seat, question, answer, round?, run? } each.
|
|
354
|
+
function checkAnswers(answers) {
|
|
355
|
+
if (answers === undefined || answers === null) return [];
|
|
356
|
+
const bad = (why) => new TypeError("options.answers " + why);
|
|
357
|
+
if (!Array.isArray(answers)) throw bad("must be an array");
|
|
358
|
+
return answers.map((a, i) => {
|
|
359
|
+
if (!isObj(a)) throw bad(`[${i}] must be an object`);
|
|
360
|
+
if (!SEATS.includes(a.seat)) throw bad(`[${i}].seat must be one of the seats`);
|
|
361
|
+
if (!isStr(a.question)) throw bad(`[${i}].question must be a string`);
|
|
362
|
+
if (!nonBlank(a.answer)) throw bad(`[${i}].answer must be a non-blank string`);
|
|
363
|
+
if (a.round !== undefined && !(Number.isInteger(a.round) && a.round > 0)) throw bad(`[${i}].round must be a positive integer`);
|
|
364
|
+
return { ...a };
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
286
368
|
function initialState(card, options = {}) {
|
|
287
369
|
if (!isObj(card) || !nonBlank(card.id) || !isStr(card.goal)) {
|
|
288
370
|
throw new TypeError("card needs an id and a goal string");
|
|
@@ -290,6 +372,7 @@ function initialState(card, options = {}) {
|
|
|
290
372
|
const o = isObj(options) ? options : {};
|
|
291
373
|
return {
|
|
292
374
|
version: 1,
|
|
375
|
+
answers: checkAnswers(o.answers),
|
|
293
376
|
card: {
|
|
294
377
|
id: card.id,
|
|
295
378
|
title: card.title ?? null,
|
|
@@ -306,13 +389,15 @@ function initialState(card, options = {}) {
|
|
|
306
389
|
base: o.base ?? "main",
|
|
307
390
|
worktree: null,
|
|
308
391
|
researchNote: null,
|
|
309
|
-
material: { lessons: "", facts: [], unverified: [], testFiles: [], mutations: [] },
|
|
392
|
+
material: { lessons: "", facts: [], unverified: [], testFiles: [], mutations: [], reviewerChecks: [] },
|
|
393
|
+
notRun: [],
|
|
310
394
|
redTests: [],
|
|
311
395
|
counters: { coderRounds: 0, fixPasses: 0, redGates: 0 },
|
|
312
396
|
seatRuns: {},
|
|
313
397
|
fixFor: null,
|
|
314
398
|
lastFindings: [],
|
|
315
399
|
lastGate: null,
|
|
400
|
+
gateTree: null,
|
|
316
401
|
pending: "setup",
|
|
317
402
|
outcome: null,
|
|
318
403
|
history: [],
|
|
@@ -362,21 +447,27 @@ function buildBrief(s) {
|
|
|
362
447
|
unverified: cp(s.material.unverified),
|
|
363
448
|
testFiles: cp(s.material.testFiles),
|
|
364
449
|
mutations: cp(effectiveMutations(s)),
|
|
450
|
+
reviewerChecks: seat === "reviewer" ? cp(s.material.reviewerChecks || []) : [],
|
|
451
|
+
notRun: seat === "reviewer" ? cp(s.notRun || []) : [],
|
|
365
452
|
rules: Object.entries(BRIEF_RULES)
|
|
366
453
|
.filter(([, r]) => ruleApplies(r, seat, s))
|
|
367
454
|
.map(([id, r]) => ({ id, text: r.text })),
|
|
368
455
|
findings: fixing && s.fixFor === "review" ? cp(s.lastFindings) : [],
|
|
369
456
|
gate: fixing && s.fixFor === "gate" ? cp(s.lastGate) : null,
|
|
370
457
|
redTests: cp(s.redTests),
|
|
458
|
+
answers: (s.answers || [])
|
|
459
|
+
.filter((a) => a.seat === seat && (a.round === undefined || a.round <= (s.seatRuns[seat] || 0) + 1))
|
|
460
|
+
.map((a) => ({ question: a.question, answer: a.answer })),
|
|
371
461
|
writeTestsFirst: seat === "coder" && s.counters.coderRounds === 0 && (s.lane === "lean" || !s.testerAgent),
|
|
372
462
|
researchNote: s.researchNote,
|
|
373
|
-
handBack: cp(
|
|
463
|
+
handBack: cp(handBackSchema(seat, s.lane)),
|
|
374
464
|
};
|
|
375
465
|
if (seat === "setup") {
|
|
376
466
|
brief.install = s.install;
|
|
377
467
|
brief.base = s.base;
|
|
378
468
|
}
|
|
379
469
|
if (seat === "gate" || seat === "reviewer") brief.gateCommand = s.gateCommand;
|
|
470
|
+
if (seat === "commit") brief.gateTree = s.gateTree;
|
|
380
471
|
return brief;
|
|
381
472
|
}
|
|
382
473
|
|
|
@@ -402,6 +493,7 @@ function buildReport(s) {
|
|
|
402
493
|
testsEdited: cp(s.acc.testsEdited),
|
|
403
494
|
filesChanged: cp(s.acc.filesChanged),
|
|
404
495
|
followUps: cp(s.acc.followUps),
|
|
496
|
+
researchNote: s.researchNote ?? null,
|
|
405
497
|
cost: null,
|
|
406
498
|
};
|
|
407
499
|
}
|
|
@@ -446,8 +538,8 @@ function validate(state, hb) {
|
|
|
446
538
|
const muts = effectiveMutations(state);
|
|
447
539
|
const needCover = (list, path, ids) => {
|
|
448
540
|
if (!Array.isArray(list)) return;
|
|
449
|
-
const have =
|
|
450
|
-
for (const id of ids) if (!have.
|
|
541
|
+
const have = list.filter(isObj).map((m) => m.id);
|
|
542
|
+
for (const id of ids) if (!have.some((h) => coversId(h, id))) errs.push(`${path} is missing listed mutation ${id}`);
|
|
451
543
|
};
|
|
452
544
|
const first = state.counters.coderRounds === 0;
|
|
453
545
|
|
|
@@ -462,7 +554,7 @@ function validate(state, hb) {
|
|
|
462
554
|
break;
|
|
463
555
|
}
|
|
464
556
|
case "research":
|
|
465
|
-
if (!
|
|
557
|
+
if (!nonBlank(hb.note)) errs.push("note must be the researcher's cited note text or a path, not blank");
|
|
466
558
|
break;
|
|
467
559
|
case "lead": {
|
|
468
560
|
if (!isStr(hb.lessons)) errs.push("lessons must be a string");
|
|
@@ -474,6 +566,7 @@ function validate(state, hb) {
|
|
|
474
566
|
}
|
|
475
567
|
if (!strArr(hb.unverified)) errs.push("unverified must be an array of strings");
|
|
476
568
|
if (!strArr(hb.testFiles)) errs.push("testFiles must be an array of strings");
|
|
569
|
+
if (hb.reviewerChecks !== undefined && !strArr(hb.reviewerChecks)) errs.push("reviewerChecks must be an array of strings");
|
|
477
570
|
if (!Array.isArray(hb.mutations)) errs.push("mutations must be an array");
|
|
478
571
|
else {
|
|
479
572
|
const ids = new Set();
|
|
@@ -481,6 +574,7 @@ function validate(state, hb) {
|
|
|
481
574
|
if (!isObj(m) || !nonBlank(m.id) || !isStr(m.case) || !isStr(m.mutation) || !isStr(m.test)) {
|
|
482
575
|
errs.push(`mutations[${i}] needs id, case, mutation and test`);
|
|
483
576
|
} else if (ids.has(m.id)) errs.push(`mutations[${i}] repeats id ${m.id}`);
|
|
577
|
+
else if (!new RegExp(MUTATION_ID_PATTERN).test(m.id)) errs.push(`mutations[${i}] id ${m.id} must have the form M<n> (M1, M2, ...)`);
|
|
484
578
|
else ids.add(m.id);
|
|
485
579
|
});
|
|
486
580
|
}
|
|
@@ -494,7 +588,14 @@ function validate(state, hb) {
|
|
|
494
588
|
errs.push("tests_red_by_design must be an array of strings");
|
|
495
589
|
}
|
|
496
590
|
let shaped = true;
|
|
497
|
-
if (hb.mutationResults !== undefined) shaped = checkMutResults(hb.mutationResults, "mutationResults", errs);
|
|
591
|
+
if (hb.mutationResults !== undefined) shaped = checkMutResults(hb.mutationResults, "mutationResults", errs, state.lane === "tests-only");
|
|
592
|
+
if (state.lane === "tests-only" && shaped && Array.isArray(hb.mutationResults)) {
|
|
593
|
+
hb.mutationResults.forEach((m, i) => {
|
|
594
|
+
if (isObj(m) && m.notRun !== undefined && !muts.some((x) => x.id === m.id)) {
|
|
595
|
+
errs.push(`mutationResults[${i}] notRun id ${m.id} is not a listed mutation`);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
}
|
|
498
599
|
if (state.lane === "tests-only" && first && muts.length > 0) {
|
|
499
600
|
if (hb.mutationResults === undefined) errs.push("mutationResults is required on the first run");
|
|
500
601
|
else if (shaped) needCover(hb.mutationResults, "mutationResults", muts.map((m) => m.id));
|
|
@@ -511,7 +612,14 @@ function validate(state, hb) {
|
|
|
511
612
|
});
|
|
512
613
|
}
|
|
513
614
|
let shaped = true;
|
|
514
|
-
if (hb.mutationResults !== undefined) shaped = checkMutResults(hb.mutationResults, "mutationResults", errs);
|
|
615
|
+
if (hb.mutationResults !== undefined) shaped = checkMutResults(hb.mutationResults, "mutationResults", errs, true);
|
|
616
|
+
if (shaped && Array.isArray(hb.mutationResults)) {
|
|
617
|
+
hb.mutationResults.forEach((m, i) => {
|
|
618
|
+
if (isObj(m) && m.notRun !== undefined && !muts.some((x) => x.id === m.id)) {
|
|
619
|
+
errs.push(`mutationResults[${i}] notRun id ${m.id} is not a listed mutation`);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
515
623
|
if (first && muts.length > 0) {
|
|
516
624
|
if (hb.mutationResults === undefined) errs.push("mutationResults is required on the first round");
|
|
517
625
|
else if (shaped) needCover(hb.mutationResults, "mutationResults", muts.map((m) => m.id));
|
|
@@ -538,11 +646,16 @@ function validate(state, hb) {
|
|
|
538
646
|
}
|
|
539
647
|
if (hb.verdict === "pass" && blocker) errs.push("verdict pass contradicts an evidenced blocker");
|
|
540
648
|
checkCmds(hb.commandsRun, "commandsRun", errs, false);
|
|
649
|
+
if (Array.isArray(hb.mutationResults)) {
|
|
650
|
+
hb.mutationResults.forEach((m, i) => {
|
|
651
|
+
if (isObj(m) && m.notRun !== undefined) errs.push(`mutationResults[${i}] notRun is not allowed for the reviewer`);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
541
654
|
if (checkMutResults(hb.mutationResults, "mutationResults", errs)) {
|
|
542
655
|
const listed = muts.map((m) => m.id);
|
|
543
656
|
needCover(hb.mutationResults, "mutationResults", listed);
|
|
544
657
|
for (const m of hb.mutationResults) {
|
|
545
|
-
if (!listed.
|
|
658
|
+
if (!listed.some((l) => coversId(m.id, l)) && m.own !== true) errs.push(`mutationResults id ${m.id} is not listed and not marked own`);
|
|
546
659
|
}
|
|
547
660
|
}
|
|
548
661
|
break;
|
|
@@ -553,6 +666,7 @@ function validate(state, hb) {
|
|
|
553
666
|
errs.push("gate seat must not run git add or git commit");
|
|
554
667
|
}
|
|
555
668
|
if (!nonBlank(hb.gateLine)) errs.push("gateLine must be a non-empty string");
|
|
669
|
+
if (!(isStr(hb.tree) && TREE_RE.test(hb.tree))) errs.push("tree must be 40 or 64 lowercase hex characters");
|
|
556
670
|
if (hb.lockWaitMs !== undefined && hb.lockWaitMs !== null && !Number.isInteger(hb.lockWaitMs)) {
|
|
557
671
|
errs.push("lockWaitMs must be an integer or null");
|
|
558
672
|
}
|
|
@@ -561,6 +675,7 @@ function validate(state, hb) {
|
|
|
561
675
|
case "commit": {
|
|
562
676
|
if (!(isStr(hb.sha) && /^[0-9a-f]{7,40}$/.test(hb.sha))) errs.push("sha must be 7 to 40 lowercase hex characters");
|
|
563
677
|
if (!nonBlank(hb.branch)) errs.push("branch must be a non-empty string");
|
|
678
|
+
if (!(isStr(hb.tree) && TREE_RE.test(hb.tree))) errs.push("tree must be 40 or 64 lowercase hex characters");
|
|
564
679
|
break;
|
|
565
680
|
}
|
|
566
681
|
default:
|
|
@@ -571,6 +686,8 @@ function validate(state, hb) {
|
|
|
571
686
|
|
|
572
687
|
// ---- next ----
|
|
573
688
|
|
|
689
|
+
const TREE_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
690
|
+
|
|
574
691
|
// git add / git commit at the start of a command or after a shell separator, global options (-C <path>, -c k=v, --flag) allowed before the verb.
|
|
575
692
|
const GIT_WRITE_RE = /(?:^|[;&|(\n]\s*)git(?:\s+(?:-[Cc]\s+(?:"[^"]*"|'[^']*'|\S+)|--[\w-]+(?:=\S+)?))*\s+(?:add|commit)\b/;
|
|
576
693
|
|
|
@@ -620,6 +737,9 @@ function next(state, handBack) {
|
|
|
620
737
|
// C3 .. C6
|
|
621
738
|
if (isObj(H.hookBlocked) && nonBlank(H.hookBlocked.message)) return stopWith(s, "hook-blocked", H.hookBlocked.message);
|
|
622
739
|
if (nonBlank(H.designQuestion)) return stopWith(s, "design-decision", H.designQuestion);
|
|
740
|
+
if (P === "gate" && Array.isArray(H.commands) && H.commands.some((c) => isObj(c) && c.timedOut === true)) {
|
|
741
|
+
return stopWith({ ...s, lastGate: { commands: cp(H.commands), gateLine: H.gateLine } }, "gate-timeout", typeof H.gateLine === "string" ? H.gateLine : "a gate command timed out");
|
|
742
|
+
}
|
|
623
743
|
if (nonBlank(H.blocked)) return stopWith(s, "seat-blocked", H.blocked);
|
|
624
744
|
const errs = validateHandBack(s, H);
|
|
625
745
|
if (errs.length > 0) return stopWith(s, "hand-back-invalid", errs.join("; "));
|
|
@@ -640,6 +760,7 @@ function next(state, handBack) {
|
|
|
640
760
|
unverified: cp(H.unverified),
|
|
641
761
|
testFiles: cp(H.testFiles),
|
|
642
762
|
mutations: cp(H.mutations),
|
|
763
|
+
reviewerChecks: strArr(H.reviewerChecks) ? cp(H.reviewerChecks) : [],
|
|
643
764
|
};
|
|
644
765
|
const to = s.lane === "lean" || !s.testerAgent ? "coder" : "tester";
|
|
645
766
|
return advance(s, to, "ok", { material });
|
|
@@ -648,13 +769,41 @@ function next(state, handBack) {
|
|
|
648
769
|
const acc = { ...s.acc, filesChanged: union(s.acc.filesChanged, H.files) };
|
|
649
770
|
if (Array.isArray(H.mutationResults) && s.lane === "tests-only") acc.mutations = cp(H.mutationResults);
|
|
650
771
|
if (s.lane === "tests-only") {
|
|
772
|
+
const seenT = new Set((s.notRun || []).map((n) => n.id));
|
|
773
|
+
const notRunT = [...(s.notRun || [])];
|
|
774
|
+
for (const m of Array.isArray(H.mutationResults) ? H.mutationResults : []) {
|
|
775
|
+
if (isObj(m) && m.notRun !== undefined && !seenT.has(m.id)) {
|
|
776
|
+
seenT.add(m.id);
|
|
777
|
+
notRunT.push({ id: m.id, reason: m.notRun });
|
|
778
|
+
}
|
|
779
|
+
}
|
|
651
780
|
return advance(s, "reviewer", "ok", {
|
|
652
781
|
acc,
|
|
782
|
+
notRun: notRunT,
|
|
653
783
|
fixFor: null,
|
|
654
784
|
counters: { ...s.counters, coderRounds: s.counters.coderRounds + 1 },
|
|
655
785
|
});
|
|
656
786
|
}
|
|
657
787
|
const claim = H.tests_red_by_design;
|
|
788
|
+
if (Array.isArray(claim) && claim.length === 0) {
|
|
789
|
+
// Pin-only card: the new tests pass at baseline. Go on only when every listed mutation was run and failed.
|
|
790
|
+
const listed = effectiveMutations(s).map((m) => m.id);
|
|
791
|
+
const results = Array.isArray(H.mutationResults) ? H.mutationResults.filter(isObj) : [];
|
|
792
|
+
const bad = listed.filter((id) => {
|
|
793
|
+
const cov = results.filter((m) => coversId(m.id, id));
|
|
794
|
+
return cov.length === 0 || cov.some((m) => m.failed !== true);
|
|
795
|
+
});
|
|
796
|
+
if (listed.length > 0 && bad.length === 0) {
|
|
797
|
+
return advance(s, "coder", "ok", { acc, redTests: [] });
|
|
798
|
+
}
|
|
799
|
+
return stopWith(
|
|
800
|
+
s,
|
|
801
|
+
"tester-red-claim-missing",
|
|
802
|
+
listed.length === 0
|
|
803
|
+
? "the tester returned an empty tests_red_by_design claim and no mutation is listed to show the pins bite"
|
|
804
|
+
: `the tester returned an empty tests_red_by_design claim but mutation ${bad.join(", ")} was not run or survived`,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
658
807
|
if (!(Array.isArray(claim) && claim.length > 0 && claim.every(nonBlank))) {
|
|
659
808
|
return stopWith(s, "tester-red-claim-missing", "the tester returned no tests_red_by_design claim");
|
|
660
809
|
}
|
|
@@ -666,8 +815,17 @@ function next(state, handBack) {
|
|
|
666
815
|
filesChanged: union(s.acc.filesChanged, H.filesChanged),
|
|
667
816
|
testsEdited: [...s.acc.testsEdited, ...H.testsEdited.map((t) => ({ round, file: t.file, reason: t.reason }))],
|
|
668
817
|
};
|
|
818
|
+
const seen = new Set((s.notRun || []).map((n) => n.id));
|
|
819
|
+
const notRun = [...(s.notRun || [])];
|
|
820
|
+
for (const m of Array.isArray(H.mutationResults) ? H.mutationResults : []) {
|
|
821
|
+
if (isObj(m) && m.notRun !== undefined && !seen.has(m.id)) {
|
|
822
|
+
seen.add(m.id);
|
|
823
|
+
notRun.push({ id: m.id, reason: m.notRun });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
669
826
|
return advance(s, "reviewer", "ok", {
|
|
670
827
|
acc,
|
|
828
|
+
notRun,
|
|
671
829
|
fixFor: null,
|
|
672
830
|
counters: { ...s.counters, coderRounds: s.counters.coderRounds + 1 },
|
|
673
831
|
});
|
|
@@ -722,7 +880,7 @@ function next(state, handBack) {
|
|
|
722
880
|
const waited = { ...s.acc, lockWaitMs: (s.acc.lockWaitMs || 0) + (Number.isInteger(H.lockWaitMs) ? H.lockWaitMs : 0) };
|
|
723
881
|
s.acc = waited;
|
|
724
882
|
if (H.commands.every((c) => c.exitCode === 0)) {
|
|
725
|
-
return advance(s, "commit", "ok", { lastGate, acc: { ...waited, gateLine: H.gateLine } });
|
|
883
|
+
return advance(s, "commit", "ok", { lastGate, gateTree: H.tree, acc: { ...waited, gateLine: H.gateLine } });
|
|
726
884
|
}
|
|
727
885
|
const redGates = s.counters.redGates + 1;
|
|
728
886
|
const counters = { ...s.counters, redGates };
|
|
@@ -731,6 +889,9 @@ function next(state, handBack) {
|
|
|
731
889
|
}
|
|
732
890
|
default: {
|
|
733
891
|
// commit
|
|
892
|
+
if (H.tree !== s.gateTree) {
|
|
893
|
+
return stopWith(s, "commit-tree-mismatch", `commit ${H.sha} tree ${H.tree} differs from the gated tree ${s.gateTree}`);
|
|
894
|
+
}
|
|
734
895
|
const state = {
|
|
735
896
|
...s,
|
|
736
897
|
pending: null,
|
|
@@ -753,6 +914,9 @@ function briefPrompt(brief, { repoRoot, worktreePath } = {}) {
|
|
|
753
914
|
out.push(`SEAT: ${b.seat} (round ${b.round}, lane ${b.lane}, model row ${b.row})`);
|
|
754
915
|
out.push(`CARD ${b.card.id}${b.card.title ? ": " + b.card.title : ""}`);
|
|
755
916
|
out.push("GOAL:\n" + b.card.goal);
|
|
917
|
+
if (b.answers && b.answers.length > 0) {
|
|
918
|
+
out.push("ANSWERS FROM THE USER (to your design questions; build on them):\n" + bullets(b.answers.map((a) => `Q: ${a.question} A: ${a.answer}`)));
|
|
919
|
+
}
|
|
756
920
|
out.push("WORKTREE: " + worktreePath);
|
|
757
921
|
if (b.seat === "setup") {
|
|
758
922
|
out.push(
|
|
@@ -763,14 +927,23 @@ function briefPrompt(brief, { repoRoot, worktreePath } = {}) {
|
|
|
763
927
|
: "There is no install command; skip the install and report install as null."),
|
|
764
928
|
);
|
|
765
929
|
}
|
|
766
|
-
if (b.researchNote) out.push("RESEARCH NOTE: " + b.researchNote);
|
|
930
|
+
if (b.researchNote) out.push("RESEARCH NOTE:" + (b.researchNote.includes("\n") ? "\n" : " ") + b.researchNote);
|
|
767
931
|
if (b.lessons) out.push("LESSONS BLOCK verbatim:\n" + b.lessons);
|
|
768
932
|
if (b.facts.length > 0) out.push("FACTS (with sources):\n" + bullets(b.facts.map((f) => `${f.text} [${f.source}]`)));
|
|
769
933
|
if (b.unverified.length > 0) out.push("UNVERIFIED:\n" + bullets(b.unverified));
|
|
770
934
|
if (b.testFiles.length > 0) out.push("TEST FILES:\n" + bullets(b.testFiles));
|
|
771
935
|
if (b.mutations.length > 0) {
|
|
772
936
|
out.push("MUTATIONS:\n" + bullets(b.mutations.map((m) => `${m.id} (${m.case}): ${m.mutation}; test that must fail: ${m.test}`)));
|
|
937
|
+
if ((b.rules || []).some((r) => r.id === "mutation-ids-verbatim")) {
|
|
938
|
+
out.push("MUTATION IDS (report each verbatim in mutationResults): " + b.mutations.map((m) => m.id).join(", "));
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (b.notRun && b.notRun.length > 0) {
|
|
942
|
+
out.push(
|
|
943
|
+
"NOT RUN BY THE CODER (report each with failed true or false):\n" + bullets(b.notRun.map((n) => `${n.id}: ${n.reason}`)),
|
|
944
|
+
);
|
|
773
945
|
}
|
|
946
|
+
if (b.reviewerChecks && b.reviewerChecks.length > 0) out.push("REVIEWER CHECKS:\n" + bullets(b.reviewerChecks));
|
|
774
947
|
if (b.redTests.length > 0) out.push("TESTS RED BY DESIGN:\n" + bullets(b.redTests));
|
|
775
948
|
if (b.writeTestsFirst) out.push("Write the test first, in the file that covers the module, then the change.");
|
|
776
949
|
if (b.findings.length > 0) {
|
|
@@ -788,6 +961,7 @@ function briefPrompt(brief, { repoRoot, worktreePath } = {}) {
|
|
|
788
961
|
);
|
|
789
962
|
}
|
|
790
963
|
if (b.gateCommand) out.push("GATE COMMAND: " + b.gateCommand);
|
|
964
|
+
if (b.gateTree) out.push("GATED TREE: " + b.gateTree);
|
|
791
965
|
out.push("RULES:\n" + bullets(b.rules.map((r) => r.text)));
|
|
792
966
|
out.push("HAND-BACK: return JSON matching this schema:\n" + JSON.stringify(b.handBack, null, 2));
|
|
793
967
|
return out.join("\n\n") + "\n";
|
|
@@ -820,7 +994,7 @@ function seatOpts(seat, brief) {
|
|
|
820
994
|
const row = SEAT_ROWS[seat]
|
|
821
995
|
const src = (models.roles && models.roles[row]) || (models.tiers && models.tiers[row]) || {}
|
|
822
996
|
const opts = {
|
|
823
|
-
schema:
|
|
997
|
+
schema: brief.handBack,
|
|
824
998
|
label: seat + ':' + args.card.id + ':' + brief.round,
|
|
825
999
|
agentType: AGENT_TYPES[seat],
|
|
826
1000
|
}
|