@dougharness/cli 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +8 -0
- package/dist/bin.js.map +1 -1
- package/dist/board-serve.js +9 -3
- package/dist/board-serve.js.map +1 -1
- package/dist/board.js +10 -2
- package/dist/board.js.map +1 -1
- package/dist/detect/repo.js +14 -1
- package/dist/detect/repo.js.map +1 -1
- package/dist/detect/types.d.ts +2 -0
- package/dist/doctor.d.ts +46 -0
- package/dist/doctor.js +491 -0
- package/dist/doctor.js.map +1 -0
- package/dist/generate/agents.d.ts +7 -0
- package/dist/generate/agents.js +62 -12
- package/dist/generate/agents.js.map +1 -1
- package/dist/generate/claude-md.js +4 -0
- package/dist/generate/claude-md.js.map +1 -1
- package/dist/generate/config.d.ts +8 -0
- package/dist/generate/config.js +4 -1
- package/dist/generate/config.js.map +1 -1
- package/dist/generate/proposal.js +15 -6
- package/dist/generate/proposal.js.map +1 -1
- package/dist/generate/settings.d.ts +2 -0
- package/dist/generate/settings.js +17 -2
- package/dist/generate/settings.js.map +1 -1
- package/dist/init.d.ts +14 -0
- package/dist/init.js +94 -8
- package/dist/init.js.map +1 -1
- package/dist/memory-setup.d.ts +28 -0
- package/dist/memory-setup.js +323 -0
- package/dist/memory-setup.js.map +1 -0
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +16 -4
- package/node_modules/@dougharness/flow/agents/planner.md +2 -2
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -2
- package/node_modules/@dougharness/flow/hooks/hooks.json +16 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +154 -12
- package/node_modules/@dougharness/flow/lib/code-index.mjs +18 -3
- package/node_modules/@dougharness/flow/lib/context.mjs +57 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +129 -4
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +185 -23
- package/node_modules/@dougharness/flow/lib/land.mjs +69 -10
- package/node_modules/@dougharness/flow/lib/learn.mjs +4 -4
- package/node_modules/@dougharness/flow/lib/memory.mjs +326 -35
- package/node_modules/@dougharness/flow/lib/plan.mjs +9 -0
- package/node_modules/@dougharness/flow/package.json +2 -1
- package/node_modules/@dougharness/flow/scripts/board.mjs +60 -20
- package/node_modules/@dougharness/flow/scripts/context.mjs +48 -0
- package/node_modules/@dougharness/flow/scripts/cost.mjs +15 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +190 -34
- package/node_modules/@dougharness/flow/scripts/plan.mjs +32 -64
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +132 -0
- package/node_modules/@dougharness/flow/skills/{core-next → doug-hand}/SKILL.md +31 -18
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +44 -10
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +5 -5
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +4 -2
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +8 -9
- package/node_modules/@dougharness/flow/skills/swarm-launch/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +119 -72
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/hooks/hooks.json +11 -0
- package/node_modules/@dougharness/gates/lib/baseline.mjs +166 -1
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +58 -1
- package/node_modules/@dougharness/gates/lib/checkpoint.mjs +1 -1
- package/node_modules/@dougharness/gates/lib/config.mjs +20 -0
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +95 -0
- package/node_modules/@dougharness/gates/lib/evidence.mjs +2 -0
- package/node_modules/@dougharness/gates/lib/run.mjs +4 -0
- package/node_modules/@dougharness/gates/lib/test-totals.mjs +40 -7
- package/node_modules/@dougharness/gates/lib/trace.mjs +24 -7
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +225 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +133 -5
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +551 -113
- package/node_modules/@dougharness/gates/scripts/trace.mjs +5 -1
- package/node_modules/@dougharness/gates/scripts/turn-start.mjs +17 -0
- package/node_modules/@dougharness/gates/scripts/verify.mjs +146 -0
- package/package.json +3 -3
- package/templates/board-app.js +2 -2
- package/node_modules/@dougharness/flow/lib/rehearse.mjs +0 -825
- package/node_modules/@dougharness/flow/scripts/rehearse.mjs +0 -129
- package/node_modules/@dougharness/flow/skills/harness-fix/SKILL.md +0 -85
|
@@ -15,7 +15,7 @@ The swarm pays on a task with independent deliverables that share no new symbol;
|
|
|
15
15
|
|
|
16
16
|
Start from a clean working tree on the base branch (`git status --porcelain` empty apart from `.doug/` and `.claude/worktrees/`).
|
|
17
17
|
|
|
18
|
-
- A card id: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" card <id>`. A card with `"track": "hand"` is
|
|
18
|
+
- A card id: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" card <id>`. A card with `"track": "hand"` is a by-hand card, not a swarm; say so, name `/doug-next <id>` as the command that takes it, and end. Move the card to In flow exactly as `/doug-next` step 1 does (`board.mjs move <id> flow`), leaving the move uncommitted working state that the served page (`doug board serve`) shows live.
|
|
19
19
|
- A goal: no board record changes; the plan carries no `card`.
|
|
20
20
|
|
|
21
21
|
Put the card's title and goal (or the goal text) in the chat verbatim, in a fenced code block. If it depends on a fact outside the repository, invoke the `research` skill first.
|
|
@@ -40,13 +40,13 @@ Invoke the `swarm-launch` skill: it checks the plan is approved with swarm on an
|
|
|
40
40
|
|
|
41
41
|
## 6. Follow-up cards
|
|
42
42
|
|
|
43
|
-
`/doug-next` step 6 verbatim: gather the run's follow-up candidates
|
|
43
|
+
`/doug-next` step 6 verbatim: gather the run's follow-up candidates and file each straight to Backlog with `doug board add`, placed by priority, then one commit.
|
|
44
44
|
|
|
45
|
-
Before the continue gate, gather the follow-up candidates this landing produced: the report's review issues, the adversary blocks classified marginal or false whose reason names a hole, the integrate stage's conflicts and failed verify tails, and the landing summary. Suggestions come only from what the run actually reported, never invented; each carries the one-line reason and cites the pass that found it. A reviewer's out-of-scope finding is
|
|
45
|
+
Before the continue gate, gather the follow-up candidates this landing produced: the report's review issues, the adversary blocks classified marginal or false whose reason names a hole, the integrate stage's conflicts and failed verify tails, and the landing summary. Suggestions come only from what the run actually reported, never invented; each carries the one-line reason and cites the pass that found it. A reviewer's out-of-scope finding is filed only when it is realistic drift (a form a model or a person would plausibly write) or when one card would close the whole class; otherwise it goes in the landing note and is not filed. The card's goal text carries both: the goal names the pass that found it (the reviewer's finding, the implementer's blockedReason or handoff, the verifier's findings, the adversary block id) and the landed commit.
|
|
46
46
|
|
|
47
|
-
A landing with no candidates
|
|
47
|
+
Follow-ups are filed without a question. A landing with no candidates files nothing. Each realistic candidate goes straight to Backlog with `doug board add` (never a script that edits the record): `doug board add <id> --title "<title>" --goal "<goal>" --component <c> --size <S|M|L> --class <tests-only|prose|gate-script|code|docs|eval|decision> --track <hand|flow> --deps <landed-id> --column backlog`. Then the lead places it in Backlog by priority with `doug board reorder`: `--top` for a defect that blocks a commit, a gate, or a user; `--after <id>` of the last such card for drift a user would hit; `--bottom` otherwise. Only the user promotes a card to Ready; the lead never files a follow-up to Ready or Decide.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
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.
|
|
50
50
|
|
|
51
51
|
A goal without a card (section 1's second bullet) skips this step, the way section 5 says such a run skips the board steps.
|
|
52
52
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: research
|
|
3
|
-
description: The research step before a plan, for a card whose goal depends on facts outside the repository (a Claude Code hook contract, a CLI's output format, a third-party API). One researcher, or several in parallel with distinct questions, write one cited note the planner and the builder work from; uncertainty is stated, never guessed. Invoked by /
|
|
3
|
+
description: The research step before a plan, for a card whose goal depends on facts outside the repository (a Claude Code hook contract, a CLI's output format, a third-party API). One researcher, or several in parallel with distinct questions, write one cited note the planner and the builder work from; uncertainty is stated, never guessed. Invoked by /doug-next, doug-hand, and /doug-plan before planning, or on its own.
|
|
4
4
|
allowed-tools: Agent, Read, Write, Bash(mkdir *), Bash(cat *), Bash(node *plan.mjs *)
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -31,11 +31,13 @@ Write the findings to `.doug/.state/research/<card-id>.md` (session state, ignor
|
|
|
31
31
|
- the sources: a URL, a file path with a line, or a command and its output, one per fact;
|
|
32
32
|
- what stayed unverified, marked `unverified`, with what was tried. Never guess a fact into the note; an unverified line is what keeps the planner from building on it.
|
|
33
33
|
|
|
34
|
+
A fact the design depends on is marked verified only when it is quoted verbatim from the raw source with its line number, fetched and searched (for example `curl -sL <url>` then `grep -n`) by the plugin's researcher agent or by a claude-code-guide asked for exact quotes; a guide answer that paraphrases, or cannot give the line, stays `unverified` in the note. The confirming fetch counts against the same research.maxFetches budget. On 2026-10-01 a guide answer quoted a sentence the raw sub-agents page does not contain, and only the grep caught it.
|
|
35
|
+
|
|
34
36
|
The note is written under the card's id, not the question's, because the id is also the promotion key: a note for a card that lands is promoted to `docs/research/<card-id>.md`, `board.mjs record` copying it there when it records the landing so a commit that cites the note has something to read afterward. A note for a card that never lands (dropped, replanned under another id) stays in state and is eventually swept with the rest of `.doug/.state/`.
|
|
35
37
|
|
|
36
38
|
Then hand it on:
|
|
37
39
|
|
|
38
40
|
- Flow track (`/doug-plan`, `/doug-next`): give the planner the note's path in its prompt. The planner writes the facts it relied on into the plan's goal with their sources and marks any unverified one.
|
|
39
|
-
- Hand track (
|
|
41
|
+
- Hand track (doug-hand): the builder reads the note before the test, cites the sources in the commit message where a fact decided the design, and names the note in the landing's `--note`.
|
|
40
42
|
|
|
41
43
|
The step spends agents only when invoked and publishes nothing.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: run-report
|
|
3
|
-
description: End a card's run with one command: append the docs/live-runs.md entry for a workflow report (or a hand-track landing), and print the three-line summary that goes in the chat. Called by /doug-next after land and by
|
|
3
|
+
description: End a card's run with one command: append the docs/live-runs.md entry for a workflow report (or a hand-track landing), and print the three-line summary that goes in the chat. Called by /doug-next after land and by doug-hand after the commit; usable on any saved report.
|
|
4
4
|
allowed-tools: Agent, Read, Bash
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -14,11 +14,11 @@ With the card id, the report (`.doug/.state/last-report.json` unless another pat
|
|
|
14
14
|
|
|
15
15
|
1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/cost.mjs" <run-id>`: the run's real cost, summed from the local Claude Code transcripts of every agent in the run and priced from the dated table in the script (no network). Its last lines are the run total and any agent it could not price. The number after `Run total:` is the cost; when the run cannot be found or every agent is unpriced, there is no cost. The workflow itself does not report cost; never estimate one. The Codex line after it, when present, is the Codex adversary's cost: pass its USD as --codex-cost <usd> to record and summary; it is never added to --cost.
|
|
16
16
|
2. Classify every adversary block in the report. A block is a finding the adversary raised as a blocker (its ledger id, `F1`; for a report from before the ledger, `pass-<n>`); `board.mjs record` refuses an id the report does not have and lists the ones it has. This is judgment work and runs on the `review` row: read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.review.model`, `roles.review.effort`), and spawn `Agent({ subagent_type: "doug-flow:reviewer", model: "<review row model>", prompt: "There is no worktree and no diff for this job: read the report at <report path> (repository root, read-only) and classify every adversary block in it as real (a defect a user would hit), marginal (true to the spec, no user impact), or false (wrong), each with a one-line reason drawn from the fix passes and the checks in the report, not from the adversary's summary. Return exactly one line per block, nothing else: F1=<real|marginal|false>: <one-line reason> (pass-<n>=... for a pre-ledger report). Edit nothing." })`. The lead passes each returned line into `--adversary` verbatim and never reclassifies.
|
|
17
|
-
3. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json> --wall "<m> min" --commit <sha> --cost <usd> [--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`.
|
|
17
|
+
3. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json> --run <run-id> --wall "<m> min" --commit <sha> --cost <usd> [--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`.
|
|
18
18
|
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.
|
|
19
19
|
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.
|
|
20
20
|
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/harmful counters to any lesson a task's `memoryUsed` names, then runs a cheap-row `claude -p --json-schema` pass that proposes and appends at most three lessons from the report, filtered in code. Its one-line summary (with the measured cost) goes in the chat. An exit 1 naming the LLM pass is said in the chat and the landing goes on regardless — reflect is a log, not a gate, the same as record.
|
|
21
|
-
7. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --wall "<m> min" --commit <sha> [--codex-cost <usd>]` (same `--cost` rule) and put its three lines in the chat verbatim, in a fenced code block: the outcome, the tasks, and what stopped or was not met.
|
|
21
|
+
7. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --run <run-id> --wall "<m> min" --commit <sha> [--codex-cost <usd>]` (same `--cost` rule) and put its three lines in the chat verbatim, in a fenced code block: the outcome, the tasks, and what stopped or was not met.
|
|
22
22
|
|
|
23
23
|
A run that stopped is recorded the same way, without `--commit`; the summary's third line quotes each task's stop reason.
|
|
24
24
|
|
|
@@ -28,24 +28,23 @@ When `.doug/plan.json` carries `cards` (made by `plan.mjs merge`), the run is re
|
|
|
28
28
|
|
|
29
29
|
1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/cost.mjs" <run-id> --by-card`: after the usual tables, a per-card table with each card's tasks' agents summed for that card and one row for the shared integration agents (the integrate and level-adversary agents, task `level-<n>`). The card rows are each card's `--cost`; the shared row is `--shared-cost`, the same number for every card. Pass the Codex line's USD as --codex-cost to the first card's record only.
|
|
30
30
|
2. Classify the adversary blocks per card: `board.mjs record` lists only that card's tasks' blocks and refuses the others' ids.
|
|
31
|
-
3. For each card, in the plan's `cards` order: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json> --wall "<m> min" --commit <sha> --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.
|
|
31
|
+
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> --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.
|
|
32
32
|
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.
|
|
33
33
|
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`).
|
|
34
34
|
6. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect <report.json> --run <run-id> --commit <sha> --adversary ...` once for the whole report, no `--card` (each appended lesson's card comes from its own task).
|
|
35
|
-
7. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --card <id> --wall "<m> min" --commit <sha>` 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.
|
|
35
|
+
7. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --card <id> --run <run-id> --wall "<m> min" --commit <sha>` 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.
|
|
36
36
|
|
|
37
37
|
## A hand-track landing
|
|
38
38
|
|
|
39
|
-
With the card id, the landing commit, the wall clock, and the gate line — one `<command> <exit>` clause per entry of `.doug/config.json` `stopGate.commands`, in that order, joined by `; ` (a test command may add its pass count in parentheses; Doug's own is `typecheck 0; test:unit 0 (<n> passed)`; a pytest project's might be `pytest 0`):
|
|
39
|
+
With the card id, the landing commit, the wall clock, and the gate line — one `<command> <exit>` clause per entry of `.doug/config.json` `stopGate.commands`, in that order, joined by `; ` (a test command may add its pass count in parentheses; Doug's own is `typecheck 0; test:unit 0 (<n> passed)`; a pytest project's might be `pytest 0`). Step 4 alone also takes the coder rounds (how many until the reviewer passed) and the lead's classed reviewer blockers:
|
|
40
40
|
|
|
41
41
|
1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> --hand --commit <sha> --wall "<m> min" --gate "<gate line>"`, with `--note "<one line>"` when something about the build is worth keeping. 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`.
|
|
42
42
|
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.
|
|
43
43
|
3. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" record <id> --hand --commit <sha> --wall "<m> min" --gate "<gate line>" [--note "<one line>"]`, the same values step 1 used. 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, USD, 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. 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.
|
|
44
|
-
|
|
45
|
-
`memory.mjs reflect` does not run on a hand-track landing: there is no report for the cheap-row pass to read.
|
|
44
|
+
4. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect --hand <id> --commit <sha> --rounds <n> [--review "R1=<class>: <reason>"]... [--note "<one line>"]`. It reads the briefs, reviewer reports and lessons.json doug-hand saved, applies helpful/harmful counters to the card's memoryUsed, proposes at most three lessons on the cheap row, filtered in code. Its one-line summary (with the measured cost) goes in the chat. An exit 1 naming the LLM pass is said in the chat and the landing goes on regardless — reflect is a log, not a gate, the same as record.
|
|
46
45
|
|
|
47
46
|
Say the commit, the wall clock, and the gate line in the chat.
|
|
48
47
|
|
|
49
48
|
## Then
|
|
50
49
|
|
|
51
|
-
Moving the card and committing the record and the log belong to the calling skill (`/doug-next` step 5,
|
|
50
|
+
Moving the card and committing the record and the log belong to the calling skill (`/doug-next` step 5, doug-hand step 4); a record change shows on the served page (`doug board serve`).
|
|
@@ -12,7 +12,7 @@ The swarm lives inside a plan task (decision 0001 as amended 2026-09-06): the pl
|
|
|
12
12
|
|
|
13
13
|
1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" validate`: the plan must be `approved`. Nothing runs on a draft; approval is the user's act (`/doug-approve`), never this skill's.
|
|
14
14
|
2. `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" show` prints `Swarm: on` when the plan opted in with `plan.mjs set swarm on`. Without it the same workflow runs the fixed pipeline; do not set it here on your own, because the opt-in is the user's (each swarm task spends a lead, N workers, and a merge instead of one implementer). The swarm pays on a task with independent deliverables that share no new symbol; a plan of small coupled tasks runs as the pipeline (`docs/decisions/0001-hierarchical-swarms.md`, Amendment 2026-09-20: one brief per task at +2.67 USD and +6.1 min against the pipeline on 2026-09-11; five briefs, 8/8 hidden tests, 8.31 USD, 8.5 min on 2026-09-19).
|
|
15
|
-
3. The caller is a user-invoked skill or command (`/doug-implement`, `/doug-next`, `/doug-swarm`,
|
|
15
|
+
3. The caller is a user-invoked skill or command (`/doug-implement`, `/doug-next`, `/doug-swarm`, doug-hand for a size M or L hand-track card the user chose to swarm): that invocation is the opt-in the Workflow tool requires. No swarm runs in the background or on a schedule.
|
|
16
16
|
|
|
17
17
|
## Launch
|
|
18
18
|
|
|
@@ -52,6 +52,9 @@ const taskBudget = { ...DEFAULT_BUDGET, ...(plan.budget && typeof plan.budget ==
|
|
|
52
52
|
// The phrase the verifier and reviewer put at the start of a finding when the spec cannot satisfy the plan's
|
|
53
53
|
// acceptance criteria. Such a block goes back to the planner and is never retried.
|
|
54
54
|
const CONTRADICTION_MARKER = 'SPEC CONTRADICTS ACCEPTANCE'
|
|
55
|
+
// Why a task stopped: every halt site sets one of these as stopClass beside its prose stopReason. Kept equal to
|
|
56
|
+
// STOP_CLASSES in lib/board.mjs by a template test (this file may not import).
|
|
57
|
+
const STOP_CLASSES = ['implementer-blocked', 'partial', 'stage-missing', 'spec-contradiction', 'environment', 'adversary-not-run', 'outside-owned', 'new-blockers-twice', 'fix-attempts-exhausted', 'stalled', 'budget', 'no-new-commit', 'dependency-skipped', 'stage-threw', 'level-adversary']
|
|
55
58
|
// The phrase a verifier, checker, or size-check puts at the start of a finding when a command could not run for
|
|
56
59
|
// an environment reason (a tool missing from the worktree even after the install step, a sandbox denial) rather
|
|
57
60
|
// than a defect in the diff. The fix loop never retries such a finding (card reused-s-task-worktree-install):
|
|
@@ -284,8 +287,11 @@ const INTEGRATE_SCHEMA = {
|
|
|
284
287
|
branch: { type: 'string' },
|
|
285
288
|
merged: { type: 'array', items: { type: 'string' } },
|
|
286
289
|
conflicts: { type: 'array', items: { type: 'string' } },
|
|
287
|
-
verify: { type: 'array', items: { type: 'object', required: ['command', 'ok'], properties: { command: { type: 'string' }, ok: { type: 'boolean' }, outputTail: { type: 'string' } } } },
|
|
290
|
+
verify: { type: 'array', items: { type: 'object', required: ['command', 'ok'], properties: { command: { type: 'string' }, ok: { type: 'boolean' }, outputTail: { type: 'string' }, credited: { type: 'string' } } } },
|
|
288
291
|
ok: { type: 'boolean' },
|
|
292
|
+
// Card level-adversary-empty-diff: the integration branch's HEAD sha before this stage's merges; the level
|
|
293
|
+
// adversary diffs from it. Optional: a missing or malformed value falls back to the level's base branch.
|
|
294
|
+
baseCommit: { type: 'string' },
|
|
289
295
|
// Card integration-acceptance-recorded: only the last level's integrate stage fills this in (step 3b of
|
|
290
296
|
// integratePrompt); not required, since an earlier level has not built the whole plan yet.
|
|
291
297
|
acceptance: { type: 'array', description: 'one entry per acceptance command listed in the prompt, [] when none', items: { type: 'object', required: ['text', 'command', 'ok', 'exitCode'], properties: { text: { type: 'string' }, command: { type: 'string' }, ok: { type: 'boolean' }, exitCode: { type: ['integer', 'null'] } } } },
|
|
@@ -300,7 +306,7 @@ const INTEGRATE_SCHEMA = {
|
|
|
300
306
|
// (empty, prose, or unrecognised) is dropped rather than trusted or blamed. A plan command with no matching
|
|
301
307
|
// report is appended as { ..., reported: false } - unreported and failed are different causes and get different
|
|
302
308
|
// reason clauses. Pure: never mutates its argument, never throws on odd shapes.
|
|
303
|
-
function enforceIntegrationAcceptance(integration, isLastLevel, entries) {
|
|
309
|
+
function enforceIntegrationAcceptance(integration, isLastLevel, entries, verifyCommands = []) {
|
|
304
310
|
if (!integration || typeof integration !== 'object') return integration
|
|
305
311
|
if (!isLastLevel) {
|
|
306
312
|
const rest = { ...integration }
|
|
@@ -309,10 +315,20 @@ function enforceIntegrationAcceptance(integration, isLastLevel, entries) {
|
|
|
309
315
|
}
|
|
310
316
|
const reported = Array.isArray(integration.acceptance) ? integration.acceptance.filter(a => a && typeof a === 'object') : []
|
|
311
317
|
const acceptance = []
|
|
318
|
+
const verifyList = Array.isArray(integration.verify) ? integration.verify.filter(v => v && typeof v === 'object') : []
|
|
319
|
+
const repeated = new Set((Array.isArray(verifyCommands) ? verifyCommands : []).map(c => String(c).trim()))
|
|
312
320
|
for (const e of entries) {
|
|
313
321
|
if (!e.command) continue
|
|
314
322
|
const wantCommand = String(e.command).trim()
|
|
315
323
|
if (!wantCommand) continue
|
|
324
|
+
// Card flow-suite-dedupe: a command that repeats a plan.verify command takes its result from the verify item.
|
|
325
|
+
if (repeated.has(wantCommand)) {
|
|
326
|
+
const v = verifyList.find(x => typeof x.command === 'string' && String(x.command).trim() === wantCommand)
|
|
327
|
+
acceptance.push(v
|
|
328
|
+
? { text: e.text, command: e.command, ok: v.ok === true, exitCode: v.ok === true ? 0 : null, from: 'verify' }
|
|
329
|
+
: { text: e.text, command: e.command, ok: false, exitCode: null, reported: false })
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
316
332
|
const match = reported.find(a => typeof a.command === 'string' && String(a.command).trim() === wantCommand)
|
|
317
333
|
acceptance.push(match
|
|
318
334
|
? { text: match.text, command: e.command, ok: match.ok, exitCode: match.exitCode }
|
|
@@ -1212,10 +1228,9 @@ function levelSpecLines(sReady) {
|
|
|
1212
1228
|
// codex-review over a level's size-S tasks on the integration branch, once, after the merge (and once more to confirm
|
|
1213
1229
|
// a fix pass). Every blocker must name a file, because that is how it finds the task it goes back to.
|
|
1214
1230
|
function levelAdversaryPrompt(li, sReady, levelBase, confirming) {
|
|
1215
|
-
const verify = (plan.verify || []).filter(Boolean)
|
|
1216
1231
|
const guard = t => String(t).replace(/^DOUG_SPEC_EOF$/gm, 'DOUG_SPEC_EOF ')
|
|
1217
1232
|
const command = [
|
|
1218
|
-
`cat <<'DOUG_SPEC_EOF' | ${adversary.command} --base ${shellQuote(levelBase)} --head ${shellQuote(integrationBranch)} --dir ${shellQuote(INTEGRATION_WORKTREE)} --spec - --timeout-ms ${adversary.timeoutMs}${typeof adversary.effort === 'string' && adversary.effort ? ` --effort ${adversary.effort}` : ''}
|
|
1233
|
+
`cat <<'DOUG_SPEC_EOF' | ${adversary.command} --base ${shellQuote(levelBase)} --head ${shellQuote(integrationBranch)} --dir ${shellQuote(INTEGRATION_WORKTREE)} --spec - --timeout-ms ${adversary.timeoutMs}${typeof adversary.effort === 'string' && adversary.effort ? ` --effort ${adversary.effort}` : ''}`,
|
|
1219
1234
|
`Level ${li} of plan "${guard(plan.title)}": the size-S tasks below were merged into ${integrationBranch} with one focused check each and no adversary of their own. Review them together against their specs. Every blocker must name the file it is about, so it can go back to the task that owns that file.`,
|
|
1220
1235
|
...(confirming ? [`A fix pass answered the blockers you raised on this level; confirm each one is fixed, name it as fixed or still open, and check the fix for regressions rather than exploring afresh.`] : [`This is the first review of this level: explore freely and try to refute the changes however you can.`]),
|
|
1221
1236
|
``,
|
|
@@ -1310,7 +1325,7 @@ function confirmationLines(pass, ledger, impl) {
|
|
|
1310
1325
|
}
|
|
1311
1326
|
|
|
1312
1327
|
function adversaryPrompt(task, impl, baseBranch, pass, ledger) {
|
|
1313
|
-
const verify = [task.verify
|
|
1328
|
+
const verify = [task.verify].filter(Boolean)
|
|
1314
1329
|
const guard = s => String(s).replace(/^DOUG_SPEC_EOF$/gm, 'DOUG_SPEC_EOF ')
|
|
1315
1330
|
const command = [
|
|
1316
1331
|
`cat <<'DOUG_SPEC_EOF' | ${adversary.command} --base ${shellQuote(baseBranch)} --head ${shellQuote(impl.branch)} --dir ${shellQuote(impl.worktreePath)} --spec - --timeout-ms ${adversary.timeoutMs}${typeof adversary.effort === 'string' && adversary.effort ? ` --effort ${adversary.effort}` : ''}${verify.map(v => ` --verify ${shellQuote(v)}`).join('')}`,
|
|
@@ -1417,14 +1432,26 @@ function integratePrompt(levelIndex, results, baseBranch, isLastLevel, keepWorkt
|
|
|
1417
1432
|
` if it exists (a previous level made it): use it as is; it already has ${integrationBranch} checked out.`,
|
|
1418
1433
|
]),
|
|
1419
1434
|
` Do every following git and verify command inside that worktree (git -C or cd into it).`,
|
|
1435
|
+
` Before any merge of this stage, run git rev-parse HEAD in that worktree and report the full 40-character sha as baseCommit.`,
|
|
1420
1436
|
`2. Merge each branch with a merge commit (git merge --no-ff <branch>). If a merge conflicts, resolve it only if the resolution is mechanical and obviously correct; otherwise git merge --abort and list the branch in conflicts. ${NO_TRAILERS} That includes the merge commits you make here.`,
|
|
1421
1437
|
`3. Install dependencies there if the verify commands need them and they are missing${plan.install ? ` (${plan.install})` : ''}. Then run every command in: ${plan.verify.length ? plan.verify.map(v => `"${v}"`).join(', ') : 'the project test command'} and record real exit statuses.`,
|
|
1438
|
+
...(plan.verify.length
|
|
1439
|
+
? [
|
|
1440
|
+
` When .doug/hooks/scripts/verify.mjs exists, run them from the repository root (not inside the worktree) as one command, so a tree already verified is credited instead of re-run:`,
|
|
1441
|
+
` node .doug/hooks/scripts/verify.mjs --json --by integrate --cwd .claude/worktrees/doug-integration --record-dir . ${worktrees.map(w => `--credit-dir ${shellQuote(w)}`).join(' ')} ${plan.verify.map(v => `--command ${shellQuote(v)}`).join(' ')}`,
|
|
1442
|
+
` Copy each entry of its JSON results[] into verify as { command, ok, outputTail }, and add credited: "verified-tree" to each when the JSON's credited is not null. When verify.mjs does not exist, run the commands one by one as above.`,
|
|
1443
|
+
]
|
|
1444
|
+
: []),
|
|
1422
1445
|
// Card integration-acceptance-recorded: only the last level runs the plan's acceptance commands, and only
|
|
1423
1446
|
// after the merges above - the plan is not complete at an earlier level, so its prompt never mentions this.
|
|
1424
1447
|
...(isLastLevel
|
|
1425
1448
|
? [
|
|
1426
1449
|
`3b. Plan acceptance criteria (a line starting with $ is the command that checks the criterion above it and exits 0 when it holds; the plan is complete at this level, so run every one now):`,
|
|
1427
|
-
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command
|
|
1450
|
+
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command
|
|
1451
|
+
? ((plan.verify || []).some(v => String(v).trim() === String(a.command).trim())
|
|
1452
|
+
? [`- ${a.text} is checked by step 3's \`${a.command}\`; do not run it again and do not report it in acceptance; the workflow takes its result from verify.`]
|
|
1453
|
+
: [`- ${a.text}`, ` $ ${a.command}`])
|
|
1454
|
+
: [`- ${a.text}`]),
|
|
1428
1455
|
`Run every $ command above inside this integration worktree, exactly as written, after the merges, and report each one in \`acceptance\` as { text, command, ok, exitCode } with its real exit code (ok means exit code 0; exitCode null only if the command could not start). Never report one you did not run; return acceptance: [] when the plan lists no commands.`,
|
|
1429
1456
|
`Report each command string exactly as it is written above, character for character, even when you ran it behind a cd or git -C prefix: the workflow matches your report to the plan by that string, and a command it cannot match counts as not run and fails the integration.`,
|
|
1430
1457
|
`A criterion with no $ line is not yours to judge here: do not report it.`,
|
|
@@ -1441,8 +1468,8 @@ function integratePrompt(levelIndex, results, baseBranch, isLastLevel, keepWorkt
|
|
|
1441
1468
|
// ---- Task loop -------------------------------------------------------------------
|
|
1442
1469
|
// One task from start to a verdict: implement (or check out a reused branch), then verify, review and the
|
|
1443
1470
|
// adversary. While the block is one the checks described inside the task's own files, relaunch the implementer in
|
|
1444
|
-
// the same worktree; a fix pass reruns the
|
|
1445
|
-
// loop stops as soon as it stops converging (no new commit, two consecutive novel blockers, or the task budget).
|
|
1471
|
+
// the same worktree; a fix pass reruns the verifier or reviewer that blocked, then one focused check and the
|
|
1472
|
+
// adversary together, and the loop stops as soon as it stops converging (no new commit, two consecutive novel blockers, or the task budget).
|
|
1446
1473
|
async function startTask(task, baseBranch, launch = agent) {
|
|
1447
1474
|
if (task.reuse) return launch(reusePrompt(task, baseBranch), { label: `reuse:${task.id}`, phase: 'Implement', isolation: 'worktree', schema: IMPLEMENT_SCHEMA, model: CHECKOUT_MODEL })
|
|
1448
1475
|
if (swarmApplies(task)) return runSwarm(task, baseBranch, launch)
|
|
@@ -1790,27 +1817,27 @@ function ownedBySibling(task, p) {
|
|
|
1790
1817
|
|
|
1791
1818
|
// Can this block be fixed by the same implementer, or must a human see it?
|
|
1792
1819
|
function retriable(task, r) {
|
|
1793
|
-
if (!r.impl) return { ok: false, reason: 'implementer returned nothing' }
|
|
1820
|
+
if (!r.impl) return { ok: false, reason: 'implementer returned nothing', stopClass: 'stage-missing' }
|
|
1794
1821
|
// A fix pass that returned no structured result changed nothing the loop knows of: the branch and worktree it
|
|
1795
1822
|
// had are still there, so the same implementer is asked again.
|
|
1796
1823
|
if (r.noResult) return { ok: true, reason: r.noResult }
|
|
1797
|
-
if (r.impl.blocked) return { ok: false, reason: `implementer blocked: ${r.impl.blockedReason || 'no reason given'}
|
|
1824
|
+
if (r.impl.blocked) return { ok: false, reason: `implementer blocked: ${r.impl.blockedReason || 'no reason given'}`, stopClass: 'implementer-blocked' }
|
|
1798
1825
|
// A partial result is not a check failure, whether it came from the initial worker's one resume or from a fix
|
|
1799
1826
|
// pass: the fix loop never retries it (card worker-context-handoff).
|
|
1800
|
-
if (r.impl.partial) return { ok: false, reason: 'a partial result is not retried by the fix loop' }
|
|
1827
|
+
if (r.impl.partial) return { ok: false, reason: 'a partial result is not retried by the fix loop', stopClass: 'partial' }
|
|
1801
1828
|
// Only a stage that actually ran can be missing; a fix pass deliberately skips the stages that did not block.
|
|
1802
1829
|
const ran = r.stages || []
|
|
1803
|
-
if (ran.includes('verify') && !r.ver) return { ok: false, reason: 'verifier returned nothing' }
|
|
1804
|
-
if (ran.includes('review') && !r.rev) return { ok: false, reason: 'reviewer returned nothing' }
|
|
1805
|
-
if (ran.includes('check') && !r.check) return { ok: false, reason: 'check returned nothing' }
|
|
1830
|
+
if (ran.includes('verify') && !r.ver) return { ok: false, reason: 'verifier returned nothing', stopClass: 'stage-missing' }
|
|
1831
|
+
if (ran.includes('review') && !r.rev) return { ok: false, reason: 'reviewer returned nothing', stopClass: 'stage-missing' }
|
|
1832
|
+
if (ran.includes('check') && !r.check) return { ok: false, reason: 'check returned nothing', stopClass: 'stage-missing' }
|
|
1806
1833
|
const texts = [...((r.ver && r.ver.findings) || []), ...(((r.rev && r.rev.issues) || []).map(i => i.description))]
|
|
1807
1834
|
const contradiction = texts.find(t => typeof t === 'string' && t.includes(CONTRADICTION_MARKER))
|
|
1808
|
-
if (contradiction) return { ok: false, reason: `spec contradicts acceptance: ${contradiction}
|
|
1835
|
+
if (contradiction) return { ok: false, reason: `spec contradicts acceptance: ${contradiction}`, stopClass: 'spec-contradiction' }
|
|
1809
1836
|
const environmentOnly = texts.find(t => typeof t === 'string' && t.includes(ENVIRONMENT_MARKER))
|
|
1810
|
-
if (environmentOnly) return { ok: false, reason: `environment, not the code: ${environmentOnly}
|
|
1811
|
-
if (adversary && r.adv && !r.adv.ran) return { ok: false, reason: `adversary did not run: ${r.adv.error || 'no reason given'}
|
|
1837
|
+
if (environmentOnly) return { ok: false, reason: `environment, not the code: ${environmentOnly}`, stopClass: 'environment' }
|
|
1838
|
+
if (adversary && r.adv && !r.adv.ran && !r.blockingStage) return { ok: false, reason: `adversary did not run: ${r.adv.error || 'no reason given'}`, stopClass: 'adversary-not-run' }
|
|
1812
1839
|
const outside = [...new Set(findingPaths(r).filter(p => !ownsPath(task, p) && !ownedBySibling(task, p)))]
|
|
1813
|
-
if (outside.length) return { ok: false, reason: `findings name files outside the owned set: ${outside.join(', ')}
|
|
1840
|
+
if (outside.length) return { ok: false, reason: `findings name files outside the owned set: ${outside.join(', ')}`, stopClass: 'outside-owned' }
|
|
1814
1841
|
return { ok: true, reason: 'every finding is within the owned files' }
|
|
1815
1842
|
}
|
|
1816
1843
|
|
|
@@ -2094,9 +2121,9 @@ function stageHead(ver, check) {
|
|
|
2094
2121
|
}
|
|
2095
2122
|
|
|
2096
2123
|
// baseBranch is a parameter, not the outer binding, so this block can be built and exercised on its own.
|
|
2097
|
-
// ctx: { ledger, launch, lead, prevCommit, blocking }. On pass 1 the verifier
|
|
2098
|
-
//
|
|
2099
|
-
//
|
|
2124
|
+
// ctx: { ledger, launch, lead, prevCommit, blocking }. On pass 1 the verifier, the reviewer, and the adversary run
|
|
2125
|
+
// together. On a fix pass the verifier or the reviewer reruns first when it is the stage that blocked, and ends the
|
|
2126
|
+
// pass if it blocks again; then the focused check and the adversary run together.
|
|
2100
2127
|
async function runChecks(task, impl, pass, baseBranch, ctx) {
|
|
2101
2128
|
const c = ctx || {}
|
|
2102
2129
|
const ledger = c.ledger || []
|
|
@@ -2117,57 +2144,68 @@ async function runChecks(task, impl, pass, baseBranch, ctx) {
|
|
|
2117
2144
|
if (check) ({ ver, rev } = readCheck(check))
|
|
2118
2145
|
blockingStage = !(check && check.passed && rev && rev.approve) ? 'check' : null
|
|
2119
2146
|
} else if (pass === 1) {
|
|
2120
|
-
// The verifier and the
|
|
2121
|
-
// launches them together; a null from
|
|
2147
|
+
// The verifier, the reviewer, and the adversary are read-only and none reads another's result, so a first pass
|
|
2148
|
+
// launches them together; a null from any (skipped, or dead after retries) is that stage returning nothing.
|
|
2122
2149
|
stages.push('verify')
|
|
2123
2150
|
stages.push('review')
|
|
2124
2151
|
const crew = crewOf(task)
|
|
2125
2152
|
for (let seat = 2; seat <= crew.reviewers; seat++) stages.push(`review-${seat}`)
|
|
2153
|
+
const withAdversary = adversaryRequired(task)
|
|
2154
|
+
const advStages = []
|
|
2126
2155
|
const pair = await parallel([
|
|
2127
2156
|
() => launch(verifyPrompt(task, impl, baseBranch), agentOpts('verify', task, { label: `verify:${task.id}`, phase: 'Verify', agentType: 'doug-flow:verifier', schema: VERIFY_SCHEMA })),
|
|
2128
2157
|
() => launch(reviewPrompt(task, impl, baseBranch), agentOpts('review', task, { label: `review:${task.id}`, phase: 'Review', agentType: 'doug-flow:reviewer', schema: REVIEW_SCHEMA })),
|
|
2129
2158
|
...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 }))),
|
|
2159
|
+
...(withAdversary ? [() => runAdversaryCrew(task, impl, pass, baseBranch, ledger, launch, advStages)] : []),
|
|
2130
2160
|
])
|
|
2131
2161
|
ver = (pair && pair[0]) || null
|
|
2132
|
-
rev = pair ? mergeReviews(pair.slice(1)) : null
|
|
2162
|
+
rev = pair ? mergeReviews(pair.slice(1, 1 + crew.reviewers)) : null
|
|
2133
2163
|
stageResults.verify = ver
|
|
2134
2164
|
stageResults.review = rev
|
|
2135
|
-
//
|
|
2136
|
-
|
|
2137
|
-
|
|
2165
|
+
// The adversary no longer waits for verify and review (card flow-adversary-parallel, 2026-10-07): waiting
|
|
2166
|
+
// added about five minutes to every full task. The cost is a Codex review of trees verify or review rejects;
|
|
2167
|
+
// integration still needs all three verdicts (isReady), and the ledger reads each stage's findings.
|
|
2168
|
+
if (withAdversary) {
|
|
2169
|
+
stages.push(...advStages)
|
|
2170
|
+
adv = (pair && pair[1 + crew.reviewers]) || null
|
|
2138
2171
|
stageResults.adversary = adv
|
|
2139
2172
|
}
|
|
2140
2173
|
blockingStage = !(ver && ver.passed) ? 'verify' : !(rev && rev.approve) ? 'review' : adversaryBlocked(adv) ? 'adversary' : null
|
|
2141
2174
|
} else {
|
|
2142
2175
|
const first = c.blocking || 'verify'
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
}
|
|
2176
|
+
if (first === 'verify') {
|
|
2177
|
+
stages.push('verify')
|
|
2178
|
+
ver = await launch(verifyPrompt(task, impl, baseBranch), agentOpts('verify', task, { label: `verify:${task.id}${suffix}`, phase: 'Verify', agentType: 'doug-flow:verifier', schema: VERIFY_SCHEMA }))
|
|
2179
|
+
stageResults.verify = ver
|
|
2180
|
+
if (!(ver && ver.passed)) blockingStage = 'verify'
|
|
2181
|
+
} else if (first === 'review') {
|
|
2182
|
+
stages.push('review')
|
|
2183
|
+
const crew = crewOf(task)
|
|
2184
|
+
for (let seat = 2; seat <= crew.reviewers; seat++) stages.push(`review-${seat}`)
|
|
2185
|
+
const seats = await parallel(Array.from({ length: crew.reviewers }, (_, i) => () => launch(reviewPrompt(task, impl, baseBranch, i + 1), agentOpts('review', task, { label: `review${i ? `-${i + 1}` : ''}:${task.id}${suffix}`, phase: 'Review', agentType: 'doug-flow:reviewer', schema: REVIEW_SCHEMA }))))
|
|
2186
|
+
rev = mergeReviews(seats || [])
|
|
2187
|
+
stageResults.review = rev
|
|
2188
|
+
if (!(rev && rev.approve)) blockingStage = 'review'
|
|
2189
|
+
}
|
|
2190
|
+
if (!blockingStage) {
|
|
2191
|
+
// The focused check and the adversary are both read-only, so they run together (card flow-adversary-parallel).
|
|
2192
|
+
// The stages are recorded in the order the serial loop ran them (the adversary first when it blocked the last
|
|
2193
|
+
// pass), since updateLedger reads them in order to tell a stage confirming a fix from one re-reporting it.
|
|
2194
|
+
const withAdversary = adversaryRequired(task)
|
|
2195
|
+
const adversaryFirst = withAdversary && first === 'adversary'
|
|
2196
|
+
const advStages = []
|
|
2197
|
+
const runCheck = () => launch(checkPrompt(task, impl, c.prevCommit || null, baseBranch, ledger), agentOpts('verify', task, { label: `check:${task.id}:${pass}`, phase: 'Verify', agentType: 'doug-flow:verifier', schema: CHECK_SCHEMA }))
|
|
2198
|
+
const runAdv = () => runAdversaryCrew(task, impl, pass, baseBranch, ledger, launch, advStages)
|
|
2199
|
+
const both = !withAdversary ? [await runCheck()] : await parallel(adversaryFirst ? [runAdv, runCheck] : [runCheck, runAdv])
|
|
2200
|
+
check = (both && both[adversaryFirst ? 1 : 0]) || null
|
|
2201
|
+
stageResults.check = check
|
|
2202
|
+
// The one check stands in for both stages, so the rest of the loop and the report read it as they read them.
|
|
2203
|
+
if (check) ({ ver, rev } = readCheck(check))
|
|
2204
|
+
if (withAdversary) { adv = (both && both[adversaryFirst ? 0 : 1]) || null; stageResults.adversary = adv }
|
|
2205
|
+
stages.push(...(adversaryFirst ? [...advStages, 'check'] : ['check', ...advStages]))
|
|
2206
|
+
const checkBlocked = !(check && check.passed && rev && rev.approve)
|
|
2207
|
+
const advBlocked = withAdversary && adversaryBlocked(adv)
|
|
2208
|
+
blockingStage = adversaryFirst && advBlocked ? 'adversary' : checkBlocked ? 'check' : advBlocked ? 'adversary' : null
|
|
2171
2209
|
}
|
|
2172
2210
|
}
|
|
2173
2211
|
const head = stageHead(ver, check)
|
|
@@ -2338,20 +2376,20 @@ async function runTask(task, baseBranch, resume) {
|
|
|
2338
2376
|
})
|
|
2339
2377
|
// The last supervisor that ran on this task, for the report; null when none did.
|
|
2340
2378
|
const lastSupervisor = () => { for (let i = attempts.length - 1; i >= 0; i--) if (attempts[i].supervisor) return attempts[i].supervisor; return null }
|
|
2341
|
-
if (ready) return { ...r, attempts, ledger, budget: budgetOf(), stopReason: null, supervisor: lastSupervisor() }
|
|
2379
|
+
if (ready) return { ...r, attempts, ledger, budget: budgetOf(), stopReason: null, stopClass: null, supervisor: lastSupervisor() }
|
|
2342
2380
|
const why = notReadyWhy(r)
|
|
2343
|
-
const halt = reason => {
|
|
2381
|
+
const halt = (stopClass, reason) => {
|
|
2344
2382
|
for (const e of ledger) if (e.status === 'open') log(`${task.id} open finding ${e.id} [${e.stage}] ${e.file || '(no file)'}: ${String(e.description).slice(0, 120)}`)
|
|
2345
|
-
return { ...r, attempts, ledger, budget: budgetOf(), stopReason: reason, supervisor: lastSupervisor() }
|
|
2383
|
+
return { ...r, attempts, ledger, budget: budgetOf(), stopReason: reason, stopClass, supervisor: lastSupervisor() }
|
|
2346
2384
|
}
|
|
2347
|
-
if (!decision.ok) return halt(`${why}; not retried: ${decision.reason}`)
|
|
2385
|
+
if (!decision.ok) return halt(decision.stopClass, `${why}; not retried: ${decision.reason}`)
|
|
2348
2386
|
// Two passes in a row that each raise a blocker nobody had seen means the checks are exploring, not converging.
|
|
2349
2387
|
const last = attempts[attempts.length - 1]
|
|
2350
2388
|
const prior = attempts[attempts.length - 2]
|
|
2351
2389
|
if (prior && last.newFindings.length && prior.newFindings.length) {
|
|
2352
|
-
return halt(`${why}; stopped: two consecutive passes raised new blockers (${prior.newFindings.join(', ')}; ${last.newFindings.join(', ')})`)
|
|
2390
|
+
return halt('new-blockers-twice', `${why}; stopped: two consecutive passes raised new blockers (${prior.newFindings.join(', ')}; ${last.newFindings.join(', ')})`)
|
|
2353
2391
|
}
|
|
2354
|
-
if (attempts.length > fixAttempts) return halt(`${why}; fix attempts exhausted (${fixAttempts} of ${fixAttempts})`)
|
|
2392
|
+
if (attempts.length > fixAttempts) return halt('fix-attempts-exhausted', `${why}; fix attempts exhausted (${fixAttempts} of ${fixAttempts})`)
|
|
2355
2393
|
// The supervisor (card fix-loop-supervisor): before a fix pass after the first, when the stall signals fire. A
|
|
2356
2394
|
// pass that returned no result attacked nothing, so it is not a stall. A task stalled a second time stops here
|
|
2357
2395
|
// instead of spending the remaining fix budget; otherwise the supervisor runs after the budget check below, on
|
|
@@ -2362,7 +2400,7 @@ async function runTask(task, baseBranch, resume) {
|
|
|
2362
2400
|
if (signals.length && attempts.some(a => a.supervisor && a.supervisor.stalled)) {
|
|
2363
2401
|
const findings = [...new Set(signals.filter(x => x.kind === 'repeat').map(x => x.finding))]
|
|
2364
2402
|
lastAttempt.supervisor = { ran: false, stalled: true, brief: null, signals, stop: { kind: 'stalled', attempts: attempts.length, findings } }
|
|
2365
|
-
return halt(`${why}; stopped: stalled twice (${describeStall(signals)})`)
|
|
2403
|
+
return halt('stalled', `${why}; stopped: stalled twice (${describeStall(signals)})`)
|
|
2366
2404
|
}
|
|
2367
2405
|
const blocking = r.blockingStage || 'verify'
|
|
2368
2406
|
// What the next pass would cost: the fix agent plus the stages rule 3 would run. The adversary stage spends two
|
|
@@ -2391,7 +2429,7 @@ async function runTask(task, baseBranch, resume) {
|
|
|
2391
2429
|
const projected = spent.elapsedMs + spent.elapsedMs / passesDone
|
|
2392
2430
|
if (projected > limit) over = `elapsedMs ${show(projected)} > ${limit}`
|
|
2393
2431
|
}
|
|
2394
|
-
if (over) return halt(`${why}; stopped: next attempt would exceed the task budget (${over})`)
|
|
2432
|
+
if (over) return halt('budget', `${why}; stopped: next attempt would exceed the task budget (${over})`)
|
|
2395
2433
|
const pass = r.pass + 1
|
|
2396
2434
|
let brief = null
|
|
2397
2435
|
if (signals.length) {
|
|
@@ -2447,7 +2485,7 @@ async function runTask(task, baseBranch, resume) {
|
|
|
2447
2485
|
r = record({ task, impl, ver: null, rev: null, adv: null, check: null, pass, stages: ['fix'], stageResults: {}, blockingStage: null, commit: fix.commit || null }, prevCommit)
|
|
2448
2486
|
const decided = fixBlocked ? retriable(task, r) : null
|
|
2449
2487
|
attempts.push({ pass, stages: ['fix'], commit: r.commit, blockingStage: null, verified: false, reviewed: false, adversary: null, newFindings: r.newFindings || [], fixedFindings: r.fixedFindings || [], notes: r.droppedFindings || [], ready: false, retriable: decided, spent: measure(), commands: passCommands(r) })
|
|
2450
|
-
return
|
|
2488
|
+
return fixBlocked ? halt(decided.stopClass, `${notReadyWhy(r)}; not retried: ${decided.reason}`) : halt('no-new-commit', `${why}; stopped: fix pass ${pass} made no new commit on ${impl.branch}`)
|
|
2451
2489
|
}
|
|
2452
2490
|
const next = { ...fix, branch: r.impl.branch, worktreePath, prevCommit, ...(r.impl.workers ? { workers: r.impl.workers, swarmStages: r.impl.swarmStages, splitReason: r.impl.splitReason, rebrief: r.impl.rebrief, checkFailed: r.impl.checkFailed } : {}) }
|
|
2453
2491
|
r = record(await runChecks(task, next, pass, baseBranch, { ledger, launch, lead: 'fix', prevCommit, blocking }), prevCommit)
|
|
@@ -2507,7 +2545,7 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2507
2545
|
}
|
|
2508
2546
|
const reason = missing.length > 1 ? `dependencies ${missing.join(', ')} were not integrated` : `dependency ${missing[0]} was not integrated`
|
|
2509
2547
|
log(`${t.id} not launched: ${reason}`)
|
|
2510
|
-
skipped.set(t, { task: t, impl: null, ver: null, rev: null, adv: null, check: null, attempts: [], ledger: [], budget: null, stages: [], stopReason: reason, skippedReason: reason })
|
|
2548
|
+
skipped.set(t, { task: t, impl: null, ver: null, rev: null, adv: null, check: null, attempts: [], ledger: [], budget: null, stages: [], stopReason: reason, stopClass: 'dependency-skipped', skippedReason: reason })
|
|
2511
2549
|
}
|
|
2512
2550
|
|
|
2513
2551
|
// Implement (or reuse) -> verify -> review -> adversary -> fix, per task, no barrier between tasks.
|
|
@@ -2523,7 +2561,7 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2523
2561
|
// was ever made; blocked:true keeps `implemented` false so reusePlan still refuses to reuse it.
|
|
2524
2562
|
const threwFallback = task => {
|
|
2525
2563
|
const reason = 'task stage threw (agent error, unknown agent type, or user skip)'
|
|
2526
|
-
return { task, impl: { taskId: task.id, branch: task.reuse || `doug/task-${task.id}`, worktreePath: '', filesTouched: [], commandsRun: [], summary: reason, blocked: true, blockedReason: reason, commit: null }, ver: null, rev: null, adv: null, check: null, attempts: [], ledger: [], budget: null, stopReason: reason }
|
|
2564
|
+
return { task, impl: { taskId: task.id, branch: task.reuse || `doug/task-${task.id}`, worktreePath: '', filesTouched: [], commandsRun: [], summary: reason, blocked: true, blockedReason: reason, commit: null }, ver: null, rev: null, adv: null, check: null, attempts: [], ledger: [], budget: null, stopReason: reason, stopClass: 'stage-threw' }
|
|
2527
2565
|
}
|
|
2528
2566
|
const byTask = new Map(runnable.map((task, i) => [task, results[i] || threwFallback(task)]))
|
|
2529
2567
|
const ready = level.map(task => byTask.get(task) || skipped.get(task)).filter(isReady)
|
|
@@ -2537,7 +2575,7 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2537
2575
|
const levelBase = baseBranch
|
|
2538
2576
|
const sReady = adversary ? ready.filter(r => taskShape(r.task) === 'S') : []
|
|
2539
2577
|
if (ready.length) {
|
|
2540
|
-
integration = enforceIntegrationAcceptance(await retryOnce(integratePrompt(li, ready, baseBranch, li === levels.length - 1, sReady.length > 0, !integratedOnce), agentOpts('integrate', null, { label: `integrate:level-${li}`, phase: 'Integrate', schema: INTEGRATE_SCHEMA }), `level-${li}`), li === levels.length - 1, acceptanceEntries(plan.acceptance || []))
|
|
2578
|
+
integration = enforceIntegrationAcceptance(await retryOnce(integratePrompt(li, ready, baseBranch, li === levels.length - 1, sReady.length > 0, !integratedOnce), agentOpts('integrate', null, { label: `integrate:level-${li}`, phase: 'Integrate', schema: INTEGRATE_SCHEMA }), `level-${li}`), li === levels.length - 1, acceptanceEntries(plan.acceptance || []), plan.verify || [])
|
|
2541
2579
|
integratedOnce = true
|
|
2542
2580
|
if (integration && integration.ok) {
|
|
2543
2581
|
baseBranch = integrationBranch
|
|
@@ -2548,7 +2586,12 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2548
2586
|
// branch, after the merge. A blocker goes back to the task that owns its file as a fix pass, the fixed branches are
|
|
2549
2587
|
// merged again, and the adversary confirms once; a level still blocked after that stops the run.
|
|
2550
2588
|
if (integration && integration.ok && sReady.length) {
|
|
2551
|
-
|
|
2589
|
+
// Card level-adversary-empty-diff: the sha the integrator read before the level's first merge is the diff base;
|
|
2590
|
+
// the branch name would equal --head on every level >= 1. The re-integration never replaces it.
|
|
2591
|
+
const reportedBase = integration && integration.baseCommit
|
|
2592
|
+
const adversaryBase = typeof reportedBase === 'string' && /^[0-9a-f]{40}$/.test(reportedBase) ? reportedBase : levelBase
|
|
2593
|
+
if (adversaryBase === levelBase) log(`level ${li} level adversary falls back to ${levelBase} because the integrator reported no base commit`)
|
|
2594
|
+
levelAdversary = await runLevelAdversary(li, sReady, adversaryBase, false)
|
|
2552
2595
|
if (levelAdversary.blocked) {
|
|
2553
2596
|
const blockers = levelAdversary.issues.filter(i => i.severity === 'blocker')
|
|
2554
2597
|
const items = blockers.length ? blockers : [{ severity: 'blocker', file: '', description: levelAdversary.summary || 'the level adversary blocked without an issue', evidence: null }]
|
|
@@ -2570,25 +2613,28 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2570
2613
|
const adv = { ran: true, verdict: 'fail', summary: levelAdversary.summary, issues, commandsRun: levelAdversary.commandsRun, error: null }
|
|
2571
2614
|
const again = await runTask(r.task, levelBase, { impl: r.impl, ver: r.ver, rev: r.rev, adv, commit: r.commit, pass: r.pass, attempts: r.attempts, ledger: r.ledger })
|
|
2572
2615
|
byTask.set(r.task, again)
|
|
2573
|
-
levelAdversary.fixed.push({ task: r.task.id, ready: isReady(again), stopReason: again.stopReason })
|
|
2616
|
+
levelAdversary.fixed.push({ task: r.task.id, ready: isReady(again), stopReason: again.stopReason, stopClass: again.stopClass || null })
|
|
2574
2617
|
}
|
|
2575
2618
|
const fixedReady = [...perTask.keys()].map(r => byTask.get(r.task)).filter(isReady)
|
|
2576
2619
|
if (fixedReady.length === perTask.size) {
|
|
2577
|
-
const again = enforceIntegrationAcceptance(await retryOnce(integratePrompt(li, fixedReady, levelBase, li === levels.length - 1, true, false), agentOpts('integrate', null, { label: `integrate:level-${li}:2`, phase: 'Integrate', schema: INTEGRATE_SCHEMA }), `level-${li}`), li === levels.length - 1, acceptanceEntries(plan.acceptance || []))
|
|
2620
|
+
const again = enforceIntegrationAcceptance(await retryOnce(integratePrompt(li, fixedReady, levelBase, li === levels.length - 1, true, false), agentOpts('integrate', null, { label: `integrate:level-${li}:2`, phase: 'Integrate', schema: INTEGRATE_SCHEMA }), `level-${li}`), li === levels.length - 1, acceptanceEntries(plan.acceptance || []), plan.verify || [])
|
|
2578
2621
|
levelAdversary.reintegration = again
|
|
2579
2622
|
// The re-integration is the final state of the branch, so its acceptance (not the first integration's)
|
|
2580
2623
|
// is what the level report carries (card integration-acceptance-recorded, behaviour 4). Only
|
|
2581
2624
|
// `acceptance` is copied onto the level's `integration`; a failed re-integration's own `ok` and
|
|
2582
2625
|
// `reason` stay on `levelAdversary.reintegration`, not on the level report.
|
|
2583
2626
|
if (integration && li === levels.length - 1 && again && typeof again === 'object') integration.acceptance = again.acceptance
|
|
2584
|
-
if (again && again.ok) levelAdversary.confirm = await runLevelAdversary(li, sReady.map(r => byTask.get(r.task) || r),
|
|
2627
|
+
if (again && again.ok) levelAdversary.confirm = await runLevelAdversary(li, sReady.map(r => byTask.get(r.task) || r), adversaryBase, true)
|
|
2585
2628
|
}
|
|
2586
2629
|
}
|
|
2587
2630
|
const cleared = !!(levelAdversary.confirm && !levelAdversary.confirm.blocked)
|
|
2588
2631
|
if (!cleared) {
|
|
2589
2632
|
for (const r of new Set([...sReady, ...perTask.keys()])) {
|
|
2590
2633
|
const cur = byTask.get(r.task) || r
|
|
2591
|
-
if (!cur.stopReason)
|
|
2634
|
+
if (!cur.stopReason) {
|
|
2635
|
+
cur.stopReason = levelAdversary.unowned ? `level adversary blocked on ${levelAdversary.unowned.join(', ')}: ${levelAdversary.summary}` : levelAdversary.confirm ? `level adversary still blocked after a fix pass: ${levelAdversary.confirm.summary}` : levelAdversary.reintegration ? 'the fixed branches did not integrate again' : `level adversary blocked: ${levelAdversary.summary}`
|
|
2636
|
+
cur.stopClass = 'level-adversary'
|
|
2637
|
+
}
|
|
2592
2638
|
byTask.set(r.task, cur)
|
|
2593
2639
|
}
|
|
2594
2640
|
for (const r of ready) integrated.delete(r.task.id)
|
|
@@ -2642,6 +2688,7 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2642
2688
|
attempts: r.attempts,
|
|
2643
2689
|
passes: r.attempts.length,
|
|
2644
2690
|
stopReason: r.stopReason,
|
|
2691
|
+
stopClass: r.stopClass || null,
|
|
2645
2692
|
// The fix-loop supervisor's last word on the task: ran, stalled, the brief it injected, and the stop when the
|
|
2646
2693
|
// task stalled twice ({ kind: 'stalled', attempts, findings }); null when the loop never stalled.
|
|
2647
2694
|
supervisor: r.supervisor || null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doug-gates",
|
|
3
3
|
"displayName": "Doug Gates",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.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"
|
|
@@ -110,6 +110,17 @@
|
|
|
110
110
|
]
|
|
111
111
|
}
|
|
112
112
|
],
|
|
113
|
+
"UserPromptSubmit": [
|
|
114
|
+
{
|
|
115
|
+
"hooks": [
|
|
116
|
+
{
|
|
117
|
+
"type": "command",
|
|
118
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/turn-start.mjs\"",
|
|
119
|
+
"timeout": 5
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
],
|
|
113
124
|
"SessionStart": [
|
|
114
125
|
{
|
|
115
126
|
"matcher": "compact|resume",
|