@dougharness/cli 1.1.1 → 1.3.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/board-serve.js +40 -6
- package/dist/board-serve.js.map +1 -1
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/memory-setup.d.ts +7 -0
- package/dist/memory-setup.js +70 -46
- package/dist/memory-setup.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +6 -2
- package/node_modules/@dougharness/flow/agents/coder.md +23 -0
- package/node_modules/@dougharness/flow/agents/implementer.md +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +1 -1
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -1
- package/node_modules/@dougharness/flow/agents/reviewer.md +3 -1
- package/node_modules/@dougharness/flow/agents/spec-writer.md +23 -0
- package/node_modules/@dougharness/flow/agents/tester.md +21 -0
- package/node_modules/@dougharness/flow/hooks/hooks.json +4 -1
- package/node_modules/@dougharness/flow/hooks/register.tsx +94 -0
- package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +117 -0
- package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +17 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +91 -10
- package/node_modules/@dougharness/flow/lib/card-land.mjs +115 -0
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +778 -0
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +393 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +15 -5
- package/node_modules/@dougharness/flow/lib/decisions.mjs +4 -2
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +144 -3
- package/node_modules/@dougharness/flow/lib/land.mjs +27 -8
- package/node_modules/@dougharness/flow/lib/memory.mjs +406 -76
- package/node_modules/@dougharness/flow/lib/plan.mjs +33 -5
- package/node_modules/@dougharness/flow/lib/worktrees.mjs +3 -3
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +440 -0
- package/node_modules/@dougharness/flow/scripts/board.mjs +107 -32
- package/node_modules/@dougharness/flow/scripts/cost.mjs +16 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +33 -10
- package/node_modules/@dougharness/flow/scripts/plan.mjs +104 -30
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +8 -2
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +30 -10
- package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +80 -0
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +6 -6
- package/node_modules/@dougharness/flow/workflows/card-queen.js +846 -0
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +63 -23
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/anchor.mjs +29 -4
- package/node_modules/@dougharness/gates/lib/bash-rules.mjs +196 -6
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +40 -8
- package/node_modules/@dougharness/gates/lib/proposals.mjs +41 -5
- package/node_modules/@dougharness/gates/lib/scope.mjs +43 -4
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/reanchor.mjs +2 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +119 -15
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +156 -14
- package/node_modules/@dougharness/gates/scripts/verify.mjs +8 -4
- package/package.json +3 -3
- package/templates/board-app.js +46 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'card-queen',
|
|
3
|
+
description: 'The queen of one hand-track card: runs the card loop contract (lib/card-loop.mjs) seat by seat in the card worktree and returns its report.',
|
|
4
|
+
phases: [
|
|
5
|
+
{ title: 'Setup' },
|
|
6
|
+
{ title: 'Research' },
|
|
7
|
+
{ title: 'Lead' },
|
|
8
|
+
{ title: 'Tester' },
|
|
9
|
+
{ title: 'Coder' },
|
|
10
|
+
{ title: 'Reviewer' },
|
|
11
|
+
{ title: 'Gate' },
|
|
12
|
+
{ title: 'Commit' },
|
|
13
|
+
],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ---- BEGIN card-loop.mjs (inlined by pnpm workflows:sync; do not edit) ----
|
|
17
|
+
// The hand card loop as a host-neutral contract (card card-loop-contract, ADR 0015).
|
|
18
|
+
// Pure: no imports, no clock, no randomness, no process or global access. A host (a Claude Code Workflow,
|
|
19
|
+
// a Codex driver, a person) runs the seat the action names, then feeds the hand-back to next().
|
|
20
|
+
// Design: .doug/.state/reports/card-loop-contract/design.md. Seat hand-backs extend docs/worker-contract.md.
|
|
21
|
+
|
|
22
|
+
const SEATS = ["setup", "research", "lead", "tester", "coder", "reviewer", "gate", "commit"];
|
|
23
|
+
const LANES = ["full", "lean", "tests-only"];
|
|
24
|
+
const STOP_REASONS = [
|
|
25
|
+
"agent-null",
|
|
26
|
+
"hand-back-invalid",
|
|
27
|
+
"hook-blocked",
|
|
28
|
+
"design-decision",
|
|
29
|
+
"seat-blocked",
|
|
30
|
+
"setup-failed",
|
|
31
|
+
"tester-red-claim-missing",
|
|
32
|
+
"reviewer-blocker-open",
|
|
33
|
+
"reviewer-major-open",
|
|
34
|
+
"gate-red-twice",
|
|
35
|
+
];
|
|
36
|
+
// The Models-table row each seat runs on. setup, gate and commit have no sourced row; cheap is a choice.
|
|
37
|
+
const SEAT_ROWS = {
|
|
38
|
+
setup: "cheap",
|
|
39
|
+
research: "plan",
|
|
40
|
+
lead: "lead",
|
|
41
|
+
tester: "implement",
|
|
42
|
+
coder: "implement",
|
|
43
|
+
reviewer: "review",
|
|
44
|
+
gate: "cheap",
|
|
45
|
+
commit: "cheap",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ALL = SEATS;
|
|
49
|
+
// id -> { text, seats, when? }. when: "mutations" (the brief lists mutations), "full", "tests-only" (the lane).
|
|
50
|
+
const BRIEF_RULES = {
|
|
51
|
+
"absolute-paths": {
|
|
52
|
+
text: "Work in the card worktree named in this brief and use absolute paths or cd to it; never rely on CLAUDE_PROJECT_DIR.",
|
|
53
|
+
seats: ALL,
|
|
54
|
+
},
|
|
55
|
+
"git-alone": {
|
|
56
|
+
text: "Give each git command alone: one command per call, no chaining with && or ;.",
|
|
57
|
+
seats: ALL,
|
|
58
|
+
},
|
|
59
|
+
"hook-block-report": {
|
|
60
|
+
text: "If a hook blocks a step, do not work around it: report it in hookBlocked with the hook's message.",
|
|
61
|
+
seats: ALL,
|
|
62
|
+
},
|
|
63
|
+
"stay-in-goal": {
|
|
64
|
+
text: "Do only what the goal asks. A defect found on the way goes in leftOut, not into the change.",
|
|
65
|
+
seats: ["tester", "coder", "reviewer"],
|
|
66
|
+
},
|
|
67
|
+
"lessons-verbatim": {
|
|
68
|
+
text: "Return the memory.mjs brief-lessons block for this card verbatim in lessons; never paraphrase it.",
|
|
69
|
+
seats: ["lead"],
|
|
70
|
+
},
|
|
71
|
+
"facts-cited": {
|
|
72
|
+
text: "Every fact a seat relies on is quoted with its source (file and line or URL); an unverified fact is marked unverified.",
|
|
73
|
+
seats: ["lead"],
|
|
74
|
+
},
|
|
75
|
+
"mutation-per-case": {
|
|
76
|
+
text: "List one mutation per case the goal names, each with a unique id and the test that must fail.",
|
|
77
|
+
seats: ["lead"],
|
|
78
|
+
},
|
|
79
|
+
"red-claim": {
|
|
80
|
+
text: "Tests that are red by design are named in tests_red_by_design; without the claim the card stops.",
|
|
81
|
+
seats: ["tester"],
|
|
82
|
+
when: "full",
|
|
83
|
+
},
|
|
84
|
+
"tests-only-no-production-edit": {
|
|
85
|
+
text: "Tests-only card: never edit a production file in the live checkout; run the mutations in a scratch copy and report each result.",
|
|
86
|
+
seats: ["tester", "coder"],
|
|
87
|
+
when: "tests-only",
|
|
88
|
+
},
|
|
89
|
+
"no-test-edit-without-reason": {
|
|
90
|
+
text: "Do not edit a test except with a stated reason in testsEdited (file and reason).",
|
|
91
|
+
seats: ["coder"],
|
|
92
|
+
},
|
|
93
|
+
"mutations-in-scratch": {
|
|
94
|
+
text: "Apply each listed mutation in a scratch copy or worktree under .doug/.state, confirm the run path shows the copy, run the one test file, and remove the copy; report each result.",
|
|
95
|
+
seats: ["coder", "reviewer", "tester"],
|
|
96
|
+
when: "mutations",
|
|
97
|
+
},
|
|
98
|
+
"reviewer-reruns-mutations": {
|
|
99
|
+
text: "Rerun every listed mutation yourself and add your own (own: true); read the diff only after running them.",
|
|
100
|
+
seats: ["reviewer"],
|
|
101
|
+
},
|
|
102
|
+
"no-trailers": {
|
|
103
|
+
text: "Commit without Co-Authored-By or Claude-Session trailers.",
|
|
104
|
+
seats: ["commit"],
|
|
105
|
+
},
|
|
106
|
+
"commit-foreground": {
|
|
107
|
+
text: "Run git commit in the foreground with the Bash tool timeout set to 600000 ms: the pre-commit hook runs the full unit suite and takes over the 2-minute default.",
|
|
108
|
+
seats: ["commit"],
|
|
109
|
+
},
|
|
110
|
+
"commit-no-background": {
|
|
111
|
+
text: "Never use run_in_background, a Monitor, or a sleep loop for git commit: a background commit dies when your seat ends. Wait for the command to return.",
|
|
112
|
+
seats: ["commit"],
|
|
113
|
+
},
|
|
114
|
+
"commit-sha-from-git-log": {
|
|
115
|
+
text: "Report the sha only from git log -1 --format=%H after git commit has returned; never report a sha you did not read from it.",
|
|
116
|
+
seats: ["commit"],
|
|
117
|
+
},
|
|
118
|
+
"gate-no-git-writes": {
|
|
119
|
+
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
|
+
seats: ["gate"],
|
|
121
|
+
},
|
|
122
|
+
"leave-plan-anchor-out": {
|
|
123
|
+
text: "Leave .doug/plan.json and .doug/anchor.md out of the commit unless the card is about them.",
|
|
124
|
+
seats: ["commit"],
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// ---- hand-back schemas (JSON Schema literals; the host may pass one as a Workflow schema or --output-schema) ----
|
|
129
|
+
|
|
130
|
+
const S = { type: "string" };
|
|
131
|
+
const NES = { type: "string", minLength: 1 };
|
|
132
|
+
const INT_OR_NULL = { type: ["integer", "null"] };
|
|
133
|
+
const STRS = { type: "array", items: S };
|
|
134
|
+
const CMD = {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: { command: S, exitCode: INT_OR_NULL },
|
|
137
|
+
required: ["command", "exitCode"],
|
|
138
|
+
};
|
|
139
|
+
const MUT_RESULT = {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: { id: S, failed: { type: "boolean" }, test: S, assertion: S, own: { type: "boolean" } },
|
|
142
|
+
required: ["id", "failed", "test"],
|
|
143
|
+
};
|
|
144
|
+
const ENVELOPE = {
|
|
145
|
+
seat: { type: "string", enum: SEATS },
|
|
146
|
+
hookBlocked: { type: "object", properties: { message: NES }, required: ["message"] },
|
|
147
|
+
designQuestion: S,
|
|
148
|
+
blocked: S,
|
|
149
|
+
leftOut: STRS,
|
|
150
|
+
};
|
|
151
|
+
function schema(seat, props, required) {
|
|
152
|
+
return {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: { ...ENVELOPE, seat: { type: "string", enum: [seat] }, ...props },
|
|
155
|
+
required: ["seat", ...required],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const HAND_BACK_SCHEMAS = {
|
|
159
|
+
setup: schema(
|
|
160
|
+
"setup",
|
|
161
|
+
{ worktree: NES, branch: NES, install: { anyOf: [{ type: "null" }, CMD] } },
|
|
162
|
+
["worktree", "branch", "install"],
|
|
163
|
+
),
|
|
164
|
+
research: schema("research", { note: NES }, ["note"]),
|
|
165
|
+
lead: schema(
|
|
166
|
+
"lead",
|
|
167
|
+
{
|
|
168
|
+
lessons: S,
|
|
169
|
+
facts: {
|
|
170
|
+
type: "array",
|
|
171
|
+
items: { type: "object", properties: { text: NES, source: NES }, required: ["text", "source"] },
|
|
172
|
+
},
|
|
173
|
+
unverified: STRS,
|
|
174
|
+
testFiles: STRS,
|
|
175
|
+
mutations: {
|
|
176
|
+
type: "array",
|
|
177
|
+
items: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: { id: NES, case: S, mutation: S, test: S },
|
|
180
|
+
required: ["id", "case", "mutation", "test"],
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
["lessons", "facts", "unverified", "testFiles", "mutations"],
|
|
185
|
+
),
|
|
186
|
+
tester: schema(
|
|
187
|
+
"tester",
|
|
188
|
+
{
|
|
189
|
+
files: { type: "array", items: NES, minItems: 1 },
|
|
190
|
+
tests_red_by_design: STRS,
|
|
191
|
+
mutationResults: { type: "array", items: MUT_RESULT },
|
|
192
|
+
},
|
|
193
|
+
["files"],
|
|
194
|
+
),
|
|
195
|
+
coder: schema(
|
|
196
|
+
"coder",
|
|
197
|
+
{
|
|
198
|
+
filesChanged: STRS,
|
|
199
|
+
commandsRun: { type: "array", items: CMD },
|
|
200
|
+
testsEdited: {
|
|
201
|
+
type: "array",
|
|
202
|
+
items: { type: "object", properties: { file: S, reason: NES }, required: ["file", "reason"] },
|
|
203
|
+
},
|
|
204
|
+
mutationResults: { type: "array", items: MUT_RESULT },
|
|
205
|
+
},
|
|
206
|
+
["filesChanged", "commandsRun", "testsEdited"],
|
|
207
|
+
),
|
|
208
|
+
reviewer: schema(
|
|
209
|
+
"reviewer",
|
|
210
|
+
{
|
|
211
|
+
verdict: { type: "string", enum: ["pass", "fail"] },
|
|
212
|
+
findings: {
|
|
213
|
+
type: "array",
|
|
214
|
+
items: {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
id: S,
|
|
218
|
+
severity: { type: "string", enum: ["blocker", "major", "minor"] },
|
|
219
|
+
file: S,
|
|
220
|
+
description: S,
|
|
221
|
+
evidence: { type: ["string", "null"] },
|
|
222
|
+
},
|
|
223
|
+
required: ["id", "severity", "file", "description"],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
commandsRun: { type: "array", items: CMD },
|
|
227
|
+
mutationResults: { type: "array", items: MUT_RESULT },
|
|
228
|
+
},
|
|
229
|
+
["verdict", "findings", "commandsRun", "mutationResults"],
|
|
230
|
+
),
|
|
231
|
+
gate: schema(
|
|
232
|
+
"gate",
|
|
233
|
+
{ commands: { type: "array", items: CMD, minItems: 1 }, gateLine: NES, lockWaitMs: INT_OR_NULL },
|
|
234
|
+
["commands", "gateLine"],
|
|
235
|
+
),
|
|
236
|
+
commit: schema("commit", { sha: { type: "string", pattern: "^[0-9a-f]{7,40}$" }, branch: S }, ["sha", "branch"]),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// ---- small helpers ----
|
|
240
|
+
|
|
241
|
+
const cp = (x) => JSON.parse(JSON.stringify(x));
|
|
242
|
+
const isObj = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
243
|
+
const isStr = (x) => typeof x === "string";
|
|
244
|
+
const nonBlank = (x) => typeof x === "string" && x.trim() !== "";
|
|
245
|
+
const isAbs = (x) => nonBlank(x) && (x.startsWith("/") || /^[A-Za-z]:[\\/]/.test(x));
|
|
246
|
+
const exitOk = (x) => x === null || Number.isInteger(x);
|
|
247
|
+
const strArr = (x) => Array.isArray(x) && x.every(isStr);
|
|
248
|
+
|
|
249
|
+
function checkCmds(list, path, errs, needOne) {
|
|
250
|
+
if (!Array.isArray(list)) {
|
|
251
|
+
errs.push(path + " must be an array");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (needOne && list.length === 0) errs.push(path + " must not be empty");
|
|
255
|
+
list.forEach((c, i) => {
|
|
256
|
+
if (!isObj(c) || !isStr(c.command) || !exitOk(c.exitCode) || c.exitCode === undefined) {
|
|
257
|
+
errs.push(`${path}[${i}] needs a command string and an integer or null exitCode`);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function checkMutResults(list, path, errs) {
|
|
263
|
+
if (!Array.isArray(list)) {
|
|
264
|
+
errs.push(path + " must be an array");
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
let good = true;
|
|
268
|
+
list.forEach((m, i) => {
|
|
269
|
+
if (!isObj(m) || !nonBlank(m.id) || typeof m.failed !== "boolean" || !isStr(m.test)) {
|
|
270
|
+
errs.push(`${path}[${i}] needs id, failed (boolean) and test`);
|
|
271
|
+
good = false;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
return good;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---- lanes and state ----
|
|
278
|
+
|
|
279
|
+
function laneFor(card) {
|
|
280
|
+
if (!isObj(card)) return "full";
|
|
281
|
+
if (card.class === "tests-only") return "tests-only";
|
|
282
|
+
if (card.size === "S" && (card.class === "prose" || card.class === "docs")) return "lean";
|
|
283
|
+
return "full";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function initialState(card, options = {}) {
|
|
287
|
+
if (!isObj(card) || !nonBlank(card.id) || !isStr(card.goal)) {
|
|
288
|
+
throw new TypeError("card needs an id and a goal string");
|
|
289
|
+
}
|
|
290
|
+
const o = isObj(options) ? options : {};
|
|
291
|
+
return {
|
|
292
|
+
version: 1,
|
|
293
|
+
card: {
|
|
294
|
+
id: card.id,
|
|
295
|
+
title: card.title ?? null,
|
|
296
|
+
size: card.size ?? null,
|
|
297
|
+
class: card.class ?? null,
|
|
298
|
+
goal: card.goal,
|
|
299
|
+
},
|
|
300
|
+
lane: laneFor(card),
|
|
301
|
+
testerAgent: o.testerAgent !== false,
|
|
302
|
+
research: o.research === true,
|
|
303
|
+
install: o.install ?? null,
|
|
304
|
+
gateCommand: o.gateCommand ?? "node .doug/hooks/scripts/verify.mjs",
|
|
305
|
+
branch: o.branch ?? "card/" + card.id,
|
|
306
|
+
base: o.base ?? "main",
|
|
307
|
+
worktree: null,
|
|
308
|
+
researchNote: null,
|
|
309
|
+
material: { lessons: "", facts: [], unverified: [], testFiles: [], mutations: [] },
|
|
310
|
+
redTests: [],
|
|
311
|
+
counters: { coderRounds: 0, fixPasses: 0, redGates: 0 },
|
|
312
|
+
seatRuns: {},
|
|
313
|
+
fixFor: null,
|
|
314
|
+
lastFindings: [],
|
|
315
|
+
lastGate: null,
|
|
316
|
+
pending: "setup",
|
|
317
|
+
outcome: null,
|
|
318
|
+
history: [],
|
|
319
|
+
acc: {
|
|
320
|
+
findings: [],
|
|
321
|
+
downgraded: [],
|
|
322
|
+
mutations: [],
|
|
323
|
+
testsEdited: [],
|
|
324
|
+
filesChanged: [],
|
|
325
|
+
followUps: [],
|
|
326
|
+
gateLine: null,
|
|
327
|
+
lockWaitMs: 0,
|
|
328
|
+
commit: null,
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const buildSeat = (s) => (s.lane === "tests-only" && s.testerAgent ? "tester" : "coder");
|
|
334
|
+
const effectiveMutations = (s) => (s.lane === "lean" ? [] : s.material.mutations);
|
|
335
|
+
|
|
336
|
+
// ---- briefs and actions ----
|
|
337
|
+
|
|
338
|
+
function ruleApplies(rule, seat, s) {
|
|
339
|
+
if (!rule.seats.includes(seat)) return false;
|
|
340
|
+
if (rule.when === "full") return s.lane === "full";
|
|
341
|
+
if (rule.when === "tests-only") return s.lane === "tests-only";
|
|
342
|
+
if (rule.when === "mutations") {
|
|
343
|
+
if (effectiveMutations(s).length === 0) return false;
|
|
344
|
+
return seat !== "tester" || s.lane === "tests-only";
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildBrief(s) {
|
|
350
|
+
const seat = s.pending;
|
|
351
|
+
const fixing = seat === buildSeat(s) && s.fixFor !== null;
|
|
352
|
+
const brief = {
|
|
353
|
+
seat,
|
|
354
|
+
row: SEAT_ROWS[seat],
|
|
355
|
+
round: (s.seatRuns[seat] || 0) + 1,
|
|
356
|
+
lane: s.lane,
|
|
357
|
+
card: { id: s.card.id, title: s.card.title, goal: s.card.goal },
|
|
358
|
+
worktree: s.worktree,
|
|
359
|
+
branch: s.branch,
|
|
360
|
+
lessons: s.material.lessons,
|
|
361
|
+
facts: cp(s.material.facts),
|
|
362
|
+
unverified: cp(s.material.unverified),
|
|
363
|
+
testFiles: cp(s.material.testFiles),
|
|
364
|
+
mutations: cp(effectiveMutations(s)),
|
|
365
|
+
rules: Object.entries(BRIEF_RULES)
|
|
366
|
+
.filter(([, r]) => ruleApplies(r, seat, s))
|
|
367
|
+
.map(([id, r]) => ({ id, text: r.text })),
|
|
368
|
+
findings: fixing && s.fixFor === "review" ? cp(s.lastFindings) : [],
|
|
369
|
+
gate: fixing && s.fixFor === "gate" ? cp(s.lastGate) : null,
|
|
370
|
+
redTests: cp(s.redTests),
|
|
371
|
+
writeTestsFirst: seat === "coder" && s.counters.coderRounds === 0 && (s.lane === "lean" || !s.testerAgent),
|
|
372
|
+
researchNote: s.researchNote,
|
|
373
|
+
handBack: cp(HAND_BACK_SCHEMAS[seat]),
|
|
374
|
+
};
|
|
375
|
+
if (seat === "setup") {
|
|
376
|
+
brief.install = s.install;
|
|
377
|
+
brief.base = s.base;
|
|
378
|
+
}
|
|
379
|
+
if (seat === "gate" || seat === "reviewer") brief.gateCommand = s.gateCommand;
|
|
380
|
+
return brief;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function buildReport(s) {
|
|
384
|
+
const stopped = s.outcome && s.outcome.kind === "stopped";
|
|
385
|
+
return {
|
|
386
|
+
card: cp(s.card),
|
|
387
|
+
lane: s.lane,
|
|
388
|
+
outcome: stopped ? "stopped" : "done",
|
|
389
|
+
reason: stopped ? s.outcome.reason : null,
|
|
390
|
+
stoppedAt: stopped ? s.outcome.seat : null,
|
|
391
|
+
detail: stopped ? s.outcome.detail : null,
|
|
392
|
+
branch: s.branch,
|
|
393
|
+
worktree: s.worktree,
|
|
394
|
+
commit: s.acc.commit,
|
|
395
|
+
gateLine: s.acc.gateLine,
|
|
396
|
+
lockWaitMs: s.acc.lockWaitMs || 0,
|
|
397
|
+
rounds: s.counters.coderRounds,
|
|
398
|
+
counters: cp(s.counters),
|
|
399
|
+
review: { findings: cp(s.acc.findings), downgraded: cp(s.acc.downgraded) },
|
|
400
|
+
mutations: cp(s.acc.mutations),
|
|
401
|
+
testsRedByDesign: cp(s.redTests),
|
|
402
|
+
testsEdited: cp(s.acc.testsEdited),
|
|
403
|
+
filesChanged: cp(s.acc.filesChanged),
|
|
404
|
+
followUps: cp(s.acc.followUps),
|
|
405
|
+
cost: null,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function currentAction(state) {
|
|
410
|
+
if (state.outcome) {
|
|
411
|
+
const report = buildReport(state);
|
|
412
|
+
return state.outcome.kind === "stopped"
|
|
413
|
+
? { kind: "stopped", reason: state.outcome.reason, report }
|
|
414
|
+
: { kind: "done", report };
|
|
415
|
+
}
|
|
416
|
+
return { kind: "seat", seat: state.pending, brief: buildBrief(state) };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ---- validation ----
|
|
420
|
+
|
|
421
|
+
function validBlocker(f) {
|
|
422
|
+
return isObj(f) && f.severity === "blocker" && nonBlank(f.evidence);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function validateHandBack(state, handBack) {
|
|
426
|
+
try {
|
|
427
|
+
return validate(state, handBack);
|
|
428
|
+
} catch (e) {
|
|
429
|
+
return ["hand-back could not be checked: " + String(e && e.message)];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function validate(state, hb) {
|
|
434
|
+
const errs = [];
|
|
435
|
+
if (!isObj(hb)) return ["hand-back must be an object"];
|
|
436
|
+
const seat = state.pending;
|
|
437
|
+
if (!seat) return ["no seat is pending"];
|
|
438
|
+
if (hb.seat !== seat) errs.push(`seat must be "${seat}", got ${JSON.stringify(hb.seat) ?? "nothing"}`);
|
|
439
|
+
if (hb.hookBlocked !== undefined && !(isObj(hb.hookBlocked) && nonBlank(hb.hookBlocked.message))) {
|
|
440
|
+
errs.push("hookBlocked needs a non-empty message");
|
|
441
|
+
}
|
|
442
|
+
if (hb.designQuestion !== undefined && !isStr(hb.designQuestion)) errs.push("designQuestion must be a string");
|
|
443
|
+
if (hb.blocked !== undefined && !isStr(hb.blocked)) errs.push("blocked must be a string");
|
|
444
|
+
if (hb.leftOut !== undefined && !strArr(hb.leftOut)) errs.push("leftOut must be an array of strings");
|
|
445
|
+
|
|
446
|
+
const muts = effectiveMutations(state);
|
|
447
|
+
const needCover = (list, path, ids) => {
|
|
448
|
+
if (!Array.isArray(list)) return;
|
|
449
|
+
const have = new Set(list.filter(isObj).map((m) => m.id));
|
|
450
|
+
for (const id of ids) if (!have.has(id)) errs.push(`${path} is missing listed mutation ${id}`);
|
|
451
|
+
};
|
|
452
|
+
const first = state.counters.coderRounds === 0;
|
|
453
|
+
|
|
454
|
+
switch (seat) {
|
|
455
|
+
case "setup": {
|
|
456
|
+
if (!isAbs(hb.worktree)) errs.push("worktree must be an absolute path");
|
|
457
|
+
if (!nonBlank(hb.branch)) errs.push("branch must be a non-empty string");
|
|
458
|
+
if (!("install" in hb)) errs.push("install is required (null when there is no install command)");
|
|
459
|
+
else if (hb.install === null) {
|
|
460
|
+
if (state.install !== null) errs.push("install must report the install command that ran");
|
|
461
|
+
} else checkCmds([hb.install], "install", errs, false);
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
case "research":
|
|
465
|
+
if (!isAbs(hb.note)) errs.push("note must be an absolute path");
|
|
466
|
+
break;
|
|
467
|
+
case "lead": {
|
|
468
|
+
if (!isStr(hb.lessons)) errs.push("lessons must be a string");
|
|
469
|
+
if (!Array.isArray(hb.facts)) errs.push("facts must be an array");
|
|
470
|
+
else {
|
|
471
|
+
hb.facts.forEach((f, i) => {
|
|
472
|
+
if (!isObj(f) || !nonBlank(f.text) || !nonBlank(f.source)) errs.push(`facts[${i}] needs text and a non-empty source`);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (!strArr(hb.unverified)) errs.push("unverified must be an array of strings");
|
|
476
|
+
if (!strArr(hb.testFiles)) errs.push("testFiles must be an array of strings");
|
|
477
|
+
if (!Array.isArray(hb.mutations)) errs.push("mutations must be an array");
|
|
478
|
+
else {
|
|
479
|
+
const ids = new Set();
|
|
480
|
+
hb.mutations.forEach((m, i) => {
|
|
481
|
+
if (!isObj(m) || !nonBlank(m.id) || !isStr(m.case) || !isStr(m.mutation) || !isStr(m.test)) {
|
|
482
|
+
errs.push(`mutations[${i}] needs id, case, mutation and test`);
|
|
483
|
+
} else if (ids.has(m.id)) errs.push(`mutations[${i}] repeats id ${m.id}`);
|
|
484
|
+
else ids.add(m.id);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case "tester": {
|
|
490
|
+
if (!Array.isArray(hb.files) || hb.files.length === 0 || !hb.files.every(nonBlank)) {
|
|
491
|
+
errs.push("files must be a non-empty array of paths");
|
|
492
|
+
}
|
|
493
|
+
if (hb.tests_red_by_design !== undefined && !strArr(hb.tests_red_by_design)) {
|
|
494
|
+
errs.push("tests_red_by_design must be an array of strings");
|
|
495
|
+
}
|
|
496
|
+
let shaped = true;
|
|
497
|
+
if (hb.mutationResults !== undefined) shaped = checkMutResults(hb.mutationResults, "mutationResults", errs);
|
|
498
|
+
if (state.lane === "tests-only" && first && muts.length > 0) {
|
|
499
|
+
if (hb.mutationResults === undefined) errs.push("mutationResults is required on the first run");
|
|
500
|
+
else if (shaped) needCover(hb.mutationResults, "mutationResults", muts.map((m) => m.id));
|
|
501
|
+
}
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case "coder": {
|
|
505
|
+
if (!strArr(hb.filesChanged)) errs.push("filesChanged must be an array of strings");
|
|
506
|
+
checkCmds(hb.commandsRun, "commandsRun", errs, false);
|
|
507
|
+
if (!Array.isArray(hb.testsEdited)) errs.push("testsEdited must be an array");
|
|
508
|
+
else {
|
|
509
|
+
hb.testsEdited.forEach((t, i) => {
|
|
510
|
+
if (!isObj(t) || !isStr(t.file) || !nonBlank(t.reason)) errs.push(`testsEdited[${i}] needs a file and a stated reason`);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
let shaped = true;
|
|
514
|
+
if (hb.mutationResults !== undefined) shaped = checkMutResults(hb.mutationResults, "mutationResults", errs);
|
|
515
|
+
if (first && muts.length > 0) {
|
|
516
|
+
if (hb.mutationResults === undefined) errs.push("mutationResults is required on the first round");
|
|
517
|
+
else if (shaped) needCover(hb.mutationResults, "mutationResults", muts.map((m) => m.id));
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
case "reviewer": {
|
|
522
|
+
if (hb.verdict !== "pass" && hb.verdict !== "fail") errs.push('verdict must be "pass" or "fail"');
|
|
523
|
+
let blocker = false;
|
|
524
|
+
if (!Array.isArray(hb.findings)) errs.push("findings must be an array");
|
|
525
|
+
else {
|
|
526
|
+
hb.findings.forEach((f, i) => {
|
|
527
|
+
if (
|
|
528
|
+
!isObj(f) ||
|
|
529
|
+
!isStr(f.id) ||
|
|
530
|
+
!["blocker", "major", "minor"].includes(f.severity) ||
|
|
531
|
+
!isStr(f.file) ||
|
|
532
|
+
!nonBlank(f.description) ||
|
|
533
|
+
(f.evidence !== undefined && f.evidence !== null && !isStr(f.evidence))
|
|
534
|
+
) {
|
|
535
|
+
errs.push(`findings[${i}] needs id, severity (blocker|major|minor), file and description`);
|
|
536
|
+
} else if (validBlocker(f)) blocker = true;
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
if (hb.verdict === "pass" && blocker) errs.push("verdict pass contradicts an evidenced blocker");
|
|
540
|
+
checkCmds(hb.commandsRun, "commandsRun", errs, false);
|
|
541
|
+
if (checkMutResults(hb.mutationResults, "mutationResults", errs)) {
|
|
542
|
+
const listed = muts.map((m) => m.id);
|
|
543
|
+
needCover(hb.mutationResults, "mutationResults", listed);
|
|
544
|
+
for (const m of hb.mutationResults) {
|
|
545
|
+
if (!listed.includes(m.id) && m.own !== true) errs.push(`mutationResults id ${m.id} is not listed and not marked own`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case "gate": {
|
|
551
|
+
checkCmds(hb.commands, "commands", errs, true);
|
|
552
|
+
if (Array.isArray(hb.commands) && hb.commands.some((c) => c && isStr(c.command) && GIT_WRITE_RE.test(c.command))) {
|
|
553
|
+
errs.push("gate seat must not run git add or git commit");
|
|
554
|
+
}
|
|
555
|
+
if (!nonBlank(hb.gateLine)) errs.push("gateLine must be a non-empty string");
|
|
556
|
+
if (hb.lockWaitMs !== undefined && hb.lockWaitMs !== null && !Number.isInteger(hb.lockWaitMs)) {
|
|
557
|
+
errs.push("lockWaitMs must be an integer or null");
|
|
558
|
+
}
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case "commit": {
|
|
562
|
+
if (!(isStr(hb.sha) && /^[0-9a-f]{7,40}$/.test(hb.sha))) errs.push("sha must be 7 to 40 lowercase hex characters");
|
|
563
|
+
if (!nonBlank(hb.branch)) errs.push("branch must be a non-empty string");
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
default:
|
|
567
|
+
errs.push("unknown seat " + seat);
|
|
568
|
+
}
|
|
569
|
+
return errs;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ---- next ----
|
|
573
|
+
|
|
574
|
+
// 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
|
+
const GIT_WRITE_RE = /(?:^|[;&|(\n]\s*)git(?:\s+(?:-[Cc]\s+(?:"[^"]*"|'[^']*'|\S+)|--[\w-]+(?:=\S+)?))*\s+(?:add|commit)\b/;
|
|
576
|
+
|
|
577
|
+
function stopWith(s, reason, detail) {
|
|
578
|
+
const seat = s.pending;
|
|
579
|
+
const state = {
|
|
580
|
+
...s,
|
|
581
|
+
pending: null,
|
|
582
|
+
outcome: { kind: "stopped", reason, seat, detail },
|
|
583
|
+
history: [...s.history, { seat, round: s.seatRuns[seat] || 0, result: "stopped", note: reason }],
|
|
584
|
+
};
|
|
585
|
+
return { state, action: currentAction(state) };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function advance(s, pending, result, extra = {}) {
|
|
589
|
+
const seat = s.pending;
|
|
590
|
+
const state = {
|
|
591
|
+
...s,
|
|
592
|
+
...extra,
|
|
593
|
+
pending,
|
|
594
|
+
history: [...s.history, { seat, round: s.seatRuns[seat], result }],
|
|
595
|
+
};
|
|
596
|
+
return { state, action: currentAction(state) };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function union(list, add) {
|
|
600
|
+
const out = [...list];
|
|
601
|
+
for (const x of add) if (!out.includes(x)) out.push(x);
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function next(state, handBack) {
|
|
606
|
+
const s = cp(state);
|
|
607
|
+
if (s.outcome || !s.pending) return { state: s, action: currentAction(s) };
|
|
608
|
+
const P = s.pending;
|
|
609
|
+
const H = handBack;
|
|
610
|
+
|
|
611
|
+
// C1 .. C2
|
|
612
|
+
if (H === null || H === undefined) return stopWith(s, "agent-null", `the ${P} seat's agent returned no hand-back`);
|
|
613
|
+
if (!isObj(H) || H.seat !== P) {
|
|
614
|
+
return stopWith(s, "hand-back-invalid", validateHandBack(s, H).join("; "));
|
|
615
|
+
}
|
|
616
|
+
s.seatRuns = { ...s.seatRuns, [P]: (s.seatRuns[P] || 0) + 1 };
|
|
617
|
+
const round = s.seatRuns[P];
|
|
618
|
+
if (strArr(H.leftOut)) s.acc = { ...s.acc, followUps: union(s.acc.followUps, H.leftOut) };
|
|
619
|
+
|
|
620
|
+
// C3 .. C6
|
|
621
|
+
if (isObj(H.hookBlocked) && nonBlank(H.hookBlocked.message)) return stopWith(s, "hook-blocked", H.hookBlocked.message);
|
|
622
|
+
if (nonBlank(H.designQuestion)) return stopWith(s, "design-decision", H.designQuestion);
|
|
623
|
+
if (nonBlank(H.blocked)) return stopWith(s, "seat-blocked", H.blocked);
|
|
624
|
+
const errs = validateHandBack(s, H);
|
|
625
|
+
if (errs.length > 0) return stopWith(s, "hand-back-invalid", errs.join("; "));
|
|
626
|
+
|
|
627
|
+
switch (P) {
|
|
628
|
+
case "setup": {
|
|
629
|
+
if (H.install !== null && H.install.exitCode !== 0) {
|
|
630
|
+
return stopWith(s, "setup-failed", `install exited ${H.install.exitCode === null ? "without a code" : H.install.exitCode}`);
|
|
631
|
+
}
|
|
632
|
+
return advance(s, s.research ? "research" : "lead", "ok", { worktree: H.worktree, branch: H.branch });
|
|
633
|
+
}
|
|
634
|
+
case "research":
|
|
635
|
+
return advance(s, "lead", "ok", { researchNote: H.note });
|
|
636
|
+
case "lead": {
|
|
637
|
+
const material = {
|
|
638
|
+
lessons: H.lessons,
|
|
639
|
+
facts: cp(H.facts),
|
|
640
|
+
unverified: cp(H.unverified),
|
|
641
|
+
testFiles: cp(H.testFiles),
|
|
642
|
+
mutations: cp(H.mutations),
|
|
643
|
+
};
|
|
644
|
+
const to = s.lane === "lean" || !s.testerAgent ? "coder" : "tester";
|
|
645
|
+
return advance(s, to, "ok", { material });
|
|
646
|
+
}
|
|
647
|
+
case "tester": {
|
|
648
|
+
const acc = { ...s.acc, filesChanged: union(s.acc.filesChanged, H.files) };
|
|
649
|
+
if (Array.isArray(H.mutationResults) && s.lane === "tests-only") acc.mutations = cp(H.mutationResults);
|
|
650
|
+
if (s.lane === "tests-only") {
|
|
651
|
+
return advance(s, "reviewer", "ok", {
|
|
652
|
+
acc,
|
|
653
|
+
fixFor: null,
|
|
654
|
+
counters: { ...s.counters, coderRounds: s.counters.coderRounds + 1 },
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
const claim = H.tests_red_by_design;
|
|
658
|
+
if (!(Array.isArray(claim) && claim.length > 0 && claim.every(nonBlank))) {
|
|
659
|
+
return stopWith(s, "tester-red-claim-missing", "the tester returned no tests_red_by_design claim");
|
|
660
|
+
}
|
|
661
|
+
return advance(s, "coder", "ok", { acc, redTests: cp(claim) });
|
|
662
|
+
}
|
|
663
|
+
case "coder": {
|
|
664
|
+
const acc = {
|
|
665
|
+
...s.acc,
|
|
666
|
+
filesChanged: union(s.acc.filesChanged, H.filesChanged),
|
|
667
|
+
testsEdited: [...s.acc.testsEdited, ...H.testsEdited.map((t) => ({ round, file: t.file, reason: t.reason }))],
|
|
668
|
+
};
|
|
669
|
+
return advance(s, "reviewer", "ok", {
|
|
670
|
+
acc,
|
|
671
|
+
fixFor: null,
|
|
672
|
+
counters: { ...s.counters, coderRounds: s.counters.coderRounds + 1 },
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
case "reviewer": {
|
|
676
|
+
const downgraded = [];
|
|
677
|
+
const findings = H.findings.map((f) => {
|
|
678
|
+
const weak = f.severity === "blocker" && !nonBlank(f.evidence);
|
|
679
|
+
if (weak) downgraded.push({ round, id: f.id, reason: "blocker without evidence, treated as major" });
|
|
680
|
+
return {
|
|
681
|
+
round,
|
|
682
|
+
id: f.id,
|
|
683
|
+
severity: weak ? "major" : f.severity,
|
|
684
|
+
file: f.file,
|
|
685
|
+
description: f.description,
|
|
686
|
+
evidence: f.evidence ?? null,
|
|
687
|
+
};
|
|
688
|
+
});
|
|
689
|
+
const acc = {
|
|
690
|
+
...s.acc,
|
|
691
|
+
findings: [...s.acc.findings, ...findings],
|
|
692
|
+
downgraded: [...s.acc.downgraded, ...downgraded],
|
|
693
|
+
mutations: cp(H.mutationResults),
|
|
694
|
+
};
|
|
695
|
+
const blockers = findings.filter((f) => f.severity === "blocker");
|
|
696
|
+
// An evidenced major gets the same one fix pass; a major without evidence and a minor stay advisory.
|
|
697
|
+
const majors = findings.filter((f) => f.severity === "major" && nonBlank(f.evidence));
|
|
698
|
+
if (blockers.length === 0 && majors.length === 0) return advance(s, "gate", "ok", { acc });
|
|
699
|
+
if (s.counters.fixPasses >= 1) {
|
|
700
|
+
if (blockers.length > 0) {
|
|
701
|
+
return stopWith(
|
|
702
|
+
{ ...s, acc },
|
|
703
|
+
"reviewer-blocker-open",
|
|
704
|
+
"blocker still open after one fix pass: " + blockers.map((f) => f.id).join(", "),
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
return stopWith(
|
|
708
|
+
{ ...s, acc },
|
|
709
|
+
"reviewer-major-open",
|
|
710
|
+
"major with evidence still open after one fix pass: " + majors.map((f) => f.id).join(", "),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
return advance(s, buildSeat(s), "blockers", {
|
|
714
|
+
acc,
|
|
715
|
+
fixFor: "review",
|
|
716
|
+
counters: { ...s.counters, fixPasses: 1 },
|
|
717
|
+
lastFindings: findings.filter((f) => f.severity !== "minor"),
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
case "gate": {
|
|
721
|
+
const lastGate = { commands: cp(H.commands), gateLine: H.gateLine };
|
|
722
|
+
const waited = { ...s.acc, lockWaitMs: (s.acc.lockWaitMs || 0) + (Number.isInteger(H.lockWaitMs) ? H.lockWaitMs : 0) };
|
|
723
|
+
s.acc = waited;
|
|
724
|
+
if (H.commands.every((c) => c.exitCode === 0)) {
|
|
725
|
+
return advance(s, "commit", "ok", { lastGate, acc: { ...waited, gateLine: H.gateLine } });
|
|
726
|
+
}
|
|
727
|
+
const redGates = s.counters.redGates + 1;
|
|
728
|
+
const counters = { ...s.counters, redGates };
|
|
729
|
+
if (redGates >= 2) return stopWith({ ...s, lastGate, counters }, "gate-red-twice", H.gateLine);
|
|
730
|
+
return advance(s, buildSeat(s), "red", { lastGate, counters, fixFor: "gate" });
|
|
731
|
+
}
|
|
732
|
+
default: {
|
|
733
|
+
// commit
|
|
734
|
+
const state = {
|
|
735
|
+
...s,
|
|
736
|
+
pending: null,
|
|
737
|
+
outcome: { kind: "done" },
|
|
738
|
+
acc: { ...s.acc, commit: H.sha },
|
|
739
|
+
history: [...s.history, { seat: P, round, result: "ok" }],
|
|
740
|
+
};
|
|
741
|
+
return { state, action: currentAction(state) };
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ---- briefPrompt: a pure text renderer of a brief for every host ----
|
|
747
|
+
|
|
748
|
+
const bullets = (items) => items.map((x) => "- " + x).join("\n");
|
|
749
|
+
|
|
750
|
+
function briefPrompt(brief, { repoRoot, worktreePath } = {}) {
|
|
751
|
+
const b = brief;
|
|
752
|
+
const out = [];
|
|
753
|
+
out.push(`SEAT: ${b.seat} (round ${b.round}, lane ${b.lane}, model row ${b.row})`);
|
|
754
|
+
out.push(`CARD ${b.card.id}${b.card.title ? ": " + b.card.title : ""}`);
|
|
755
|
+
out.push("GOAL:\n" + b.card.goal);
|
|
756
|
+
out.push("WORKTREE: " + worktreePath);
|
|
757
|
+
if (b.seat === "setup") {
|
|
758
|
+
out.push(
|
|
759
|
+
"SETUP:\n" +
|
|
760
|
+
`Create the card worktree: git -C ${repoRoot} worktree add -b ${b.branch} ${worktreePath} ${b.base}\n` +
|
|
761
|
+
(b.install
|
|
762
|
+
? `Then run the install command in ${worktreePath}: ${b.install}`
|
|
763
|
+
: "There is no install command; skip the install and report install as null."),
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (b.researchNote) out.push("RESEARCH NOTE: " + b.researchNote);
|
|
767
|
+
if (b.lessons) out.push("LESSONS BLOCK verbatim:\n" + b.lessons);
|
|
768
|
+
if (b.facts.length > 0) out.push("FACTS (with sources):\n" + bullets(b.facts.map((f) => `${f.text} [${f.source}]`)));
|
|
769
|
+
if (b.unverified.length > 0) out.push("UNVERIFIED:\n" + bullets(b.unverified));
|
|
770
|
+
if (b.testFiles.length > 0) out.push("TEST FILES:\n" + bullets(b.testFiles));
|
|
771
|
+
if (b.mutations.length > 0) {
|
|
772
|
+
out.push("MUTATIONS:\n" + bullets(b.mutations.map((m) => `${m.id} (${m.case}): ${m.mutation}; test that must fail: ${m.test}`)));
|
|
773
|
+
}
|
|
774
|
+
if (b.redTests.length > 0) out.push("TESTS RED BY DESIGN:\n" + bullets(b.redTests));
|
|
775
|
+
if (b.writeTestsFirst) out.push("Write the test first, in the file that covers the module, then the change.");
|
|
776
|
+
if (b.findings.length > 0) {
|
|
777
|
+
out.push(
|
|
778
|
+
"FINDINGS TO FIX:\n" +
|
|
779
|
+
bullets(b.findings.map((f) => `${f.id} [${f.severity}] ${f.file}: ${f.description}${f.evidence ? " (evidence: " + f.evidence + ")" : ""}`)),
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (b.gate) {
|
|
783
|
+
out.push(
|
|
784
|
+
"RED GATE TO FIX:\n" +
|
|
785
|
+
bullets(b.gate.commands.map((c) => `${c.command} exited ${c.exitCode}`)) +
|
|
786
|
+
"\n" +
|
|
787
|
+
b.gate.gateLine,
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (b.gateCommand) out.push("GATE COMMAND: " + b.gateCommand);
|
|
791
|
+
out.push("RULES:\n" + bullets(b.rules.map((r) => r.text)));
|
|
792
|
+
out.push("HAND-BACK: return JSON matching this schema:\n" + JSON.stringify(b.handBack, null, 2));
|
|
793
|
+
return out.join("\n\n") + "\n";
|
|
794
|
+
}
|
|
795
|
+
// ---- END card-loop.mjs ----
|
|
796
|
+
|
|
797
|
+
// A thin adapter: the contract above decides everything; this loop only runs the seat it names and feeds back the hand-back.
|
|
798
|
+
const AGENT_TYPES = {
|
|
799
|
+
setup: 'doug-flow:implementer',
|
|
800
|
+
research: 'doug-flow:researcher',
|
|
801
|
+
lead: 'doug-flow:lead',
|
|
802
|
+
tester: 'doug-flow:tester',
|
|
803
|
+
coder: 'doug-flow:coder',
|
|
804
|
+
reviewer: 'doug-flow:reviewer',
|
|
805
|
+
gate: 'doug-flow:implementer',
|
|
806
|
+
commit: 'doug-flow:implementer',
|
|
807
|
+
}
|
|
808
|
+
const PHASE_TITLES = {
|
|
809
|
+
setup: 'Setup',
|
|
810
|
+
research: 'Research',
|
|
811
|
+
lead: 'Lead',
|
|
812
|
+
tester: 'Tester',
|
|
813
|
+
coder: 'Coder',
|
|
814
|
+
reviewer: 'Reviewer',
|
|
815
|
+
gate: 'Gate',
|
|
816
|
+
commit: 'Commit',
|
|
817
|
+
}
|
|
818
|
+
const models = args.models || {}
|
|
819
|
+
function seatOpts(seat, brief) {
|
|
820
|
+
const row = SEAT_ROWS[seat]
|
|
821
|
+
const src = (models.roles && models.roles[row]) || (models.tiers && models.tiers[row]) || {}
|
|
822
|
+
const opts = {
|
|
823
|
+
schema: HAND_BACK_SCHEMAS[seat],
|
|
824
|
+
label: seat + ':' + args.card.id + ':' + brief.round,
|
|
825
|
+
agentType: AGENT_TYPES[seat],
|
|
826
|
+
}
|
|
827
|
+
if (src.model && src.model !== 'inherit') opts.model = src.model
|
|
828
|
+
if (src.effort && src.effort !== 'inherit') opts.effort = src.effort
|
|
829
|
+
return opts
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
let s = initialState(args.card, args.options)
|
|
833
|
+
let a = currentAction(s)
|
|
834
|
+
while (a.kind === 'seat') {
|
|
835
|
+
phase(PHASE_TITLES[a.seat])
|
|
836
|
+
let hb
|
|
837
|
+
try {
|
|
838
|
+
hb = await agent(briefPrompt(a.brief, { repoRoot: args.repoRoot, worktreePath: args.worktreePath }), seatOpts(a.seat, a.brief))
|
|
839
|
+
} catch (err) {
|
|
840
|
+
// The runtime throws after repeated schema-validation failures; a null hand-back lets the contract stop with agent-null.
|
|
841
|
+
log('card-queen: ' + a.seat + ' seat agent() threw: ' + (err && err.message ? err.message : String(err)))
|
|
842
|
+
hb = null
|
|
843
|
+
}
|
|
844
|
+
;({ state: s, action: a } = next(s, hb))
|
|
845
|
+
}
|
|
846
|
+
return { ...a.report, history: s.history }
|