@dougharness/cli 1.2.0 → 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/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- 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/hooks/register.tsx +50 -31
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +35 -3
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +25 -0
- package/node_modules/@dougharness/flow/lib/decisions.mjs +4 -2
- package/node_modules/@dougharness/flow/lib/memory.mjs +358 -64
- package/node_modules/@dougharness/flow/lib/plan.mjs +1 -1
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +37 -4
- package/node_modules/@dougharness/flow/scripts/memory.mjs +13 -1
- package/node_modules/@dougharness/flow/scripts/plan.mjs +5 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/workflows/card-queen.js +43 -4
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +56 -16
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +119 -15
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +18 -4
- package/package.json +3 -3
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// autopilot.mjs filed <id> --kind <follow-up|user> [--from <card>] --position "<text>"
|
|
11
11
|
// autopilot.mjs caps [--usd <n>] [--wall <min>] [--context <pct>]
|
|
12
12
|
// autopilot.mjs pick [--max <n>] JSON { launch, needsPlan, held, alone }; a held line per held card on stderr
|
|
13
|
-
// autopilot.mjs queen-args <id> --kind <flow|hand> writes .doug/.state/reports/<id>/queen-args.json, prints
|
|
13
|
+
// autopilot.mjs queen-args <id> --kind <flow|hand> [--with <id,id,...>] writes .doug/.state/reports/<id>/queen-args.json and the per-card queen.js beside it, prints both paths (args, then queen.js)
|
|
14
|
+
// autopilot.mjs agent-types --kind <flow|hand> prints the agent types that queen spawns, one per line
|
|
14
15
|
// autopilot.mjs launched <id> --run <wf_id> --kind <flow|hand> --worktree <abs> --args-file <path>
|
|
15
16
|
// autopilot.mjs report --run <wf_id> --file <result.json> saves the queen's report, prints land|resume|ask
|
|
16
17
|
// autopilot.mjs resumed <id> --run <wf_id>
|
|
@@ -18,13 +19,13 @@
|
|
|
18
19
|
// autopilot.mjs end
|
|
19
20
|
// --dir <d> names the project (default cwd). Exit 0 done; 1 no run or unknown card; 2 bad usage.
|
|
20
21
|
import { execFileSync } from "node:child_process";
|
|
21
|
-
import { copyFileSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { copyFileSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
22
23
|
import { dirname, join, resolve } from "node:path";
|
|
23
24
|
import { fileURLToPath } from "node:url";
|
|
24
25
|
import { loadModels } from "../lib/models.mjs";
|
|
25
26
|
import { AUTOPILOT_FILE, STEPS, readAutopilot } from "../lib/autopilot-view.mjs";
|
|
26
27
|
import { contextThreshold } from "../lib/context.mjs";
|
|
27
|
-
import { classifyNotification, pickCards, workersMax } from "../lib/coordinator.mjs";
|
|
28
|
+
import { classifyNotification, pickCards, queenLabel, renameQueenMeta, workersMax } from "../lib/coordinator.mjs";
|
|
28
29
|
import { nextReadyCards } from "../lib/board.mjs";
|
|
29
30
|
import { planFor, reportPathFor } from "../lib/plan.mjs";
|
|
30
31
|
|
|
@@ -313,7 +314,39 @@ switch (cmd) {
|
|
|
313
314
|
const out = join(dir, ".doug/.state/reports", id, "queen-args.json");
|
|
314
315
|
mkdirSync(dirname(out), { recursive: true });
|
|
315
316
|
writeFileSync(out, JSON.stringify(args) + "\n");
|
|
316
|
-
|
|
317
|
+
const flying = (readAutopilot(dir)?.cards ?? []).filter((c) => ["running", "done", "landing"].includes(c.state)).map((c) => c.id);
|
|
318
|
+
const batch = String(opts.with ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
319
|
+
const srcFile = join(dirname(fileURLToPath(import.meta.url)), "..", "workflows", kind === "hand" ? "card-queen.js" : "doug-implement.js");
|
|
320
|
+
let renamed;
|
|
321
|
+
try {
|
|
322
|
+
renamed = renameQueenMeta(readFileSync(srcFile, "utf8"), { kind, label: queenLabel(id, [...flying, ...batch]) });
|
|
323
|
+
} catch (e) {
|
|
324
|
+
fail(`cannot build the per-card queen from ${srcFile}: ${e.message}`);
|
|
325
|
+
}
|
|
326
|
+
const script = join(dirname(out), "queen.js");
|
|
327
|
+
rmSync(script, { force: true });
|
|
328
|
+
writeFileSync(script, renamed);
|
|
329
|
+
process.stdout.write(out + "\n" + script + "\n");
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case "agent-types": {
|
|
333
|
+
const kind = need("kind");
|
|
334
|
+
if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
|
|
335
|
+
const wf = join(dirname(fileURLToPath(import.meta.url)), "..", "workflows", kind === "hand" ? "card-queen.js" : "doug-implement.js");
|
|
336
|
+
let src;
|
|
337
|
+
try {
|
|
338
|
+
src = readFileSync(wf, "utf8");
|
|
339
|
+
} catch (e) {
|
|
340
|
+
fail(`cannot read ${wf}: ${e.message}`);
|
|
341
|
+
}
|
|
342
|
+
if (kind === "hand") {
|
|
343
|
+
const m = src.match(/const AGENT_TYPES = \{([^}]*)\}/);
|
|
344
|
+
if (!m) fail("AGENT_TYPES not found in card-queen.js");
|
|
345
|
+
src = m[1];
|
|
346
|
+
}
|
|
347
|
+
const types = [...new Set([...src.matchAll(kind === "hand" ? /'(doug-flow:[\w-]+)'/g : /agentType: '([^']+)'/g)].map((x) => x[1]))];
|
|
348
|
+
if (types.length === 0) fail("no agent types found");
|
|
349
|
+
process.stdout.write(types.join("\n") + "\n");
|
|
317
350
|
break;
|
|
318
351
|
}
|
|
319
352
|
case "launched": {
|
|
@@ -97,7 +97,11 @@
|
|
|
97
97
|
// 0); --dry-run applies nothing but still runs the LLM pass and
|
|
98
98
|
// prints what would happen. Cost is measured from the same local
|
|
99
99
|
// transcripts cost.mjs reads, falling back to the CLI's own
|
|
100
|
-
// cost estimate.
|
|
100
|
+
// cost estimate. For each appended lesson with no CLAUDE.md home
|
|
101
|
+
// it prints `claude-md candidate for lesson <id>: <command>`
|
|
102
|
+
// (edits file under .doug/.state/learn/claude-md-candidates/,
|
|
103
|
+
// nothing under --dry-run) or `... dropped: <reason>`; --json
|
|
104
|
+
// carries claudeMd and claudeMdDropped. Needs Node >=22.16 (node:sqlite).
|
|
101
105
|
// memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "<R<n> or <card-id>/R<n>>=<class>[: <reason>]"]...
|
|
102
106
|
// [--harmful <id>]... [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
103
107
|
// (card memory-every-session) the same pass for a hand landing:
|
|
@@ -776,6 +780,14 @@ try {
|
|
|
776
780
|
for (const r of result.rejected) {
|
|
777
781
|
process.stdout.write(` rejected ${r.text ? JSON.stringify(r.text) : "(no text)"}: ${r.reason}\n`);
|
|
778
782
|
}
|
|
783
|
+
for (const c of result.claudeMd || []) {
|
|
784
|
+
process.stdout.write(
|
|
785
|
+
result.dryRun
|
|
786
|
+
? `claude-md candidate for lesson ${c.lessonId} (dry run, not written)\n`
|
|
787
|
+
: `claude-md candidate for lesson ${c.lessonId}: ${c.command}\n`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
for (const d of result.claudeMdDropped || []) process.stdout.write(`claude-md candidate for lesson ${d.lessonId} dropped: ${d.reason}\n`);
|
|
779
791
|
}
|
|
780
792
|
break;
|
|
781
793
|
}
|
|
@@ -265,7 +265,7 @@ async function attachCodeContext(plan, dir, provider) {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
const SETTABLE = ["install", "baseBranch", "integrationBranch", "integrationWorktree", "adversary.command", "adversary.fallback", "card", "fixAttempts", "swarm", "workerCheck"];
|
|
268
|
+
const SETTABLE = ["install", "baseBranch", "integrationBranch", "integrationWorktree", "adversary.command", "adversary.fallback", "card", "fixAttempts", "swarm", "workerCheck", "crew.testers"];
|
|
269
269
|
|
|
270
270
|
const [cmd, ...argsRaw] = process.argv.slice(2);
|
|
271
271
|
// --card=<id> is the same as --card <id>.
|
|
@@ -520,6 +520,10 @@ switch (cmd) {
|
|
|
520
520
|
// Card swarm-topology (candidate 3): a deterministic per-worker check before the merge, behind the swarm opt-in.
|
|
521
521
|
if (!/^(on|off|true|false)$/i.test(value.trim())) fail(`plan.mjs set workerCheck: the value must be on or off, got ${JSON.stringify(value)}`, 2);
|
|
522
522
|
next.workerCheck = /^(on|true)$/i.test(value.trim());
|
|
523
|
+
} else if (key === "crew.testers") {
|
|
524
|
+
// Card crew-tester-seat: how many verifier seats run in parallel per full-shape task.
|
|
525
|
+
if (!/^[1-9]\d*$/.test(value.trim())) fail(`plan.mjs set crew.testers: the value must be a positive integer, got ${JSON.stringify(value)}`, 2);
|
|
526
|
+
next.crew = { ...(plan.crew && typeof plan.crew === "object" && !Array.isArray(plan.crew) ? plan.crew : {}), testers: Number(value.trim()) };
|
|
523
527
|
} else if (key === "fixAttempts") {
|
|
524
528
|
if (!/^\d+$/.test(value)) fail(`plan.mjs set fixAttempts: the value must be a non-negative integer, got ${JSON.stringify(value)}`, 2);
|
|
525
529
|
next.fixAttempts = Number(value);
|
|
@@ -104,7 +104,7 @@ Each card still runs its track unchanged (research when needed, tester, coder, r
|
|
|
104
104
|
|
|
105
105
|
The queue approval stands for each flow-track card's plan approval: the lead puts plan.mjs show in the chat and runs plan.mjs approve itself. A card runs by hand unless its goal asks for a swarm. A hand card whose goal asks for a swarm follows doug-hand's 2b swarm branch, with the queue approval standing for its plan approval. Follow-ups are filed to Backlog as section 6 says, without a question. On every queen notification the lead measures the session's cost with cost.mjs --session <id> and checks every cap.
|
|
106
106
|
|
|
107
|
-
Parallel cards: up to `workers.max` cards (default 3) run at once, each under its own queen, a background run in the card's own worktree, and main only coordinates. Main picks with `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" pick` up to workers.max cards that are disjoint: for hand cards, goals that name no common path and components that differ; for flow cards, plans whose owned files do not overlap. Flow cards in `needsPlan` get one `doug-flow:planner` each, all in one message, on the plan row, each writing `.doug/plans/<id>.json` and validating it with `plan.mjs validate --card <id>`. For each planned card main runs `plan.mjs set card <id> --card <id>`, `plan.mjs set integrationBranch doug/<id> --card <id>`, and `plan.mjs set integrationWorktree .claude/worktrees/doug-integration-<id> --card <id>`, puts `plan.mjs show --card <id>` in the chat, and runs `plan.mjs approve --card <id>`, because the queue approval stands for it; then main picks again. A flow card gets section 1's move and a hand card gets doug-hand step 2's class, move, and condition open, before its launch. A flow queen is `Workflow({
|
|
107
|
+
Parallel cards: up to `workers.max` cards (default 3) run at once, each under its own queen, a background run in the card's own worktree, and main only coordinates. Main picks with `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" pick` up to workers.max cards that are disjoint: for hand cards, goals that name no common path and components that differ; for flow cards, plans whose owned files do not overlap. Flow cards in `needsPlan` get one `doug-flow:planner` each, all in one message, on the plan row, each writing `.doug/plans/<id>.json` and validating it with `plan.mjs validate --card <id>`. For each planned card main runs `plan.mjs set card <id> --card <id>`, `plan.mjs set integrationBranch doug/<id> --card <id>`, and `plan.mjs set integrationWorktree .claude/worktrees/doug-integration-<id> --card <id>`, puts `plan.mjs show --card <id>` in the chat, and runs `plan.mjs approve --card <id>`, because the queue approval stands for it; then main picks again. A flow card gets section 1's move and a hand card gets doug-hand step 2's class, move, and condition open, before its launch. A flow queen is `Workflow({ scriptPath: <the queen.js path>, args: <plan.mjs json --card <id>> })`, the queen.js being the per-card copy of `workflows/doug-implement.js` that `autopilot.mjs queen-args` writes to `.doug/.state/reports/<id>/queen.js`; a hand queen is `Workflow({ scriptPath: <the queen.js path>, args })`, the per-card copy of `workflows/card-queen.js` at the same path, with args `{ card, options, models, repoRoot, worktreePath }`, the worktreePath being `<repoRoot>/.claude/worktrees/card-<id>`. Before any queen launch, main runs `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" agent-types --kind <flow|hand>`, which prints the agent types that queen spawns, one per line, and compares each name with the agent types its own session lists; when one is missing it launches no queen and tells the user to run /reload-plugins or restart. To launch a queen, main runs `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" queen-args <id> --kind <flow|hand> --with <the other ids launched in the same message>`, which writes `.doug/.state/reports/<id>/queen-args.json` and the per-card `.doug/.state/reports/<id>/queen.js`, whose run name is `<short id> card-queen` or `<short id> flow-queen`, and prints both paths; then `Workflow({ scriptPath: <the queen.js path>, args })` with that file's JSON as args; then `autopilot.mjs launched <id> --run <wf_id> --kind <flow|hand> --worktree <abs> --args-file <path>`, the flow worktree being `<repoRoot>/<the plan's integrationWorktree>`, because that value is relative. Main starts all the Workflow calls in one message, and it never briefs, builds, or reviews a queen card. Each queen's report arrives in that run's task-notification `<result>`, not in the Workflow tool's return, and main pipes it through `autopilot.mjs report --run <wf_id> --file .doug/.state/briefs/<id>-result.json`, the queen's result saved there, which prints land, resume, or ask. Land means the landing steps below; resume relaunches the same `.doug/.state/reports/<id>/queen.js` with `resumeFromRunId` and the saved `queen-args.json`, then runs `autopilot.mjs resumed <id> --run <new wf_id>`, once, and a second resume is ask; ask stops that card and asks the user. Main lands one card at a time: a flow card with `plan.mjs done --card <id>` then `plan.mjs land --card <id>`, a hand card with `plan.mjs land-card <id> --branch card/<id> --worktree <abs>`. After each landing main runs `board.mjs move`, the run-report skill with `cost.mjs <wf_id> --report <per-card report path>`, which is `.doug/.state/reports/<id>/last-report.json` for a flow card and `.doug/.state/reports/<id>/card-report.json` for a hand card, and `autopilot.mjs lock-wait <id> --ms <n>`, where n is the `lockWaitMs` line that `plan.mjs land` or `land-card` printed, and the landing commit carries the card's plan `.doug/plans/<id>.json`. A resume never replans. When a queen's run needs the user, main asks while the other queens keep running. A cap reached launches no new queen and lets the in-flight ones finish. In manual or accept-edits mode every queen launch prompts, and the Workflow allow rule or "Yes, and don't ask again" runs n queens unattended (docs/research/parallel-cards-research.md section 4). A card tagged swarm (or `swarm: true`, or whose goal says to run it as a swarm) and a decision card run alone, as today, after the in-flight queens finish; `/doug-next <id>` by hand is unchanged.
|
|
108
108
|
|
|
109
109
|
The lead keeps the run's state file, `.doug/.state/autopilot.json` (uncommitted), current through `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs"`, which the live pane and the board's Autopilot panel (`doug board serve`) read:
|
|
110
110
|
|
|
@@ -21,7 +21,7 @@ The session model never researches itself; it asks the questions and writes the
|
|
|
21
21
|
|
|
22
22
|
## One researcher or several
|
|
23
23
|
|
|
24
|
-
One question, one researcher. Several independent questions get several researchers in the same message, in parallel, each with one distinct question and told not to answer the others'; their answers merge into one note. A request or card that names a crew of researchers (`crew: { "researchers": 2 }` in the plan's terms) gets that many, each with its own question. Do not give two researchers the same question: a disagreement between two guesses is not a check. Each question names the budget (at most 6 WebSearch plus WebFetch calls, research.maxFetches) and lists the raw URLs to try, in priority order.
|
|
24
|
+
One question, one researcher. Several independent questions get several researchers in the same message, in parallel, each with one distinct question and told not to answer the others'; their answers merge into one note. A request or card that names a crew of researchers (`crew: { "researchers": 2 }` in the plan's terms) gets that many, each with its own question. Do not give two researchers the same question: a disagreement between two guesses is not a check. Each question names the budget (at most 6 WebSearch plus WebFetch calls, research.maxFetches) and lists the raw URLs to try, in priority order. Each researcher brief asks exactly one question; a brief that reads an external system and then compares it with this repository asks two. A comparison with this repository's code is its own question for its own researcher, never appended to a question about an external system. Every researcher brief names where to read, the raw URLs in priority order or the directories and files of a downloaded tree, and its turn budget: at most 40 tool calls (the researcher agent's maxTurns), stopping with at least 5 to spare to return what it has. On 2026-10-13 two of the three memory-research-tencentdb researchers, each briefed to read an external codebase and then compare it with Doug, ran into the 30-turn cap and returned a half sentence with no findings.
|
|
25
25
|
|
|
26
26
|
## The note
|
|
27
27
|
|
|
@@ -30,6 +30,7 @@ const STOP_REASONS = [
|
|
|
30
30
|
"setup-failed",
|
|
31
31
|
"tester-red-claim-missing",
|
|
32
32
|
"reviewer-blocker-open",
|
|
33
|
+
"reviewer-major-open",
|
|
33
34
|
"gate-red-twice",
|
|
34
35
|
];
|
|
35
36
|
// The Models-table row each seat runs on. setup, gate and commit have no sourced row; cheap is a choice.
|
|
@@ -102,6 +103,22 @@ const BRIEF_RULES = {
|
|
|
102
103
|
text: "Commit without Co-Authored-By or Claude-Session trailers.",
|
|
103
104
|
seats: ["commit"],
|
|
104
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
|
+
},
|
|
105
122
|
"leave-plan-anchor-out": {
|
|
106
123
|
text: "Leave .doug/plan.json and .doug/anchor.md out of the commit unless the card is about them.",
|
|
107
124
|
seats: ["commit"],
|
|
@@ -532,6 +549,9 @@ function validate(state, hb) {
|
|
|
532
549
|
}
|
|
533
550
|
case "gate": {
|
|
534
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
|
+
}
|
|
535
555
|
if (!nonBlank(hb.gateLine)) errs.push("gateLine must be a non-empty string");
|
|
536
556
|
if (hb.lockWaitMs !== undefined && hb.lockWaitMs !== null && !Number.isInteger(hb.lockWaitMs)) {
|
|
537
557
|
errs.push("lockWaitMs must be an integer or null");
|
|
@@ -551,6 +571,9 @@ function validate(state, hb) {
|
|
|
551
571
|
|
|
552
572
|
// ---- next ----
|
|
553
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
|
+
|
|
554
577
|
function stopWith(s, reason, detail) {
|
|
555
578
|
const seat = s.pending;
|
|
556
579
|
const state = {
|
|
@@ -670,12 +693,21 @@ function next(state, handBack) {
|
|
|
670
693
|
mutations: cp(H.mutationResults),
|
|
671
694
|
};
|
|
672
695
|
const blockers = findings.filter((f) => f.severity === "blocker");
|
|
673
|
-
|
|
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 });
|
|
674
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
|
+
}
|
|
675
707
|
return stopWith(
|
|
676
708
|
{ ...s, acc },
|
|
677
|
-
"reviewer-
|
|
678
|
-
"
|
|
709
|
+
"reviewer-major-open",
|
|
710
|
+
"major with evidence still open after one fix pass: " + majors.map((f) => f.id).join(", "),
|
|
679
711
|
);
|
|
680
712
|
}
|
|
681
713
|
return advance(s, buildSeat(s), "blockers", {
|
|
@@ -801,7 +833,14 @@ let s = initialState(args.card, args.options)
|
|
|
801
833
|
let a = currentAction(s)
|
|
802
834
|
while (a.kind === 'seat') {
|
|
803
835
|
phase(PHASE_TITLES[a.seat])
|
|
804
|
-
|
|
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
|
+
}
|
|
805
844
|
;({ state: s, action: a } = next(s, hb))
|
|
806
845
|
}
|
|
807
846
|
return { ...a.report, history: s.history }
|
|
@@ -4,7 +4,7 @@ export const meta = {
|
|
|
4
4
|
whenToUse: 'After /doug-plan produced .doug/plan.json and the user approved it with /doug-approve. Pass the plan JSON as args.',
|
|
5
5
|
phases: [
|
|
6
6
|
{ title: 'Implement', detail: 'one implementer per task, fresh context, own worktree (with swarm on, a lead splits a full-shape task into worker briefs, workers implement them in parallel, and the lead merges); a task marked reuse checks out its recorded branch instead; a task whose dependency did not integrate is never launched; a block the checks can describe is fixed in place until the task budget, the fix-attempt ceiling, a pass that made no new commit, or two consecutive passes raising a new blocker stops it' },
|
|
7
|
-
{ title: 'Verify', detail: 'a separate verifier runs the task command in the worktree on the first pass; a fix pass runs one focused check over the fix delta instead, and the full verifier again only when the verifier is the stage that blocked' },
|
|
7
|
+
{ title: 'Verify', detail: 'a separate verifier runs the task command in the worktree on the first pass; a fix pass runs one focused check over the fix delta instead, and the full verifier again only when the verifier is the stage that blocked; a task whose crew names more testers gets extra verifiers in parallel' },
|
|
8
8
|
{ title: 'Review', detail: 'spec compliance, then quality, against the diff on the first pass; on a fix pass the focused check carries scope and regression risk unless the reviewer is the stage that blocked; a task whose crew names two reviewers gets a second one in parallel with a distinct brief' },
|
|
9
9
|
{ title: 'Adversary', detail: 'codex-review (a different model, read-only) explores freely on the first pass and confirms the open findings on later passes, under one material blocker gate; when it cannot run, a Claude adversary on plan.adversary.fallback stands in; blockers stop integration' },
|
|
10
10
|
{ title: 'Integrate', detail: 'merge the level into the integration branch and run plan.verify' },
|
|
@@ -64,6 +64,9 @@ const ENVIRONMENT_MARKER = 'ENVIRONMENT ONLY'
|
|
|
64
64
|
// The one rule about commit messages, in every prompt that commits or checks commits: this repository records
|
|
65
65
|
// attribution nowhere in git, and plan.mjs land refuses a branch that carries either trailer.
|
|
66
66
|
const NO_TRAILERS = 'Commit messages carry no attribution trailers: no Co-Authored-By line and no Claude-Session line, whatever your defaults say. A commit with either is a finding and the plan cannot land.'
|
|
67
|
+
// The one rule about how a commit runs, in every implement prompt that commits: the pre-commit hook runs the full
|
|
68
|
+
// unit suite, which outlives Bash's default timeout, and a background commit dies with the agent that started it.
|
|
69
|
+
const FOREGROUND_COMMIT = 'Run git commit in the foreground with the Bash tool timeout set to 600000 ms (the pre-commit hook runs the full suite): never run_in_background, a Monitor, or a sleep loop. Take the sha only from git rev-parse HEAD after git commit has returned.'
|
|
67
70
|
const LESSONS_NOT_SPEC = 'These are context from earlier runs, not part of the spec or the acceptance criteria; never report a finding, a spec contradiction, or a blocker against a lesson.'
|
|
68
71
|
// The one rule about a partial result, in every prompt a worker (or a resumed worker) can end with one from
|
|
69
72
|
// (card worker-context-handoff): quotes the harness's context notice (a PostToolUse hook in plugins/doug-gates) so
|
|
@@ -831,7 +834,7 @@ function ledgerText(ledger, kind) {
|
|
|
831
834
|
function implementPrompt(task, baseBranch, opts) {
|
|
832
835
|
const resume = !!(opts && opts.resume)
|
|
833
836
|
const branchCheck = resume
|
|
834
|
-
? ` Before that, if git rev-parse --verify --quiet refs/heads/doug/task-${task.id} succeeds, the branch doug/task-${task.id} was created by the previous agent for this task in this same run, which stopped without returning a result; it is yours, not stale. Find that agent's worktree with git worktree list --porcelain (the one with branch refs/heads/doug/task-${task.id}); if none is listed, go straight to the checkout. If it has uncommitted changes to owned files, commit them there first with a message starting "${task.id}: ". ${NO_TRAILERS} Then remove that worktree with git worktree remove --force <path>, then git checkout doug/task-${task.id} in your own worktree and continue the task from that branch's state (do not recreate the branch or reset it). Never return blocked for the branch existing.`
|
|
837
|
+
? ` Before that, if git rev-parse --verify --quiet refs/heads/doug/task-${task.id} succeeds, the branch doug/task-${task.id} was created by the previous agent for this task in this same run, which stopped without returning a result; it is yours, not stale. Find that agent's worktree with git worktree list --porcelain (the one with branch refs/heads/doug/task-${task.id}); if none is listed, go straight to the checkout. If it has uncommitted changes to owned files, commit them there first with a message starting "${task.id}: ". ${NO_TRAILERS} ${FOREGROUND_COMMIT} Then remove that worktree with git worktree remove --force <path>, then git checkout doug/task-${task.id} in your own worktree and continue the task from that branch's state (do not recreate the branch or reset it). Never return blocked for the branch existing.`
|
|
835
838
|
: ` Before that, if git rev-parse --verify --quiet refs/heads/doug/task-${task.id} succeeds, the branch is a leftover of an earlier run: do not check it out or build on it; change nothing and return blocked=true with blockedReason "stale branch doug/task-${task.id} already exists; run plan.mjs replan, which renames it".`
|
|
836
839
|
return [
|
|
837
840
|
`You are implementing exactly one task from an approved plan. Work only in your own git worktree.`,
|
|
@@ -854,7 +857,7 @@ function implementPrompt(task, baseBranch, opts) {
|
|
|
854
857
|
` A fresh worktree has no installed dependencies: if the verification command needs them, run the project's install command first${plan.install ? ` (${plan.install})` : ''}. Installing is not a file change you own; do not commit lockfile changes it makes.`,
|
|
855
858
|
`2. Touch no file outside your owned list. If the task cannot be done without another file, stop and report blocked=true with the reason.`,
|
|
856
859
|
`3. Run the verification command before finishing and report each command with its real exit status. Never report a command you did not run.`,
|
|
857
|
-
`4. Commit your work on the branch with a message starting "${task.id}: ". ${NO_TRAILERS}`,
|
|
860
|
+
`4. Commit your work on the branch with a message starting "${task.id}: ". ${NO_TRAILERS} ${FOREGROUND_COMMIT}`,
|
|
858
861
|
`5. Return the structured result. filesTouched must come from git, not memory, and commit must be the output of git rev-parse HEAD in your worktree after that commit.`,
|
|
859
862
|
`6. ${PARTIAL_RULE}`,
|
|
860
863
|
].join('\n')
|
|
@@ -885,7 +888,7 @@ function reusePrompt(task, baseBranch) {
|
|
|
885
888
|
function leadPrompt(task, baseBranch, opts) {
|
|
886
889
|
const resume = !!(opts && opts.resume)
|
|
887
890
|
const branchCheck = resume
|
|
888
|
-
? ` Before that, if git rev-parse --verify --quiet refs/heads/doug/task-${task.id} succeeds, the branch doug/task-${task.id} was created by the previous agent for this task in this same run, which stopped without returning a result; it is yours, not stale. Find that agent's worktree with git worktree list --porcelain (the one with branch refs/heads/doug/task-${task.id}); if none is listed, go straight to the checkout. If it has uncommitted changes to owned files, commit them there first with a message starting "${task.id}: ". ${NO_TRAILERS} Then remove that worktree with git worktree remove --force <path>, then git checkout doug/task-${task.id} in your own worktree and continue the task from that branch's state (do not recreate the branch or reset it). Never return blocked for the branch existing.`
|
|
891
|
+
? ` Before that, if git rev-parse --verify --quiet refs/heads/doug/task-${task.id} succeeds, the branch doug/task-${task.id} was created by the previous agent for this task in this same run, which stopped without returning a result; it is yours, not stale. Find that agent's worktree with git worktree list --porcelain (the one with branch refs/heads/doug/task-${task.id}); if none is listed, go straight to the checkout. If it has uncommitted changes to owned files, commit them there first with a message starting "${task.id}: ". ${NO_TRAILERS} ${FOREGROUND_COMMIT} Then remove that worktree with git worktree remove --force <path>, then git checkout doug/task-${task.id} in your own worktree and continue the task from that branch's state (do not recreate the branch or reset it). Never return blocked for the branch existing.`
|
|
889
892
|
: ` Before that, if git rev-parse --verify --quiet refs/heads/doug/task-${task.id} succeeds, the branch is a leftover of an earlier run: do not check it out or build on it; change nothing and return blocked=true with blockedReason "stale branch doug/task-${task.id} already exists; run plan.mjs replan, which renames it".`
|
|
890
893
|
return [
|
|
891
894
|
`You are the lead of a swarm for exactly one task of an approved plan. You split the task into worker briefs; you do not implement anything and you edit no file. Work only in your own git worktree.`,
|
|
@@ -944,7 +947,7 @@ function workerPrompt(task, brief, n, lead) {
|
|
|
944
947
|
` A fresh worktree has no installed dependencies: if the verification command needs them, run the project's install command first${plan.install ? ` (${plan.install})` : ''}. Installing is not a file change you own; do not commit lockfile changes it makes.`,
|
|
945
948
|
`2. Touch no file outside your owned list. If the brief cannot be done without another file, stop and report blocked=true with a blockedReason that names exactly what you need - the file, the symbol or interface, or the decision - because the lead may re-brief you once on it; a vague reason wastes that chance.`,
|
|
946
949
|
`3. Run the verification command before finishing and report each command with its real exit status. Never report a command you did not run.`,
|
|
947
|
-
`4. Commit your work on the branch with a message starting "${task.id}: w${n} ". ${NO_TRAILERS}`,
|
|
950
|
+
`4. Commit your work on the branch with a message starting "${task.id}: w${n} ". ${NO_TRAILERS} ${FOREGROUND_COMMIT}`,
|
|
948
951
|
`5. Return the structured result. filesTouched must come from git diff --name-only ${lead.branch}...HEAD, and commit must be the output of git rev-parse HEAD in your worktree after that commit.`,
|
|
949
952
|
`6. ${PARTIAL_RULE}`,
|
|
950
953
|
].join('\n')
|
|
@@ -1048,7 +1051,7 @@ function fixPrompt(task, r, pass, baseBranch, ledger, brief) {
|
|
|
1048
1051
|
lines.push(`1. Touch no file outside your owned list. If a finding can only be met by changing another file, stop and return blocked=true with the reason.`)
|
|
1049
1052
|
lines.push(`2. Do not delete, skip, or weaken any test to make a check pass; make the code satisfy it. If that is impossible within the spec, return blocked=true and say why.`)
|
|
1050
1053
|
lines.push(`3. Run the verification command before finishing; it must pass. Report every command with its real exit status and never one you did not run.`)
|
|
1051
|
-
lines.push(`4. Commit on ${r.impl.branch} with a message starting "${task.id}: fix ". Do not amend or rewrite earlier commits. A pass that ends with no new commit stops the task. ${NO_TRAILERS}`)
|
|
1054
|
+
lines.push(`4. Commit on ${r.impl.branch} with a message starting "${task.id}: fix ". Do not amend or rewrite earlier commits. A pass that ends with no new commit stops the task. ${NO_TRAILERS} ${FOREGROUND_COMMIT}`)
|
|
1052
1055
|
lines.push(`5. Return the structured result with taskId="${task.id}", branch="${r.impl.branch}", worktreePath${r.worktreeGone ? ' = the absolute path of your worktree (git rev-parse --show-toplevel)' : `="${r.impl.worktreePath}"`}, filesTouched from git diff --name-only ${baseBranch}...HEAD, commit from git rev-parse HEAD after your commit, blocked=false.`)
|
|
1053
1056
|
lines.push(`6. ${PARTIAL_RULE}`)
|
|
1054
1057
|
return lines.join('\n')
|
|
@@ -1081,7 +1084,7 @@ function resumePrompt(task, prev, handoff, baseBranch, brief) {
|
|
|
1081
1084
|
`Rules:`,
|
|
1082
1085
|
`1. Touch no file outside your owned list. If the work cannot be done without another file, stop and report blocked=true with the reason.`,
|
|
1083
1086
|
`2. Run the verification command before finishing and report each command with its real exit status. Never report a command you did not run.`,
|
|
1084
|
-
`3. Commit your work on ${prev.branch} with a message starting "${task.id}: resume ". ${NO_TRAILERS}`,
|
|
1087
|
+
`3. Commit your work on ${prev.branch} with a message starting "${task.id}: resume ". ${NO_TRAILERS} ${FOREGROUND_COMMIT}`,
|
|
1085
1088
|
`4. ${PARTIAL_RULE}`,
|
|
1086
1089
|
`5. Return the structured result. filesTouched must come from git diff --name-only ${baseBranch}...HEAD, and commit must be the output of git rev-parse HEAD in your worktree after that commit.`,
|
|
1087
1090
|
].join('\n')
|
|
@@ -1125,9 +1128,10 @@ function supervisePrompt(task, r, attempts, ledger, signals) {
|
|
|
1125
1128
|
].join('\n')
|
|
1126
1129
|
}
|
|
1127
1130
|
|
|
1128
|
-
function verifyPrompt(task, impl, baseBranch) {
|
|
1131
|
+
function verifyPrompt(task, impl, baseBranch, seat = 1) {
|
|
1129
1132
|
return [
|
|
1130
1133
|
`You are a verifier. You did not write this code. Your job is to run it, not to read and approve it.`,
|
|
1134
|
+
...(seat > 1 ? [`You are crew seat ${seat} of ${crewOf(task).testers} on this task: another verifier runs the same checks at the same time. Run the verification and acceptance commands yourself, and beyond them probe the behavior the spec implies that those commands may not cover: edge cases (empty, missing, boundary, and repeated input) and error paths (invalid input, a failing dependency). A defect you find is a finding even when every command passes.`] : []),
|
|
1131
1135
|
``,
|
|
1132
1136
|
`Task ${task.id}: ${task.title}`,
|
|
1133
1137
|
`Spec:`,
|
|
@@ -1990,13 +1994,41 @@ async function runAdversary(task, impl, pass, baseBranch, ledger, launch, stages
|
|
|
1990
1994
|
}
|
|
1991
1995
|
|
|
1992
1996
|
// ---- Crew (card crew-sizing) ---------------------------------------------------
|
|
1993
|
-
// A task (or the plan) declares how many reviewers and
|
|
1997
|
+
// A task (or the plan) declares how many reviewers, adversaries and testers read it; default one each. More coders is a
|
|
1994
1998
|
// split into more tasks, never a shared worktree, so the crew has no coders row. Researchers run before planning
|
|
1995
1999
|
// (the research skill) and are not launched here.
|
|
1996
2000
|
function crewOf(task) {
|
|
1997
2001
|
const src = { ...((plan.crew && typeof plan.crew === 'object') ? plan.crew : {}), ...((task && task.crew && typeof task.crew === 'object') ? task.crew : {}) }
|
|
1998
2002
|
const n = v => (Number.isInteger(v) && v > 0 ? v : 1)
|
|
1999
|
-
return { reviewers: n(src.reviewers), adversaries: n(src.adversaries) }
|
|
2003
|
+
return { reviewers: n(src.reviewers), adversaries: n(src.adversaries), testers: n(src.testers) }
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// One verification out of several tester seats (card crew-tester-seat): passed only when every seat that returned
|
|
2007
|
+
// passed, findings and commands concatenated so the ledger's fingerprint collapses a duplicate, and an acceptance
|
|
2008
|
+
// criterion (matched by text) ok only when every seat reporting it ok. A seat that returned nothing is dropped; when
|
|
2009
|
+
// no seat returned anything the stage returned nothing; one seat comes back unchanged.
|
|
2010
|
+
function mergeVerifies(seats) {
|
|
2011
|
+
const got = seats.filter(Boolean)
|
|
2012
|
+
if (!got.length) return null
|
|
2013
|
+
if (got.length === 1 && seats.length === 1) return got[0]
|
|
2014
|
+
const acceptance = []
|
|
2015
|
+
for (const v of got) {
|
|
2016
|
+
for (const a of (v.acceptance || [])) {
|
|
2017
|
+
const at = acceptance.findIndex(x => x.text === a.text)
|
|
2018
|
+
if (at < 0) acceptance.push({ ...a })
|
|
2019
|
+
else if (acceptance[at].ok && !a.ok) acceptance[at] = { ...a }
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return {
|
|
2023
|
+
taskId: got[0].taskId,
|
|
2024
|
+
passed: got.every(v => v.passed),
|
|
2025
|
+
commandsRun: got.flatMap(v => v.commandsRun || []),
|
|
2026
|
+
findings: got.flatMap(v => v.findings || []),
|
|
2027
|
+
findingFiles: got.flatMap(v => v.findingFiles || []),
|
|
2028
|
+
acceptance,
|
|
2029
|
+
head: got[0].head,
|
|
2030
|
+
seats: got.length,
|
|
2031
|
+
}
|
|
2000
2032
|
}
|
|
2001
2033
|
|
|
2002
2034
|
// One review out of several: every seat must approve, and every seat's issues are kept, so a finding one reviewer
|
|
@@ -2159,19 +2191,21 @@ async function runChecks(task, impl, pass, baseBranch, ctx) {
|
|
|
2159
2191
|
// The verifier, the reviewer, and the adversary are read-only and none reads another's result, so a first pass
|
|
2160
2192
|
// launches them together; a null from any (skipped, or dead after retries) is that stage returning nothing.
|
|
2161
2193
|
stages.push('verify')
|
|
2162
|
-
stages.push('review')
|
|
2163
2194
|
const crew = crewOf(task)
|
|
2195
|
+
for (let seat = 2; seat <= crew.testers; seat++) stages.push(`verify-${seat}`)
|
|
2196
|
+
stages.push('review')
|
|
2164
2197
|
for (let seat = 2; seat <= crew.reviewers; seat++) stages.push(`review-${seat}`)
|
|
2165
2198
|
const withAdversary = adversaryRequired(task)
|
|
2166
2199
|
const advStages = []
|
|
2167
2200
|
const pair = await parallel([
|
|
2168
2201
|
() => launch(verifyPrompt(task, impl, baseBranch), agentOpts('verify', task, { label: `verify:${task.id}`, phase: 'Verify', agentType: 'doug-flow:verifier', schema: VERIFY_SCHEMA })),
|
|
2202
|
+
...Array.from({ length: crew.testers - 1 }, (_, i) => () => launch(verifyPrompt(task, impl, baseBranch, i + 2), agentOpts('verify', task, { label: `verify-${i + 2}:${task.id}`, phase: 'Verify', agentType: 'doug-flow:verifier', schema: VERIFY_SCHEMA }))),
|
|
2169
2203
|
() => launch(reviewPrompt(task, impl, baseBranch), agentOpts('review', task, { label: `review:${task.id}`, phase: 'Review', agentType: 'doug-flow:reviewer', schema: REVIEW_SCHEMA })),
|
|
2170
2204
|
...Array.from({ length: crew.reviewers - 1 }, (_, i) => () => launch(reviewPrompt(task, impl, baseBranch, i + 2), agentOpts('review', task, { label: `review-${i + 2}:${task.id}`, phase: 'Review', agentType: 'doug-flow:reviewer', schema: REVIEW_SCHEMA }))),
|
|
2171
2205
|
...(withAdversary ? [() => runAdversaryCrew(task, impl, pass, baseBranch, ledger, launch, advStages)] : []),
|
|
2172
2206
|
])
|
|
2173
|
-
ver =
|
|
2174
|
-
rev = pair ? mergeReviews(pair.slice(
|
|
2207
|
+
ver = pair ? mergeVerifies(pair.slice(0, crew.testers)) : null
|
|
2208
|
+
rev = pair ? mergeReviews(pair.slice(crew.testers, crew.testers + crew.reviewers)) : null
|
|
2175
2209
|
stageResults.verify = ver
|
|
2176
2210
|
stageResults.review = rev
|
|
2177
2211
|
// The adversary no longer waits for verify and review (card flow-adversary-parallel, 2026-10-07): waiting
|
|
@@ -2179,7 +2213,7 @@ async function runChecks(task, impl, pass, baseBranch, ctx) {
|
|
|
2179
2213
|
// integration still needs all three verdicts (isReady), and the ledger reads each stage's findings.
|
|
2180
2214
|
if (withAdversary) {
|
|
2181
2215
|
stages.push(...advStages)
|
|
2182
|
-
adv = (pair && pair[
|
|
2216
|
+
adv = (pair && pair[crew.testers + crew.reviewers]) || null
|
|
2183
2217
|
stageResults.adversary = adv
|
|
2184
2218
|
}
|
|
2185
2219
|
blockingStage = !(ver && ver.passed) ? 'verify' : !(rev && rev.approve) ? 'review' : adversaryBlocked(adv) ? 'adversary' : null
|
|
@@ -2187,7 +2221,13 @@ async function runChecks(task, impl, pass, baseBranch, ctx) {
|
|
|
2187
2221
|
const first = c.blocking || 'verify'
|
|
2188
2222
|
if (first === 'verify') {
|
|
2189
2223
|
stages.push('verify')
|
|
2190
|
-
|
|
2224
|
+
const testers = crewOf(task).testers
|
|
2225
|
+
for (let seat = 2; seat <= testers; seat++) stages.push(`verify-${seat}`)
|
|
2226
|
+
const verifyOpts = seat => agentOpts('verify', task, { label: `verify${seat > 1 ? `-${seat}` : ''}:${task.id}${suffix}`, phase: 'Verify', agentType: 'doug-flow:verifier', schema: VERIFY_SCHEMA })
|
|
2227
|
+
if (testers > 1) {
|
|
2228
|
+
const seats = await parallel(Array.from({ length: testers }, (_, i) => () => launch(verifyPrompt(task, impl, baseBranch, i + 1), verifyOpts(i + 1))))
|
|
2229
|
+
ver = mergeVerifies(seats || [])
|
|
2230
|
+
} else ver = await launch(verifyPrompt(task, impl, baseBranch), verifyOpts(1))
|
|
2191
2231
|
stageResults.verify = ver
|
|
2192
2232
|
if (!(ver && ver.passed)) blockingStage = 'verify'
|
|
2193
2233
|
} else if (first === 'review') {
|
|
@@ -2422,7 +2462,7 @@ async function runTask(task, baseBranch, resume) {
|
|
|
2422
2462
|
const fellBack = (r.stages || []).includes('adversary-fallback')
|
|
2423
2463
|
const crew = crewOf(task)
|
|
2424
2464
|
const advAgents = adversaryRequired(task) ? (fellBack ? 1 : 0) + crew.adversaries : 0
|
|
2425
|
-
const firstStage = blocking === 'adversary' ? advAgents : blocking === 'review' ? crew.reviewers : 1
|
|
2465
|
+
const firstStage = blocking === 'adversary' ? advAgents : blocking === 'review' ? crew.reviewers : blocking === 'verify' ? crew.testers : 1
|
|
2426
2466
|
const nextStages = firstStage + (blocking === 'check' ? 0 : 1) + (blocking === 'adversary' ? 0 : advAgents)
|
|
2427
2467
|
const spent = measure()
|
|
2428
2468
|
const passesDone = attempts.length
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doug-gates",
|
|
3
3
|
"displayName": "Doug Gates",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougharness/gates",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Deterministic Claude Code hooks: protected paths, bash guard, secret scan on edits and commands, format on edit, edit-loop detection, stop gate, re-anchor after compaction.",
|
|
6
6
|
"type": "module",
|