@dougharness/cli 1.5.0 → 1.6.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/generate/agents.js +4 -0
- package/dist/generate/agents.js.map +1 -1
- package/dist/generate/claude-md.js +1 -1
- package/dist/generate/claude-md.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/coder.md +2 -0
- package/node_modules/@dougharness/flow/agents/implementer.md +2 -0
- package/node_modules/@dougharness/flow/agents/planner.md +1 -1
- package/node_modules/@dougharness/flow/agents/reviewer.md +2 -0
- package/node_modules/@dougharness/flow/agents/tester.md +2 -0
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +16 -6
- package/node_modules/@dougharness/flow/lib/code-index.mjs +2 -2
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +4 -2
- package/node_modules/@dougharness/flow/lib/decisions.mjs +2 -2
- package/node_modules/@dougharness/flow/lib/learn.mjs +11 -4
- package/node_modules/@dougharness/flow/lib/memory.mjs +46 -21
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +16 -0
- package/node_modules/@dougharness/flow/scripts/board.mjs +5 -1
- package/node_modules/@dougharness/flow/scripts/learn.mjs +3 -1
- package/node_modules/@dougharness/flow/scripts/memory.mjs +218 -9
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +3 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +2 -0
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +6 -3
- package/node_modules/@dougharness/flow/workflows/card-queen.js +16 -6
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +1 -0
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +175 -4
- package/node_modules/@dougharness/gates/lib/test-totals.mjs +1 -1
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +69 -99
- package/node_modules/@dougharness/gates/scripts/verify.mjs +27 -9
- package/package.json +3 -3
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
// ids that the card's fetched.jsonl lists; verified true; passes = --rounds, default 1; one fixed
|
|
163
163
|
// blocker ledger entry per --review id), then reflects on it
|
|
164
164
|
// exactly as above. --commit is required (exit 2). No schema change.
|
|
165
|
-
// memory.mjs reflect --failures <card-id> [dir] [--session <id>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
165
|
+
// memory.mjs reflect --failures <card-id> [dir] [--session <id>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--detach] [--dry-run] [--json]
|
|
166
166
|
// (card memory-failure-lessons) the failure pass: reads the card's
|
|
167
167
|
// run history for stopped reports, blocks classed real and
|
|
168
168
|
// denials, prints the item count with the blocks it hid by class
|
|
@@ -172,7 +172,10 @@
|
|
|
172
172
|
// .doug/.state/memory/pitfalls.jsonl (no new lesson, no helpful).
|
|
173
173
|
// Exclusive with --hand and a report path (exit 2); idempotent on
|
|
174
174
|
// the slice; --dry-run makes no model call and prints the prompt it would send;
|
|
175
|
-
//
|
|
175
|
+
// --detach runs it as job failures-<card>; autopilot and doug-hand start it
|
|
176
|
+
// so when a card stops, --max-usd 0.5 on the cheap row (user, 2026-10-28).
|
|
177
|
+
// A real block with the same class on a file a live pitfall's recorded
|
|
178
|
+
// occurrence names is a code-side repeat (via 'file'); empty files never match
|
|
176
179
|
// memory.mjs ideas [dir] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--cap <n>] [--dry-run] [--json]
|
|
177
180
|
// (card memory-idea-proposals, ADR 0019 L3) one cheap-row `claude
|
|
178
181
|
// -p --json-schema` call (--max-usd default 0.5) over the live
|
|
@@ -275,15 +278,15 @@
|
|
|
275
278
|
// remove: [{line,enforcedBy,source}], note }); writes only a diff
|
|
276
279
|
// under .doug/.state/learn/, never CLAUDE.md itself; refuses when
|
|
277
280
|
// an edit would touch the Models section or the fenced Commands
|
|
278
|
-
// block, or would push CLAUDE.md over
|
|
281
|
+
// block, or would push CLAUDE.md over 200 lines. Approve and write
|
|
279
282
|
// it with `learn.mjs apply <diff>`
|
|
280
283
|
// lesson add and import embed only the row(s) they just wrote, best-effort, when a provider is configured: a
|
|
281
284
|
// provider failure prints one stderr line and never turns into a nonzero exit for that add/import. A pending
|
|
282
285
|
// row from an earlier add is left alone until `reembed` (or a later add/import) picks it up.
|
|
283
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, appendFileSync, statSync } from "node:fs";
|
|
286
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, appendFileSync, statSync, openSync, closeSync, renameSync, linkSync, unlinkSync } from "node:fs";
|
|
284
287
|
import { resolve, join, dirname } from "node:path";
|
|
285
288
|
import { createHash } from "node:crypto";
|
|
286
|
-
import { spawnSync } from "node:child_process";
|
|
289
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
287
290
|
import { parseAdversaryClasses, loadBoard, saveBoard, findCard } from "../lib/board.mjs";
|
|
288
291
|
import { planFor, unwrapReport } from "../lib/plan.mjs";
|
|
289
292
|
import { costRun } from "../lib/cost.mjs";
|
|
@@ -301,7 +304,7 @@ import {
|
|
|
301
304
|
} from "../lib/embeddings.mjs";
|
|
302
305
|
|
|
303
306
|
const argv = process.argv.slice(2);
|
|
304
|
-
const FLAGS = new Set(["json", "all", "hand", "dry-run", "check", "no-check", "unconfirmed"]);
|
|
307
|
+
const FLAGS = new Set(["json", "all", "hand", "dry-run", "check", "no-check", "unconfirmed", "detach", "earlier"]);
|
|
305
308
|
const REPEATABLE = new Set(["adversary", "review", "harmful"]);
|
|
306
309
|
const opts = {};
|
|
307
310
|
const positional = [];
|
|
@@ -326,8 +329,145 @@ function fail(msg, code = 1) {
|
|
|
326
329
|
process.exit(code);
|
|
327
330
|
}
|
|
328
331
|
|
|
332
|
+
// Card detach-background-chores: start `reflect` (or any command) as a detached job and read it later with
|
|
333
|
+
// `memory.mjs job <name>`. State lives in <dir>/.doug/.state/jobs/<name>.json (pid, argv, startedAt, and on exit
|
|
334
|
+
// exitCode/signal/endedAt, written by the runner); stdout and stderr go to <name>.log, never to the caller's
|
|
335
|
+
// pipes, so the caller returns at once.
|
|
336
|
+
function jobPaths(dir, name) {
|
|
337
|
+
const jobs = join(dir, ".doug", ".state", "jobs");
|
|
338
|
+
return { jobs, file: join(jobs, `${name}.json`), log: join(jobs, `${name}.log`) };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function readJobFile(file) {
|
|
342
|
+
try {
|
|
343
|
+
return JSON.parse(readFileSync(file, "utf8")) || {};
|
|
344
|
+
} catch {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// A job file is replaced whole (write a temp file, rename it over), so a concurrent reader never sees a truncated
|
|
350
|
+
// file. `ifAbsent` links the temp file in instead, so the start command never overwrites a result the runner wrote
|
|
351
|
+
// (startDetachedJob removes an ended job's old file first, so the new start record is linked in fresh).
|
|
352
|
+
function writeJobFile(file, state, ifAbsent = false) {
|
|
353
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
354
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
355
|
+
try {
|
|
356
|
+
if (ifAbsent) linkSync(tmp, file);
|
|
357
|
+
else renameSync(tmp, file);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
if (!ifAbsent || !err || err.code !== "EEXIST") throw err;
|
|
360
|
+
} finally {
|
|
361
|
+
if (ifAbsent) {
|
|
362
|
+
try {
|
|
363
|
+
unlinkSync(tmp);
|
|
364
|
+
} catch {}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function startDetachedJob(dir, name, command) {
|
|
370
|
+
const { jobs, file, log } = jobPaths(dir, name);
|
|
371
|
+
mkdirSync(jobs, { recursive: true });
|
|
372
|
+
// A name is reusable once its job ended or its process is gone; a live job holding it is never touched.
|
|
373
|
+
const held = readJobFile(file);
|
|
374
|
+
if (held && held.endedAt === undefined && held.exitCode === undefined && !held.signal && pidAlive(held.pid)) {
|
|
375
|
+
fail(`job ${name} is already running (pid ${held.pid}); read it with: memory.mjs job ${name}`, 1);
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
unlinkSync(file);
|
|
379
|
+
} catch {}
|
|
380
|
+
const fd = openSync(log, "w");
|
|
381
|
+
const startedAt = new Date().toISOString();
|
|
382
|
+
let child;
|
|
383
|
+
try {
|
|
384
|
+
child = spawn(process.execPath, [process.argv[1], "__jobrun", dir, name, "--", ...command], {
|
|
385
|
+
detached: true,
|
|
386
|
+
stdio: ["ignore", fd, fd],
|
|
387
|
+
cwd: process.cwd(),
|
|
388
|
+
env: process.env,
|
|
389
|
+
});
|
|
390
|
+
child.unref();
|
|
391
|
+
} finally {
|
|
392
|
+
closeSync(fd);
|
|
393
|
+
}
|
|
394
|
+
writeJobFile(file, { name, argv: command, pid: child.pid, startedAt }, true);
|
|
395
|
+
return { name, pid: child.pid };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// The detached runner: runs `command` with the log as its stdout/stderr, then records how it ended.
|
|
399
|
+
async function runJob(dir, name, command) {
|
|
400
|
+
const { file } = jobPaths(dir, name);
|
|
401
|
+
// Only this runner's own start record (same pid) lends startedAt and argv; nothing else carries over.
|
|
402
|
+
const prior = readJobFile(file) || {};
|
|
403
|
+
const own = prior.pid === process.pid;
|
|
404
|
+
const state = { name, argv: own && prior.argv ? prior.argv : command, startedAt: own && prior.startedAt ? prior.startedAt : new Date().toISOString(), pid: process.pid };
|
|
405
|
+
writeJobFile(file, state);
|
|
406
|
+
const end = await new Promise((resolveEnd) => {
|
|
407
|
+
const c = spawn(command[0], command.slice(1), { stdio: "inherit", cwd: process.cwd(), env: process.env });
|
|
408
|
+
c.on("error", (err) => {
|
|
409
|
+
process.stderr.write(`job: cannot start ${command[0]}: ${err.message}\n`);
|
|
410
|
+
resolveEnd({ exitCode: 127, signal: null });
|
|
411
|
+
});
|
|
412
|
+
c.on("close", (code, signal) => resolveEnd({ exitCode: code, signal }));
|
|
413
|
+
});
|
|
414
|
+
const done = { ...state, exitCode: end.exitCode, signal: end.signal, endedAt: new Date().toISOString() };
|
|
415
|
+
writeJobFile(file, done);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// One job's state as { state: "pending"|"done"|"failed", line, st }; null when there is no job file.
|
|
419
|
+
function jobStatus(dir, name) {
|
|
420
|
+
const { file, log } = jobPaths(dir, name);
|
|
421
|
+
const st = readJobFile(file);
|
|
422
|
+
if (!st) return null;
|
|
423
|
+
let lines = [];
|
|
424
|
+
try {
|
|
425
|
+
lines = readFileSync(log, "utf8").split("\n");
|
|
426
|
+
} catch {
|
|
427
|
+
lines = [];
|
|
428
|
+
}
|
|
429
|
+
const ended = st.endedAt !== undefined || st.exitCode !== undefined || st.signal;
|
|
430
|
+
const lastLine = () => lines.map((l) => l.trim()).filter(Boolean).slice(-1)[0] || "";
|
|
431
|
+
if (!ended) {
|
|
432
|
+
if (pidAlive(st.pid)) return { state: "pending", line: `${name}: pending (started ${st.startedAt}, pid ${st.pid})`, st };
|
|
433
|
+
return { state: "failed", line: `${name}: failed (the job process ${st.pid} is gone and wrote no result)${lastLine() ? `; last output: ${lastLine()}` : ""}`, st };
|
|
434
|
+
}
|
|
435
|
+
if (st.exitCode === 0 && !st.signal) {
|
|
436
|
+
const secs = st.endedAt && st.startedAt ? Math.max(0, Math.round((Date.parse(st.endedAt) - Date.parse(st.startedAt)) / 1000)) : null;
|
|
437
|
+
return { state: "done", line: `${jobSummaryLine(lines)}${secs === null ? "" : ` (job ${name}, ${secs} s)`}`, st };
|
|
438
|
+
}
|
|
439
|
+
return { state: "failed", line: `${name}: failed (${st.signal ? `signal ${st.signal}` : `exit ${st.exitCode}`})${lastLine() ? `: ${lastLine()}` : ""}`, st };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Mark a finished job as read, so a later landing's `job --earlier` does not report it again.
|
|
443
|
+
function markJobReported(dir, name, st) {
|
|
444
|
+
try {
|
|
445
|
+
writeJobFile(jobPaths(dir, name).file, { ...st, reportedAt: new Date().toISOString() });
|
|
446
|
+
} catch {}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function pidAlive(pid) {
|
|
450
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
451
|
+
try {
|
|
452
|
+
process.kill(pid, 0);
|
|
453
|
+
return true;
|
|
454
|
+
} catch (err) {
|
|
455
|
+
return !!err && err.code === "EPERM";
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function jobSummaryLine(lines) {
|
|
460
|
+
const nonEmpty = lines.filter((l) => l.trim());
|
|
461
|
+
const reflected = nonEmpty.filter((l) => /(?:^|\) )(?:Reflected on |Already reflected on )/.test(l));
|
|
462
|
+
if (reflected.length) return reflected[reflected.length - 1];
|
|
463
|
+
const flush = nonEmpty.filter((l) => !/^\s/.test(l));
|
|
464
|
+
return (flush.length ? flush : nonEmpty).slice(-1)[0] || "";
|
|
465
|
+
}
|
|
466
|
+
|
|
329
467
|
function dirFrom(args, n) {
|
|
330
|
-
|
|
468
|
+
const d = resolve(process.env.CLAUDE_PROJECT_DIR || args[n] || process.cwd());
|
|
469
|
+
if (!existsSync(join(d, ".doug"))) fail(`not a Doug project: ${d} has no .doug/`, 1);
|
|
470
|
+
return d;
|
|
331
471
|
}
|
|
332
472
|
|
|
333
473
|
function verifiedWord(v) {
|
|
@@ -771,7 +911,7 @@ try {
|
|
|
771
911
|
'usage: memory.mjs reflect <report.json> [dir] [--run <wf-id>] [--card <id>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--harmful <id>]... [--claude-bin <path>] [--claude-dir <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
772
912
|
if (opts.failures !== undefined) {
|
|
773
913
|
const failuresUsage =
|
|
774
|
-
"usage: memory.mjs reflect --failures <card-id> [dir] [--session <id>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]";
|
|
914
|
+
"usage: memory.mjs reflect --failures <card-id> [dir] [--session <id>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--detach] [--dry-run] [--json]";
|
|
775
915
|
const card = textOrNull(opts.failures);
|
|
776
916
|
if (!card || opts.hand) fail(`${failuresUsage}\n--failures takes a card id and is exclusive with --hand and a report path.`, 2);
|
|
777
917
|
if (rest.length > 1) fail(failuresUsage, 2);
|
|
@@ -786,6 +926,11 @@ try {
|
|
|
786
926
|
if (isFile) fail(`${failuresUsage}\n--failures takes a card id and is exclusive with --hand and a report path.`, 2);
|
|
787
927
|
}
|
|
788
928
|
const fdir = dirFrom(rest, 0);
|
|
929
|
+
if (opts.detach) {
|
|
930
|
+
const started = startDetachedJob(fdir, `failures-${card}`, [process.execPath, process.argv[1], ...argv.filter((a) => a !== "--detach")]);
|
|
931
|
+
process.stdout.write(`${started.name}: started detached (pid ${started.pid}); read it with: memory.mjs job ${started.name}\n`);
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
789
934
|
const fmodels = loadModels(fdir);
|
|
790
935
|
const fmodel = textOrNull(opts.model) || (fmodels.tiers.cheap && fmodels.tiers.cheap.model) || "haiku";
|
|
791
936
|
let fmaxUsd = 0.5;
|
|
@@ -874,6 +1019,11 @@ try {
|
|
|
874
1019
|
} catch (err) {
|
|
875
1020
|
fail(`${handUsage}\n${err.message}`, 2);
|
|
876
1021
|
}
|
|
1022
|
+
if (opts.detach) {
|
|
1023
|
+
const started = startDetachedJob(dir, `reflect-${id}`, [process.execPath, process.argv[1], ...argv.filter((a) => a !== "--detach")]);
|
|
1024
|
+
process.stdout.write(`${started.name}: started detached (pid ${started.pid}); read it with: memory.mjs job ${started.name}\n`);
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
877
1027
|
const base = join(dir, ".doug", ".state", "reports", id);
|
|
878
1028
|
const readDirFiles = (sub) => {
|
|
879
1029
|
const d = join(base, sub);
|
|
@@ -958,6 +1108,12 @@ try {
|
|
|
958
1108
|
fail(`cannot parse report file ${reportPath} as JSON: ${err.message}`);
|
|
959
1109
|
}
|
|
960
1110
|
classes = parseAdversaryClasses(opts.adversary || []);
|
|
1111
|
+
if (opts.detach) {
|
|
1112
|
+
const tag = textOrNull(opts.card) || textOrNull(opts.run) || createHash("sha256").update(reportText).digest("hex").slice(0, 12);
|
|
1113
|
+
const started = startDetachedJob(dir, `reflect-${tag}`, [process.execPath, process.argv[1], ...argv.filter((a) => a !== "--detach")]);
|
|
1114
|
+
process.stdout.write(`${started.name}: started detached (pid ${started.pid}); read it with: memory.mjs job ${started.name}\n`);
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
961
1117
|
}
|
|
962
1118
|
const reportHash = createHash("sha256").update(reportText).digest("hex");
|
|
963
1119
|
const plan = planFor(dir, opts.card);
|
|
@@ -1051,6 +1207,59 @@ try {
|
|
|
1051
1207
|
}
|
|
1052
1208
|
break;
|
|
1053
1209
|
}
|
|
1210
|
+
case "__jobrun": {
|
|
1211
|
+
const raw = process.argv.slice(2);
|
|
1212
|
+
const sep = raw.indexOf("--");
|
|
1213
|
+
if (sep < 3 || sep === raw.length - 1) fail("usage: memory.mjs __jobrun <dir> <name> -- <command> [args...]", 2);
|
|
1214
|
+
await runJob(raw[1], raw[2], raw.slice(sep + 1));
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
case "job": {
|
|
1218
|
+
const usage = "usage: memory.mjs job <name> [dir] [--wait <seconds>] | memory.mjs job --earlier [dir]";
|
|
1219
|
+
if (opts.earlier) {
|
|
1220
|
+
// Earlier landings' jobs nobody has read yet: pending, failed, or finished with the summary never printed.
|
|
1221
|
+
const edir = dirFrom(rest, 0);
|
|
1222
|
+
const { jobs } = jobPaths(edir, "x");
|
|
1223
|
+
let names = [];
|
|
1224
|
+
try {
|
|
1225
|
+
names = readdirSync(jobs).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5));
|
|
1226
|
+
} catch {
|
|
1227
|
+
names = [];
|
|
1228
|
+
}
|
|
1229
|
+
let failed = false;
|
|
1230
|
+
let any = false;
|
|
1231
|
+
for (const n of names.sort()) {
|
|
1232
|
+
const js = jobStatus(edir, n);
|
|
1233
|
+
if (!js || js.st.reportedAt) continue;
|
|
1234
|
+
any = true;
|
|
1235
|
+
process.stdout.write(`earlier job ${js.line}\n`);
|
|
1236
|
+
if (js.state === "failed") failed = true;
|
|
1237
|
+
if (js.state !== "pending") markJobReported(edir, n, js.st);
|
|
1238
|
+
}
|
|
1239
|
+
if (!any) process.stdout.write("no earlier jobs unread\n");
|
|
1240
|
+
if (failed) process.exit(1);
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
const [name] = rest;
|
|
1244
|
+
if (!name || !/^[A-Za-z0-9._-]+$/.test(name)) fail(usage, 2);
|
|
1245
|
+
let waitSec = 0;
|
|
1246
|
+
if (opts.wait !== undefined) {
|
|
1247
|
+
waitSec = /^\d+$/.test(String(opts.wait)) ? Number(opts.wait) : NaN;
|
|
1248
|
+
if (!Number.isInteger(waitSec)) fail(`${usage}\n--wait must be a whole number of seconds, got ${JSON.stringify(opts.wait)}.`, 2);
|
|
1249
|
+
}
|
|
1250
|
+
const jdir = dirFrom(rest, 1);
|
|
1251
|
+
const deadline = Date.now() + waitSec * 1000;
|
|
1252
|
+
let js = jobStatus(jdir, name);
|
|
1253
|
+
if (!js) fail(`${name}: no such job in ${jdir}`, 1);
|
|
1254
|
+
while (js.state === "pending" && Date.now() < deadline) {
|
|
1255
|
+
await new Promise((r) => setTimeout(r, Math.min(500, Math.max(1, deadline - Date.now()))));
|
|
1256
|
+
js = jobStatus(jdir, name) || js;
|
|
1257
|
+
}
|
|
1258
|
+
process.stdout.write(`${js.line}\n`);
|
|
1259
|
+
if (js.state !== "pending") markJobReported(jdir, name, js.st);
|
|
1260
|
+
if (js.state === "failed") process.exit(1);
|
|
1261
|
+
break;
|
|
1262
|
+
}
|
|
1054
1263
|
case "outcomes": {
|
|
1055
1264
|
const dir = dirFrom(rest, 0);
|
|
1056
1265
|
const { openMemory, listOutcomes } = await import("../lib/memory.mjs");
|
|
@@ -2080,7 +2289,7 @@ try {
|
|
|
2080
2289
|
break;
|
|
2081
2290
|
}
|
|
2082
2291
|
default:
|
|
2083
|
-
fail("usage: memory.mjs <record|reflect|outcomes|arms|lessons|lesson|history|model|import|recall|brief-lessons|doctor|reembed|embed-check|index|condition|decision|rule|claude-md> ...", 2);
|
|
2292
|
+
fail("usage: memory.mjs <record|reflect|job|outcomes|arms|lessons|lesson|history|model|import|recall|brief-lessons|doctor|reembed|embed-check|index|condition|decision|rule|claude-md> ...", 2);
|
|
2084
2293
|
}
|
|
2085
2294
|
} catch (err) {
|
|
2086
2295
|
fail(err.message);
|
|
@@ -86,7 +86,7 @@ Before the commit the lead itself runs `node .doug/hooks/scripts/verify.mjs` (th
|
|
|
86
86
|
|
|
87
87
|
Stay within the card's goal. A defect found on the way that the goal does not cover becomes a new hand-track card, not part of this commit.
|
|
88
88
|
|
|
89
|
-
When a by-hand run stops a card after Start (a hook block it reports, a design question answered stop, a gate it cannot get green, an unresolved reviewer blocker, or the user saying stop), the lead runs the recording step once: `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" record <id> --hand --stop <reason> --detail "<one line>"`. `<reason>` is one of the card loop's stop reasons (`lib/card-loop.mjs` `STOP_REASONS`) or `user-stopped` when the user stopped it.
|
|
89
|
+
When a by-hand run stops a card after Start (a hook block it reports, a design question answered stop, a gate it cannot get green, an unresolved reviewer blocker, or the user saying stop), the lead runs the recording step once: `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" record <id> --hand --stop <reason> --detail "<one line>"`. `<reason>` is one of the card loop's stop reasons (`lib/card-loop.mjs` `STOP_REASONS`) or `user-stopped` when the user stopped it. Then it starts the failure pass once, detached: `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect --failures <id> --max-usd 0.5 --detach` (cheap row, capped at $0.5; the doug-hand start is the approval; user, 2026-10-28). A start that fails does not stop the card's hand-back.
|
|
90
90
|
|
|
91
91
|
## 4. Record and land
|
|
92
92
|
|
|
@@ -106,6 +106,8 @@ Follow-ups are filed without a question. A landing with no candidates files noth
|
|
|
106
106
|
|
|
107
107
|
The lead says in the chat what it filed and where: each new card's id, title, and its Backlog position with the reason. Then one commit `"Board: <id> follow-ups added: <new-id>, <new-id>"`, whatever the count; the served page (`doug board serve`) shows the new cards without a republish. Batch: each card's deps name the card of the batch it came from; still one commit for the whole batch.
|
|
108
108
|
|
|
109
|
+
Last read: after the landing commit and the follow-up step, and before the final message, the lead reads the reflect that `run-report` started detached: `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" job reflect-<id> --wait 120`. It polls until the job is done or failed, or 120 s have passed. Its line (reflect's one-line summary with the measured cost) goes in the chat. `pending` after the cap is said as pending in the chat and the landing goes on; the next landing's `run-report` reads it with `memory.mjs job --earlier`. A `failed` line (exit 1) is said in the chat with its reason and the landing goes on regardless: reflect is a log, not a gate. Batch: one read for the batch's job.
|
|
110
|
+
|
|
109
111
|
## Cards the user asks for
|
|
110
112
|
|
|
111
113
|
A card the user asks for is a mini ADR, not a one-line goal. Before filing it, the lead asks the scoping questions in one `AskUserQuestion` of up to 4 questions, and writes the answers into the goal. The goal carries the fixed headings Context / Decision / Options / Questions; `docs/decisions` stays for real ADRs. Only a user-requested card takes this form: a follow-up the lead files keeps step 5's lighter form, so autopilot follow-ups stay question-free. The goal opens with the user's request quoted verbatim under Context, and each Decision line is marked (user answer) or (lead, not asked). One question names the broadest word in the user's request and offers its readings. An option is marked Recommended only when its description cites the fact it rests on, checked before asking; an option resting on an unchecked fact says (unverified) and is not Recommended, and a question with no checked option has no Recommended. When the user asked for research, "Let the research decide" is the Recommended option. A card the user asks for while autopilot runs is scoped at once with the same questions, filed to Backlog, and the queue resumes at the card it was on: the request is input, not a gate, and the running queue is unchanged.
|
|
@@ -88,6 +88,8 @@ Follow-ups are filed without a question. A landing with no candidates files noth
|
|
|
88
88
|
|
|
89
89
|
The lead says in the chat what it filed and where: each new card's id, title, and its Backlog position with the reason. Then one commit `"Board: <id> follow-ups added: <new-id>, <new-id>"`, whatever the count; the served page (`doug board serve`) shows the new cards without a republish. Batch: each card's deps name the card of the batch it came from; still one commit for the whole batch.
|
|
90
90
|
|
|
91
|
+
Last read: after the landing commit and the follow-up step, and before the final message, the lead reads the reflect that `run-report` started detached: `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" job reflect-<card id, else the run id> (a batch: reflect-<run id>) --wait 120`. It polls until the job is done or failed, or 120 s have passed. Its line (reflect's one-line summary with the measured cost) goes in the chat. `pending` after the cap is said as pending in the chat and the landing goes on; the next landing's `run-report` reads it with `memory.mjs job --earlier`. A `failed` line (exit 1) is said in the chat with its reason and the landing goes on regardless: reflect is a log, not a gate. Batch: one read for the batch's job.
|
|
92
|
+
|
|
91
93
|
## Cards the user asks for
|
|
92
94
|
|
|
93
95
|
A card the user asks for is a mini ADR, not a one-line goal. Before filing it, the lead asks the scoping questions in one `AskUserQuestion` of up to 4 questions, and writes the answers into the goal. The goal carries the fixed headings Context / Decision / Options / Questions; `docs/decisions` stays for real ADRs. Only a user-requested card takes this form: a follow-up the lead files keeps section 6's lighter form, so autopilot follow-ups stay question-free. The goal opens with the user's request quoted verbatim under Context, and each Decision line is marked (user answer) or (lead, not asked). One question names the broadest word in the user's request and offers its readings. An option is marked Recommended only when its description cites the fact it rests on, checked before asking; an option resting on an unchecked fact says (unverified) and is not Recommended, and a question with no checked option has no Recommended. When the user asked for research, "Let the research decide" is the Recommended option. A card the user asks for while autopilot runs is scoped at once with the same questions, filed to Backlog, and the queue resumes at the card it was on: the request is input, not a gate, and the running queue is unchanged.
|
|
@@ -19,8 +19,9 @@ With the card id, the report (`.doug/.state/last-report.json` unless another pat
|
|
|
19
19
|
3. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json> --run <run-id> --wall "<m> min" --commit <sha> --cost <usd> --summary "<one sentence>" [--codex-cost <usd>] --adversary "F1=<class>: <reason>"` with `--adversary` repeated once per block, leaving `--cost` out when step 1 gave no number and `--codex-cost` out when step 1 showed no Codex line. This appends the entry to `docs/live-runs.md`: the measures table with the precision line (n real / n marginal / n false), the per-level, per-task table, and one line per classified block. It also promotes `.doug/.state/research/<id>.md` to `docs/research/<id>.md` when that note exists, printing one line saying so; add that path to the landing commit alongside `docs/live-runs.md`.
|
|
20
20
|
4. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" import`: rolls any auto-memory file written since the last import into the lessons store — it is idempotent, a rewritten file supersedes its old row, and it never writes under the auto-memory directory. Its one-line output goes in the chat; an exit 1 naming Node is said in the chat and the landing goes on, the same as record below.
|
|
21
21
|
5. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" record <report.json> --run <run-id> --card <id> --commit <sha> --adversary "F1=<class>: <reason>"` with `--adversary` repeated once per block, the same values step 3 used, and `--commit` left out for a run that stopped. It appends one row per task of the report to the local outcome log `.doug/.state/memory/memory.db` (uncommitted; run id, card, spec hash, owned files from the plan, size, shape, models, verify and review results, the classified adversary blocks, fix passes, budget, tokens and USD measured from the same transcripts `cost.mjs` read, or null where nothing was measured). It needs Node >=22.16 (node:sqlite). When it exits 1 naming Node, say so in the chat and go on, since the record is a log, not a gate.
|
|
22
|
-
6. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect <report.json> --run <run-id> --card <id> --commit <sha> --adversary "F1=<class>: <reason>"` with `--adversary` repeated once per block, the same values step 5 used. It applies deterministic helpful counters to any lesson a task's `memoryUsed` names. A real block no longer bumps harmful; pass `--harmful <id>` once per recalled lesson the lead judges actually misled the work (it must be in a task's `memoryUsed`, else exit 1). Give `--harmful` on the report's first reflect: a rerun is "Already reflected" and ignores it. Then it runs a cheap-row `claude -p --json-schema` pass that proposes and appends at most three lessons from the report, filtered in code.
|
|
22
|
+
6. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect <report.json> --run <run-id> --card <id> --commit <sha> --adversary "F1=<class>: <reason>" --detach` with `--adversary` repeated once per block, the same values step 5 used. It applies deterministic helpful counters to any lesson a task's `memoryUsed` names. A real block no longer bumps harmful; pass `--harmful <id>` once per recalled lesson the lead judges actually misled the work (it must be in a task's `memoryUsed`, else exit 1). Give `--harmful` on the report's first reflect: a rerun is "Already reflected" and ignores it. Then it runs a cheap-row `claude -p --json-schema` pass that proposes and appends at most three lessons from the report, filtered in code. Before it starts, `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" job --earlier` reads any job an earlier landing left pending, failed, or finished unread: each line goes in the chat, a failed one with its reason, and the landing goes on. It starts detached and returns at once (the model call runs in the background, about 80 s); the caller reads its result at the end of the landing. If the `--detach` start is refused (`job reflect-<id> is already running (pid <pid>) ...`), put that line in the chat verbatim, say this landing's reflect did not start because an earlier reflect holds the name, and the landing goes on (reflect is a log, not a gate); the end-of-landing `job reflect-<id> --wait 120` read then reports that running job.
|
|
23
23
|
7. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --run <run-id> --wall "<m> min" --commit <sha> --summary "<one sentence>" [--codex-cost <usd>]` (same `--cost` rule) and put its sentence first in the chat, outside the fence (it is the same sentence step 3's record printed, so it goes in the chat once), then its three lines verbatim in a fenced code block: the outcome, the tasks, and what stopped or was not met. The workflow form's landing block is printed by `board.mjs record` (both forms) and by `board.mjs summary --card` as their last three lines (Changed, Outcome, Learned; at most 100 words, `--note` feeds Learned); plain `summary` (no `--card`) prints no block. Your final message carries it verbatim, and the Stop gate blocks until it does.
|
|
24
|
+
8. Reading the detached reflect is not this skill's step: the caller reads it as the last thing of the landing, after the landing commit and the follow-up step, with `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" job reflect-<card id, else the run id> --wait 120` (doug-hand section 5, doug-next section 6). That read polls until the job is done or failed, or 120 s have passed. Its line (reflect's one-line summary with the measured cost) goes in the chat. `pending` after the cap is said as pending: pending is said as pending in the chat, reflect is still running and the landing goes on; the next landing's `memory.mjs job --earlier` reads it. A `failed` line (exit 1) is said in the chat with its reason and the landing goes on regardless — reflect is a log, not a gate, the same as record.
|
|
24
25
|
|
|
25
26
|
A run that stopped is recorded the same way, without `--commit`; the summary's third line quotes each task's stop reason.
|
|
26
27
|
|
|
@@ -33,8 +34,9 @@ When `.doug/plan.json` carries `cards` (made by `plan.mjs merge`), the run is re
|
|
|
33
34
|
3. For each card, in the plan's `cards` order: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json> --run <run-id> --wall "<m> min" --commit <sha> --summary "<one sentence>" --cost <that card's usd> --shared-cost <shared usd> [--codex-cost <usd>, first card only] --adversary ...`. Record reads the batch from the plan file: the entry names the other cards, shows only that card's tasks and the integration rows of their levels, and counts the shared cost once, under the first card.
|
|
34
35
|
4. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" import` once for the whole report, the same as the workflow-run form's step 4: its one-line output goes in the chat, an exit 1 naming Node is said in the chat and the landing goes on, and it is idempotent, a rewritten file supersedes its old row, and it never writes under the auto-memory directory.
|
|
35
36
|
5. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" record <report.json> --run <run-id> --commit <sha> --adversary ...` once for the whole report, with every card's block classes (each task carries its card in a batch report; no `--card`).
|
|
36
|
-
6. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect <report.json> --run <run-id> --commit <sha> --adversary
|
|
37
|
+
6. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect <report.json> --run <run-id> --commit <sha> --adversary ... --detach` once for the whole report, no `--card` (each appended lesson's card comes from its own task). A real block no longer bumps harmful; pass `--harmful <id>` once per recalled lesson the lead judges actually misled the work (it must be in a task's `memoryUsed`, else exit 1). Give `--harmful` on the report's first reflect: a rerun is "Already reflected" and ignores it. Before it starts, `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" job --earlier` reads any job an earlier landing left pending, failed, or finished unread: each line goes in the chat, a failed one with its reason, and the landing goes on. If the `--detach` start is refused (`job reflect-<id> is already running (pid <pid>) ...`), put that line in the chat verbatim, say this landing's reflect did not start because an earlier reflect holds the name, and the landing goes on (reflect is a log, not a gate); the end-of-landing `job reflect-<id> --wait 120` read then reports that running job.
|
|
37
38
|
7. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --card <id> --run <run-id> --wall "<m> min" --commit <sha> --summary "<one sentence>"` per card (same `--cost` rule), and put each card's three lines in the chat. Its "Acceptance not met" line reads only that card's own tasks by their `[<card>] ` tag (or the final integration level's acceptance, when the report carries it), so another card's failing command, or a command that depends on a level not yet run, never shows as this card's; the line names which source it used. In a batch, each card's landing block (one per card) goes in the final message verbatim.
|
|
39
|
+
8. Reading the detached reflect is not this skill's step: the caller reads it as the last thing of the landing, after the landing commit and the follow-up step, with `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" job reflect-<run id> --wait 120` (doug-hand section 5, doug-next section 6). That read polls until the job is done or failed, or 120 s have passed. Its line (reflect's one-line summary with the measured cost) goes in the chat. `pending` after the cap is said as pending: pending is said as pending in the chat, reflect is still running and the landing goes on; the next landing's `memory.mjs job --earlier` reads it. A `failed` line (exit 1) is said in the chat with its reason and the landing goes on regardless — reflect is a log, not a gate, the same as record.
|
|
38
40
|
|
|
39
41
|
## A hand-track landing
|
|
40
42
|
|
|
@@ -43,7 +45,8 @@ With the card id, the landing commit, the wall clock, and the gate line — one
|
|
|
43
45
|
1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> --hand --commit <sha> --wall "<m> min" --gate "<gate line>" --summary "<one sentence>" --cost <usd>`, with `--note "<one line>"` when something about the build is worth keeping. `<usd>` is the card's measured cost, never an estimate: `node "${CLAUDE_PLUGIN_ROOT}/scripts/cost.mjs" --session <session> --since <openedAt>` prices the lead session and its subagents from the card's `openedAt` (in `.doug/.state/reports/<id>/condition.json`) to now, where `<session>` is `CLAUDE_CODE_SESSION_ID` (not the card id); take the session total's dollar figure. When nothing was measured (no condition.json, no session id, or cost.mjs exits non-zero), leave `--cost` out: the entry then has no Cost row, never a `$0`, and you never estimate one. This appends the hand-track entry to `docs/live-runs.md`; its table is the summary. It also promotes `.doug/.state/research/<id>.md` to `docs/research/<id>.md` when that note exists, printing one line saying so; add that path to the landing commit alongside `docs/live-runs.md`.
|
|
44
46
|
2. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" import`, the same as above: its one-line output goes in the chat, an exit 1 naming Node is said in the chat and the landing goes on, and it is idempotent, supersedes a rewritten file, and never writes under the auto-memory directory.
|
|
45
47
|
3. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" record <id> --hand --commit <sha> --wall "<m> min" --gate "<gate line>" [--cost <usd>] [--note "<one line>"]`, the same values step 1 used, leaving `--cost` out when step 1 did. It records one row in the local outcome log `.doug/.state/memory/memory.db` (uncommitted), keyed `hand:<sha>` so re-recording the same commit updates that row and track `hand`; a card that lands twice, in two commits, gets two rows. Its workflow-only columns (verified, review counts, adversary verdict, blocks, fix passes, budget, tokens, the report itself) are null because they do not exist on this track, never because they measured zero: no independent verifier ran, and the gate's own text is the `gate` column instead. The USD column is the measured `--cost`, null when none was given. It needs Node >=22.16 (node:sqlite). When it exits 1 naming Node, say so in the chat and go on, since the record is a log, not a gate.
|
|
46
|
-
4. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect --hand <id> --commit <sha> --rounds <n> [--review "<key>=<class>: <reason>"]... (the key is "`R<n>` or `<card>/R<n>`") [--harmful <id>]... [--note "<one line>"]`. It reads the briefs, reviewer reports and lessons.json doug-hand saved, applies helpful counters to the card's memoryUsed (only lessons fetched with `lesson show <id> --card <id>`). A real block no longer bumps harmful; pass `--harmful <id>` once per recalled lesson the lead judges actually misled the work (it must be in a task's `memoryUsed`, else exit 1). Give `--harmful` on the report's first reflect: a rerun is "Already reflected" and ignores it. It proposes at most three lessons on the cheap row, filtered in code.
|
|
48
|
+
4. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect --hand <id> --commit <sha> --rounds <n> [--review "<key>=<class>: <reason>"]... (the key is "`R<n>` or `<card>/R<n>`") [--harmful <id>]... [--note "<one line>"] --detach`. It reads the briefs, reviewer reports and lessons.json doug-hand saved, applies helpful counters to the card's memoryUsed (only lessons fetched with `lesson show <id> --card <id>`). A real block no longer bumps harmful; pass `--harmful <id>` once per recalled lesson the lead judges actually misled the work (it must be in a task's `memoryUsed`, else exit 1). Give `--harmful` on the report's first reflect: a rerun is "Already reflected" and ignores it. It proposes at most three lessons on the cheap row, filtered in code. Before it starts, `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" job --earlier` reads any job an earlier landing left pending, failed, or finished unread: each line goes in the chat, a failed one with its reason, and the landing goes on. It starts detached and returns at once, so the model call does not hold the landing; the caller reads its result at the end of the landing. Reflect is a log, not a gate, the same as record. If the `--detach` start is refused (`job reflect-<id> is already running (pid <pid>) ...`), put that line in the chat verbatim, say this landing's reflect did not start because an earlier reflect holds the name, and the landing goes on (reflect is a log, not a gate); the end-of-landing `job reflect-<id> --wait 120` read then reports that running job.
|
|
49
|
+
5. Reading the detached reflect is not this skill's step: the caller reads it as the last thing of the landing, after the landing commit and the follow-up step, with `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" job reflect-<id> --wait 120` (doug-hand section 5, doug-next section 6). That read polls until the job is done or failed, or 120 s have passed. Its line (reflect's one-line summary with the measured cost) goes in the chat. `pending` after the cap is said as pending: pending is said as pending in the chat, reflect is still running and the landing goes on; the next landing's `memory.mjs job --earlier` reads it. A `failed` line (exit 1) is said in the chat with its reason and the landing goes on regardless — reflect is a log, not a gate, the same as record.
|
|
47
50
|
|
|
48
51
|
Say the one-sentence summary record printed first, then the commit, the wall clock, and the gate line in the chat. In the hand form the landing block is printed by `board.mjs record` (both forms) and by `board.mjs summary --card` as their last three lines (Changed, Outcome, Learned; at most 100 words, `--note` feeds Learned); plain `summary` (no `--card`) prints no block. Your final message carries it verbatim, and the Stop gate blocks until it does.
|
|
49
52
|
|
|
@@ -118,6 +118,14 @@ const BRIEF_RULES = {
|
|
|
118
118
|
seats: ["coder", "reviewer", "tester"],
|
|
119
119
|
when: "mutations",
|
|
120
120
|
},
|
|
121
|
+
"suites-through-verify": {
|
|
122
|
+
text: "Run a full test suite only through `node .doug/hooks/scripts/verify.mjs`, never the gate's commands (the commands in `stopGate.commands`) directly, with or without CI=1: verify.mjs takes gate.lock, so your suite queues behind a running gate instead of overlapping it.",
|
|
123
|
+
seats: ["tester", "coder", "reviewer", "gate"],
|
|
124
|
+
},
|
|
125
|
+
"tests-take-gate-lock": {
|
|
126
|
+
text: "Run every other test command, a single test file or a mutation's test run in a scratch copy included, as `node <card worktree>/.doug/hooks/lib/gate-lock.mjs run -- <command>` with the Bash timeout set to 600000, so it waits on gate.lock instead of overlapping a gate; never wrap verify.mjs in gate-lock.mjs, since verify.mjs takes the lock itself. Put env vars right after `--` (`-- CI=1 pnpm exec vitest run <file>`) and quote a compound command as one argument (`-- 'cd dir && vitest run x'`).",
|
|
127
|
+
seats: ["tester", "coder", "reviewer"],
|
|
128
|
+
},
|
|
121
129
|
"reviewer-reruns-mutations": {
|
|
122
130
|
text: "Rerun every listed mutation yourself and add your own (own: true); read the diff only after running them.",
|
|
123
131
|
seats: ["reviewer"],
|
|
@@ -870,15 +878,17 @@ function next(state, handBack) {
|
|
|
870
878
|
// An evidenced major gets the same one fix pass; a major without evidence and a minor stay advisory.
|
|
871
879
|
const majors = findings.filter((f) => f.severity === "major" && nonBlank(f.evidence));
|
|
872
880
|
if (blockers.length === 0 && majors.length === 0) return advance(s, "gate", "ok", { acc });
|
|
873
|
-
//
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
881
|
+
// Each user answer to a reviewer stop gets one coder round when the reviewer that carried it still has an open finding.
|
|
882
|
+
const given = s.answerCoderRounds || [];
|
|
883
|
+
const fresh = (s.answers || [])
|
|
884
|
+
.filter((a) => a.seat === "reviewer" && (a.round ?? 1) <= (s.seatRuns.reviewer || 0))
|
|
885
|
+
.map((a) => a.round ?? 1)
|
|
886
|
+
.filter((r) => !given.includes(r));
|
|
887
|
+
if (s.counters.fixPasses >= 1 && fresh.length > 0) {
|
|
878
888
|
return advance(s, buildSeat(s), "blockers", {
|
|
879
889
|
acc,
|
|
880
890
|
fixFor: "review",
|
|
881
|
-
|
|
891
|
+
answerCoderRounds: [...given, ...fresh],
|
|
882
892
|
lastFindings: findings.filter((f) => f.severity !== "minor"),
|
|
883
893
|
});
|
|
884
894
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doug-gates",
|
|
3
3
|
"displayName": "Doug Gates",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
5
5
|
"description": "Deterministic guardrails for Claude Code: protected paths, bash guard, secret scan on edits and commands, format on edit, edit-loop detection, a verification gate on Stop and on SubagentStop, a snapshot of the plan and session into the anchor before compaction and re-anchoring after it, and a run trace of subagents and tool calls. Configured by .doug/config.json.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Doug contributors"
|
|
@@ -215,6 +215,7 @@ export function appendBlockLine(dir, { hook, agent, reason, class: cls, session,
|
|
|
215
215
|
class: cls,
|
|
216
216
|
session: session ?? null,
|
|
217
217
|
...plain,
|
|
218
|
+
files: Array.isArray(plain.files) ? plain.files : [],
|
|
218
219
|
}) + "\n";
|
|
219
220
|
mkdirSync(join(dir, ".doug/.state"), { recursive: true });
|
|
220
221
|
appendFileSync(join(dir, BLOCKS_RELPATH), line);
|