@dougharness/cli 1.4.0 → 1.5.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/ablate.js +2 -2
- package/dist/ablate.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +1 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +9 -9
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +44 -5
- package/node_modules/@dougharness/flow/lib/decisions.mjs +2 -4
- package/node_modules/@dougharness/flow/lib/learn.mjs +95 -54
- package/node_modules/@dougharness/flow/lib/memory.mjs +1380 -44
- package/node_modules/@dougharness/flow/lib/plan.mjs +40 -0
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +162 -1
- package/node_modules/@dougharness/flow/scripts/board.mjs +38 -14
- package/node_modules/@dougharness/flow/scripts/learn.mjs +1 -1
- package/node_modules/@dougharness/flow/scripts/memory.mjs +658 -36
- package/node_modules/@dougharness/flow/scripts/plan.mjs +3 -3
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +6 -2
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +2 -0
- package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +9 -8
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/workflows/card-queen.js +44 -5
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +34 -19
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/baseline.mjs +9 -0
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +5 -3
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +33 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +48 -33
- package/node_modules/@dougharness/gates/scripts/verify.mjs +20 -4
- package/package.json +3 -3
|
@@ -179,18 +179,18 @@ function cachingProvider(provider) {
|
|
|
179
179
|
async function attachLessons(plan, dir, provider) {
|
|
180
180
|
if (!existsSync(join(dir, MEMORY_DB_RELPATH))) return plan;
|
|
181
181
|
try {
|
|
182
|
-
const { openMemory, recallLessons, degradedWarning, degradedReason,
|
|
182
|
+
const { openMemory, LIVE_LESSON_SQL, recallLessons, degradedWarning, degradedReason, renderFlowLessons } = await import("../lib/memory.mjs");
|
|
183
183
|
const staleDays = readMemoryStaleDays(dir);
|
|
184
184
|
const m = openMemory(dir);
|
|
185
185
|
try {
|
|
186
186
|
const detectedProvider = await detectOnce(provider);
|
|
187
187
|
// An empty lesson store has nothing to degrade (a code-index-only database, say): no warning for it.
|
|
188
|
-
const hasLessons = m.prepare(
|
|
188
|
+
const hasLessons = m.prepare(`SELECT 1 FROM lessons WHERE ${LIVE_LESSON_SQL} LIMIT 1`).get() !== undefined;
|
|
189
189
|
const tasks = [];
|
|
190
190
|
for (const task of plan.tasks) {
|
|
191
191
|
const taskRecall = await recallLessons(m, `${task.title}\n${task.spec}`, { files: task.files, k: 8, checkoutDir: dir, provider, staleDays, detectedProvider });
|
|
192
192
|
if (hasLessons) warnOnce(degradedWarning(taskRecall, { what: "memory recall", detectedProvider }));
|
|
193
|
-
const taskLessons =
|
|
193
|
+
const taskLessons = renderFlowLessons(taskRecall.lessons);
|
|
194
194
|
const lessonsText = taskLessons.text !== null && taskRecall.degraded ? `(recall was keyword-only: ${degradedReason(taskRecall, detectedProvider)})\n${taskLessons.text}` : taskLessons.text;
|
|
195
195
|
tasks.push(taskLessons.text === null ? task : { ...task, lessons: lessonsText, lessonIds: taskLessons.ids });
|
|
196
196
|
}
|
|
@@ -40,6 +40,8 @@ Batch: one `AskUserQuestion` names every remaining card of the batch (id, title,
|
|
|
40
40
|
|
|
41
41
|
A size L card with no `docs/specs/<card-id>/spec.md` is offered feature mode first (ADR 0012): after Start and before 2b, ask with `AskUserQuestion` whether to run `/doug-start <id>` (specification, optional pseudocode, architecture, refinement) or build now; `/doug-start <id>` ends this loop and the card stays In flow. S and M cards go straight on, and any card can still run `/doug-start <id>` on request. Under autopilot the offer is not asked and the card goes straight on.
|
|
42
42
|
|
|
43
|
+
Batch: ask the offer for each such card before 2b; a card answered `/doug-start <id>` is dropped from the batch and stays In flow for `/doug-start <id>`, and the rest of the batch goes on (one card left is a plain single-card run; none left ends this loop).
|
|
44
|
+
|
|
43
45
|
## 2b. Shape: by hand, or a swarm
|
|
44
46
|
|
|
45
47
|
Size S, or a card with no size at all: by hand, no question.
|
|
@@ -50,7 +52,7 @@ Batch: a batch runs only as a swarm, so for two or more cards this step does not
|
|
|
50
52
|
|
|
51
53
|
Recommend a swarm only when the goal asks for two or more independent deliverables: separate pieces of work, each with its own test file, where none uses a function, type, or file another one adds (the 2026-09-19 split probe's four report commands, docs/live-runs.md); otherwise recommend by hand. Say in the question, in one clause, which side of this rule the card is on. The recommendation is the lead's reading of the goal, not a default.
|
|
52
54
|
|
|
53
|
-
Otherwise, for one card, ask with `AskUserQuestion`: run it as a swarm, or by hand. Swarm: spawn the planner the way `doug-plan` does, on the `plan` row: read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.plan.model`, `roles.plan.effort`), and pass that row's model (omit `model` when the row says `inherit`), but tell it this plan runs as a swarm the way `/doug-swarm` does: `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<title>\": <goal>. This plan runs as a swarm: set \"swarm\": true in the plan file, and size every task M or L unless it is truly one bounded change (a size-S task keeps a single implementer). Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts. Write .doug/plan.json and return the rendered plan." })`. Then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set card <id>` and `plan.mjs set swarm on` (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set swarm on`) guarantee the card and the opt-in even if the planner forgot, put `plan.mjs show` in the chat, and ask the approval gate with `AskUserQuestion`: approve as written, edit first, or stop. Approval is never automatic.
|
|
55
|
+
Otherwise, for one card, ask with `AskUserQuestion`: run it as a swarm, or by hand. Swarm: spawn the planner the way `doug-plan` does, on the `plan` row: read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.plan.model`, `roles.plan.effort`), and pass that row's model (omit `model` when the row says `inherit`), but tell it this plan runs as a swarm the way `/doug-swarm` does: `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<title>\": <goal>. This plan runs as a swarm: set \"swarm\": true in the plan file, and size every task M or L unless it is truly one bounded change (a size-S task keeps a single implementer). Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts. Write .doug/plan.json and return the rendered plan." })`. Then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set card <id>` and `plan.mjs set swarm on` (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set swarm on`) guarantee the card and the opt-in even if the planner forgot, put the complete output of `plan.mjs show` in the chat, verbatim, in a fenced code block, before the AskUserQuestion (never a summary instead), and ask the approval gate with `AskUserQuestion`: approve as written, edit first, or stop. Approval is never automatic.
|
|
54
56
|
|
|
55
57
|
On approve: `plan.mjs approve`, `plan.mjs anchor`, invoke the `swarm-launch` skill. Note the wall clock when it starts and ends. When it returns, save the `result` object of the Workflow tool's output to `.doug/.state/last-report.json` with the Write tool and note the run id the Workflow tool printed (`wf_...`), the way `/doug-next` step 4 does: `plan.mjs replan`, `plan.mjs done`, `board.mjs record`, and `cost.mjs <run-id>` all read those two. If the report carries `paused`, follow `/doug-next` step 4's paused paragraph: name the tasks in `next` and ask with `AskUserQuestion` whether to open the gate; yes reopens with `plan.mjs gate open <level>` and a relaunch with `resumeFromRunId`, no leaves the card In flow and stops — `plan.mjs done` refuses while the last report is paused. A paused or failed batch changes nothing in this mechanism: the paused paragraph and `plan.mjs replan` both work from the plan file, which keeps `cards` through replan, and a second pass is detected the way `/doug-next` section 2 is, by the plan's `cards` matching this batch. Otherwise read the report as `swarm-launch` says, then `plan.mjs done` and `plan.mjs land`; a report that is not ok goes back through `plan.mjs replan` and the approval gate above, or by hand if the user says so — for a batch, "by hand" dissolves it: the first card continues by hand at step 3, and every other card of the batch goes back to Ready with `board.mjs move <id> ready`.
|
|
56
58
|
|
|
@@ -64,7 +66,7 @@ The session model is the lead and only the lead, on both tracks; it never implem
|
|
|
64
66
|
|
|
65
67
|
First the research step when step 1 called for one: invoke the `research` skill. It names the agent (claude-code-guide for Claude Code questions, the plugin's read-only `researcher` agent otherwise), runs several researchers in parallel when there are several distinct questions, and writes one cited note to `.doug/.state/research/<id>.md`; read it before writing the brief, and treat a line marked unverified as a fact the code must not depend on.
|
|
66
68
|
|
|
67
|
-
Then recall memory for the card: `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" brief-lessons <id>`, and paste its output verbatim, header, lesson lines with their ids, and `memoryUsed` line, into every tester, coder, and reviewer brief. When it prints no lessons, or exits nonzero, say so in the chat and go on: recall never blocks a card. Save each brief to `.doug/.state/reports/<id>/briefs/<n>-<seat>.md` before spawning its agent, and each reviewer report to `.doug/.state/reports/<id>/reviews/<n>.md` when it returns.
|
|
69
|
+
Then recall memory for the card: `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" brief-lessons <id>`, and paste its output verbatim, header, lesson lines with their ids, and `memoryUsed` line, into every tester, coder, and reviewer brief. Then fetch the full text of each indexed lesson the card needs with `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" lesson show <id> --card <id>` and paste each output verbatim below that block in the same briefs; never cut or paraphrase a fetched lesson. When it prints no lessons, or exits nonzero, say so in the chat and go on: recall never blocks a card. Save each brief to `.doug/.state/reports/<id>/briefs/<n>-<seat>.md` before spawning its agent, and each reviewer report to `.doug/.state/reports/<id>/reviews/<n>.md` when it returns.
|
|
68
70
|
|
|
69
71
|
On the lean lane and on a tests-only card, every brief carries the same `brief-lessons` block.
|
|
70
72
|
|
|
@@ -84,6 +86,8 @@ Before the commit the lead itself runs `node .doug/hooks/scripts/verify.mjs` (th
|
|
|
84
86
|
|
|
85
87
|
Stay within the card's goal. A defect found on the way that the goal does not cover becomes a new hand-track card, not part of this commit.
|
|
86
88
|
|
|
89
|
+
When a by-hand run stops a card after Start (a hook block it reports, a design question answered stop, a gate it cannot get green, an unresolved reviewer blocker, or the user saying stop), the lead runs the recording step once: `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" record <id> --hand --stop <reason> --detail "<one line>"`. `<reason>` is one of the card loop's stop reasons (`lib/card-loop.mjs` `STOP_REASONS`) or `user-stopped` when the user stopped it.
|
|
90
|
+
|
|
87
91
|
## 4. Record and land
|
|
88
92
|
|
|
89
93
|
With the commit sha (the merge sha for a swarmed card) and the wall clock since step 1 (or since 2b's launch, for a swarmed card):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doug-learn
|
|
3
|
-
description: Log-then-propose self-improvement (card learn-signals). Counts signals from the outcomes/lessons store and the run trace, renders each candidate change - demote a violated prose rule to a hook, delete a rule nothing references, promote a
|
|
3
|
+
description: Log-then-propose self-improvement (card learn-signals). Counts signals from the outcomes/lessons store and the run trace, renders each candidate change - demote a violated prose rule to a hook, delete a rule nothing references, promote a pitfall lesson recorded for 3 cards over 2 sessions (as a path-scoped `.claude/rules` file, or a CLAUDE.md Gotchas bullet when no files are known), tighten a skill description - as a diff, and asks the user to approve each one before applying it. Only the user invokes this; it never applies a change on its own.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
allowed-tools: AskUserQuestion, Read, Bash(node *learn.mjs *), Bash(git *)
|
|
6
6
|
---
|
|
@@ -33,6 +33,8 @@ If `.doug/plan.json` is already a draft whose `card` is this id (or whose `cards
|
|
|
33
33
|
|
|
34
34
|
A size L card with no `docs/specs/<card-id>/spec.md` is offered feature mode first (ADR 0012): before the planner, ask with `AskUserQuestion` whether to run `/doug-start <id>` (specification, optional pseudocode, architecture, refinement) or plan now; `/doug-start <id>` ends this loop and the card stays In flow. S and M cards go straight to the plan, and any card can still run `/doug-start <id>` on request. Under autopilot the offer is not asked and the card goes straight to the plan.
|
|
35
35
|
|
|
36
|
+
Batch: ask the offer for each such card before any planner is spawned; a card answered `/doug-start <id>` is dropped from the batch and stays In flow for `/doug-start <id>`, and the rest of the batch goes on (one card left is a plain single-card run; none left ends this loop).
|
|
37
|
+
|
|
36
38
|
If the card's goal depends on a fact outside the repository (a Claude Code hook contract, a CLI's output format, a third-party API), invoke the `research` skill first and add `Research note: <path>` to the planner's prompt; the planner writes the facts it relied on into the plan's goal with their sources.
|
|
37
39
|
|
|
38
40
|
The planner runs on the `plan` row (see "Who does the work" above): read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.plan.model`, `roles.plan.effort`), and pass that row's model on every planner spawn below (omit `model` when the row says `inherit`).
|
|
@@ -19,7 +19,8 @@ The Walking skeleton writes no phase file; it is done when the `walking-skeleton
|
|
|
19
19
|
|
|
20
20
|
- Only a committed phase file counts as done: a phase file that exists but is uncommitted means that phase is still waiting at its gate, so the next run re-asks that gate, showing the file, and does not move on.
|
|
21
21
|
- A phase file is written by the lead (this session). The spec-writer and researcher agents return text and write nothing.
|
|
22
|
-
- Before writing a phase, check whether its file exists. If it does, write the new text to `.doug/.state/scratch/<name>.new`, show `git diff --no-index` of the two, and ask (AskUserQuestion: replace / keep / revise). Never overwrite a phase file without that answer.
|
|
22
|
+
- Before writing a phase, check whether its file exists. If it does, write the new text to `.doug/.state/scratch/<name>.new`, show `git diff --no-index` of the two in the chat, in a fenced code block, before the AskUserQuestion, and ask (AskUserQuestion: replace / keep / revise). Never overwrite a phase file without that answer.
|
|
23
|
+
- Show the file first: before every gate that asks about a phase file, put the file's complete text in the chat, verbatim, in a fenced code block, before the AskUserQuestion. Never replace it with a summary or a path, and never truncate it (a very long file may go over more than one message). After the block the lead may add a few lines (assumptions, open questions); the questions themselves go in the AskUserQuestion.
|
|
23
24
|
- The gate after each phase asks AskUserQuestion with the options approve / revise / stop. Approve: commit the phase file (a plain message, no trailers) and go to the next phase. Revise: take the user's note, redraft, show the diff, ask again. Stop: leave the file uncommitted and end; a later `/doug-start` resumes at the first missing phase file.
|
|
24
25
|
- At every gate the lead calls AskUserQuestion, even when the session looks non-interactive; a denied or unanswered call ends the phase there, and the lead does nothing past it (it names the phase that is waiting).
|
|
25
26
|
|
|
@@ -31,7 +32,7 @@ Start here: if `.doug/config.json` is missing, run `doug init --yes` first. It w
|
|
|
31
32
|
|
|
32
33
|
Writes `docs/product/idea.md` (problem, users, success, non-goals). Spawn the spec-writer with the user's idea and ask it for those four sections:
|
|
33
34
|
`Agent({ subagent_type: "doug-flow:spec-writer", prompt: "<the user's idea verbatim>. Draft docs/product/idea.md with sections: problem, users, success, non-goals. Return the file text." })`
|
|
34
|
-
Write the file, then ask at the AskUserQuestion gate (approve / revise / stop).
|
|
35
|
+
Write the file, show the file first (Phase rules), then ask at the AskUserQuestion gate (approve / revise / stop).
|
|
35
36
|
|
|
36
37
|
### 2. Architecture
|
|
37
38
|
|
|
@@ -39,7 +40,7 @@ Writes `docs/product/architecture.md` plus the ADR `docs/decisions/0001-stack.md
|
|
|
39
40
|
`Agent({ subagent_type: "doug-flow:researcher", prompt: "<one stack question>. Cite each fact with its source." })`
|
|
40
41
|
Then spawn the architect on the `plan` row's model (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models`; omit `model` when it says `inherit`):
|
|
41
42
|
`Agent({ subagent_type: "architect", model: "<the plan row's model>", prompt: "<the approved idea and the research>. Return the text of docs/product/architecture.md and, separately, the body of the stack ADR without its title line." })`
|
|
42
|
-
Write `docs/product/architecture.md` from its returned text. `docs/decisions/` is proposal-only, so the lead never Writes it directly. Save the ADR body under `.doug/.state/scratch/` and run `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" decision propose --title "Stack" --file <body>`, which renders the diff for `docs/decisions/0001-stack.md` under `.doug/.state/learn/` and writes nothing else.
|
|
43
|
+
Write `docs/product/architecture.md` from its returned text. `docs/decisions/` is proposal-only, so the lead never Writes it directly. Save the ADR body under `.doug/.state/scratch/` and run `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" decision propose --title "Stack" --file <body>`, which renders the diff for `docs/decisions/0001-stack.md` under `.doug/.state/learn/` and writes nothing else. Show the file first (Phase rules) for `architecture.md` and for the proposal's ADR diff, then ask at the AskUserQuestion gate (approve / revise / stop). On approve, run `node "${CLAUDE_PLUGIN_ROOT}/scripts/learn.mjs" apply <proposal-file>`, which writes the ADR, and commit. Unapproved, the proposal stays pending and the ADR does not exist.
|
|
43
44
|
|
|
44
45
|
### 3. Walking skeleton
|
|
45
46
|
|
|
@@ -49,7 +50,7 @@ Writes the first card with `doug board add` (id `walking-skeleton`, size S, clas
|
|
|
49
50
|
|
|
50
51
|
Writes `docs/product/scope.md` (v1 in one paragraph, then one line per card) and the board cards. Spawn the spec-writer with the approved idea and architecture; it returns the text of `scope.md` and the card list as one line per card (id, title, goal, size, class, column):
|
|
51
52
|
`Agent({ subagent_type: "doug-flow:spec-writer", prompt: "<idea and architecture paths>. Draft docs/product/scope.md and its card list: id, title, goal, size, class, column. Return the file text." })`
|
|
52
|
-
The lead never drafts the scope; it writes the returned `scope.md` and runs `doug board add` only for each card the user approved. A card counts as approved at the gate, or in advance in the conversation. Cards are written with `doug board add`, never by a script editing `.doug/board.json`. Put at most 7 to Ready for v1 and the rest to Backlog, each with a size; a large card then goes through feature mode (`/doug-start <card-id>`). Show the board (`doug board list`) and ask at the AskUserQuestion gate (approve / revise / stop).
|
|
53
|
+
The lead never drafts the scope; it writes the returned `scope.md` and runs `doug board add` only for each card the user approved. A card counts as approved at the gate, or in advance in the conversation. Cards are written with `doug board add`, never by a script editing `.doug/board.json`. Put at most 7 to Ready for v1 and the rest to Backlog, each with a size; a large card then goes through feature mode (`/doug-start <card-id>`). Show the file first (Phase rules) for `scope.md`, show the board (`doug board list`) and ask at the AskUserQuestion gate (approve / revise / stop).
|
|
53
54
|
|
|
54
55
|
## Feature mode
|
|
55
56
|
|
|
@@ -59,21 +60,21 @@ Files live under `docs/specs/<card-id>/`. A goal with no card id gets a short ke
|
|
|
59
60
|
|
|
60
61
|
Writes `spec.md`: what must be true when the work is done, as outcomes a command can check, plus non-goals and open questions.
|
|
61
62
|
`Agent({ subagent_type: "doug-flow:spec-writer", prompt: "<card goal or the user's goal verbatim>. Draft docs/specs/<card-id>/spec.md: outcomes, non-goals, open questions. Return the file text." })`
|
|
62
|
-
Write the file, then ask at the AskUserQuestion gate (approve / revise / stop).
|
|
63
|
+
Write the file, show the file first (Phase rules), then ask at the AskUserQuestion gate (approve / revise / stop).
|
|
63
64
|
|
|
64
65
|
### 2. Pseudocode (optional)
|
|
65
66
|
|
|
66
|
-
Writes `pseudocode.md`, only when the user asks for it at this gate (AskUserQuestion: write it / skip). Skip leaves no file and goes to Architecture; a missing `pseudocode.md` counts as skipped once `architecture.md` exists, or when the user said they skipped it; resuming with only `spec.md` asks write / skip once. When written, spawn the spec-writer again for the algorithm in plain steps, and gate it as above.
|
|
67
|
+
Writes `pseudocode.md`, only when the user asks for it at this gate (AskUserQuestion: write it / skip). Skip leaves no file and goes to Architecture; a missing `pseudocode.md` counts as skipped once `architecture.md` exists, or when the user said they skipped it; resuming with only `spec.md` asks write / skip once. When written, spawn the spec-writer again for the algorithm in plain steps, and gate it as above: show the file first (Phase rules).
|
|
67
68
|
|
|
68
69
|
### 3. Architecture
|
|
69
70
|
|
|
70
|
-
Writes `architecture.md` in the shape of the architect agent: modules, files, interfaces, the test for each. Every file it names will be owned by a plan task, so name each exactly. Spawn the project's `architect` agent on the `plan` row's model with `spec.md` (and `pseudocode.md` if present) and the code it touches: `Agent({ subagent_type: "architect", model: "<the plan row's model>", prompt: "<the spec path>. Return the text of docs/specs/<card-id>/architecture.md." })`. The lead writes its returned text to the file and never drafts it.
|
|
71
|
+
Writes `architecture.md` in the shape of the architect agent: modules, files, interfaces, the test for each. Every file it names will be owned by a plan task, so name each exactly. Spawn the project's `architect` agent on the `plan` row's model with `spec.md` (and `pseudocode.md` if present) and the code it touches: `Agent({ subagent_type: "architect", model: "<the plan row's model>", prompt: "<the spec path>. Return the text of docs/specs/<card-id>/architecture.md." })`. The lead writes its returned text to the file and never drafts it. Show the file first (Phase rules), then ask at the AskUserQuestion gate (approve / revise / stop).
|
|
71
72
|
|
|
72
73
|
### 4. Refinement
|
|
73
74
|
|
|
74
75
|
The planner writes an approvable plan whose tasks own the files `architecture.md` names. Read the `plan` row with `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` and pass its model (omit `model` when it says `inherit`):
|
|
75
76
|
`Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "<card goal>. Spec: docs/specs/<card-id>/spec.md. Architecture: docs/specs/<card-id>/architecture.md. Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts. Write .doug/plan.json as a draft and return the rendered plan." })`
|
|
76
|
-
Set the card when the argument was a board card id (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set card <card-id>`), render it (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" show`) and ask at the AskUserQuestion gate (approve / revise / stop). The plan is approved with `/doug-approve`, by the user; this skill never approves it.
|
|
77
|
+
Set the card when the argument was a board card id (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set card <card-id>`), render it (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" show`), show the file first (Phase rules), meaning the plan's complete `show` output in the chat, and ask at the AskUserQuestion gate (approve / revise / stop). The plan is approved with `/doug-approve`, by the user; this skill never approves it.
|
|
77
78
|
|
|
78
79
|
### 5. Completion
|
|
79
80
|
|
|
@@ -22,7 +22,7 @@ Put the card's title and goal (or the goal text) in the chat verbatim, in a fenc
|
|
|
22
22
|
|
|
23
23
|
## 2. Plan on the plan row
|
|
24
24
|
|
|
25
|
-
The planner runs on the `plan` row, the same row `doug-plan` uses, not the `lead` row (the `lead` row is the swarm's own lead agent inside the workflow, a different job). Read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.plan.model`, `roles.plan.effort`). Spawn the planner on it: `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<title>\": <goal>. This plan runs as a swarm: set \"swarm\": true in the plan file, and size every task M or L unless it is truly one bounded change (a size-S task keeps a single implementer). Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts. Write .doug/plan.json and return the rendered plan." })` (for a goal, the prompt is the goal text with the same swarm sentence); omit `model` when the row says `inherit`. The planner's rules apply unchanged: disjoint file ownership per task, a verify command per task, acceptance criteria with commands, no tier set on its own.
|
|
25
|
+
The planner runs on the `plan` row, the same row `doug-plan` uses, not the `lead` row (the `lead` row is the swarm's own lead agent inside the workflow, a different job). Read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.plan.model`, `roles.plan.effort`). Spawn the planner on it: `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<title>\": <goal>. This plan runs as a swarm: set \"swarm\": true in the plan file, and size every task M or L unless it is truly one bounded change (a size-S task keeps a single implementer). The shape gate keeps a task off the lead unless it owns two or more source files (not counting its tests), and a size-S task never reaches the lead, so give each M or L task two or more source files. Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts. Write .doug/plan.json and return the rendered plan." })` (for a goal, the prompt is the goal text with the same swarm sentence); omit `model` when the row says `inherit`. The planner's rules apply unchanged: disjoint file ownership per task, a verify command per task, acceptance criteria with commands, no tier set on its own.
|
|
26
26
|
|
|
27
27
|
Then guarantee the opt-in and the card (`plan.mjs set swarm on`): `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set swarm on`, and `plan.mjs set card <id>` for a card. `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" show` and put its complete output in the chat verbatim, in a fenced code block, before anything else; it prints `Swarm: on`. Name any task the planner sized S (it will not swarm) and any assumption it stated.
|
|
28
28
|
|
|
@@ -43,7 +43,7 @@ With the card id, the landing commit, the wall clock, and the gate line — one
|
|
|
43
43
|
1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> --hand --commit <sha> --wall "<m> min" --gate "<gate line>" --summary "<one sentence>" --cost <usd>`, with `--note "<one line>"` when something about the build is worth keeping. `<usd>` is the card's measured cost, never an estimate: `node "${CLAUDE_PLUGIN_ROOT}/scripts/cost.mjs" --session <session> --since <openedAt>` prices the lead session and its subagents from the card's `openedAt` (in `.doug/.state/reports/<id>/condition.json`) to now, where `<session>` is `CLAUDE_CODE_SESSION_ID` (not the card id); take the session total's dollar figure. When nothing was measured (no condition.json, no session id, or cost.mjs exits non-zero), leave `--cost` out: the entry then has no Cost row, never a `$0`, and you never estimate one. This appends the hand-track entry to `docs/live-runs.md`; its table is the summary. It also promotes `.doug/.state/research/<id>.md` to `docs/research/<id>.md` when that note exists, printing one line saying so; add that path to the landing commit alongside `docs/live-runs.md`.
|
|
44
44
|
2. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" import`, the same as above: its one-line output goes in the chat, an exit 1 naming Node is said in the chat and the landing goes on, and it is idempotent, supersedes a rewritten file, and never writes under the auto-memory directory.
|
|
45
45
|
3. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" record <id> --hand --commit <sha> --wall "<m> min" --gate "<gate line>" [--cost <usd>] [--note "<one line>"]`, the same values step 1 used, leaving `--cost` out when step 1 did. It records one row in the local outcome log `.doug/.state/memory/memory.db` (uncommitted), keyed `hand:<sha>` so re-recording the same commit updates that row and track `hand`; a card that lands twice, in two commits, gets two rows. Its workflow-only columns (verified, review counts, adversary verdict, blocks, fix passes, budget, tokens, the report itself) are null because they do not exist on this track, never because they measured zero: no independent verifier ran, and the gate's own text is the `gate` column instead. The USD column is the measured `--cost`, null when none was given. It needs Node >=22.16 (node:sqlite). When it exits 1 naming Node, say so in the chat and go on, since the record is a log, not a gate.
|
|
46
|
-
4. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect --hand <id> --commit <sha> --rounds <n> [--review "<key>=<class>: <reason>"]... (the key is "`R<n>` or `<card>/R<n>`") [--harmful <id>]... [--note "<one line>"]`. It reads the briefs, reviewer reports and lessons.json doug-hand saved, applies helpful counters to the card's memoryUsed. A real block no longer bumps harmful; pass `--harmful <id>` once per recalled lesson the lead judges actually misled the work (it must be in a task's `memoryUsed`, else exit 1). Give `--harmful` on the report's first reflect: a rerun is "Already reflected" and ignores it. It proposes at most three lessons on the cheap row, filtered in code. 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
|
+
4. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" reflect --hand <id> --commit <sha> --rounds <n> [--review "<key>=<class>: <reason>"]... (the key is "`R<n>` or `<card>/R<n>`") [--harmful <id>]... [--note "<one line>"]`. It reads the briefs, reviewer reports and lessons.json doug-hand saved, applies helpful counters to the card's memoryUsed (only lessons fetched with `lesson show <id> --card <id>`). A real block no longer bumps harmful; pass `--harmful <id>` once per recalled lesson the lead judges actually misled the work (it must be in a task's `memoryUsed`, else exit 1). Give `--harmful` on the report's first reflect: a rerun is "Already reflected" and ignores it. It proposes at most three lessons on the cheap row, filtered in code. 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.
|
|
47
47
|
|
|
48
48
|
Say the one-sentence summary record printed first, then the commit, the wall clock, and the gate line in the chat. In the hand form the landing block is printed by `board.mjs record` (both forms) and by `board.mjs summary --card` as their last three lines (Changed, Outcome, Learned; at most 100 words, `--note` feeds Learned); plain `summary` (no `--card`) prints no block. Your final message carries it verbatim, and the Stop gate blocks until it does.
|
|
49
49
|
|
|
@@ -75,7 +75,11 @@ const BRIEF_RULES = {
|
|
|
75
75
|
seats: ["tester", "coder", "reviewer"],
|
|
76
76
|
},
|
|
77
77
|
"lessons-verbatim": {
|
|
78
|
-
text: "
|
|
78
|
+
text: "Run memory.mjs brief-lessons <card-id> <repo-root>, passing the REPO ROOT named in this brief as [dir], and return its block for this card verbatim in lessons; never paraphrase it.",
|
|
79
|
+
seats: ["lead"],
|
|
80
|
+
},
|
|
81
|
+
"lessons-fetch": {
|
|
82
|
+
text: "For each indexed lesson the card needs, run memory.mjs lesson show <id> <repo-root> --card <card-id>, passing the REPO ROOT named in this brief as [dir], and append its output verbatim to lessons below the brief-lessons block; never cut or paraphrase a fetched lesson.",
|
|
79
83
|
seats: ["lead"],
|
|
80
84
|
},
|
|
81
85
|
"facts-cited": {
|
|
@@ -120,7 +124,7 @@ const BRIEF_RULES = {
|
|
|
120
124
|
},
|
|
121
125
|
"no-trailers": {
|
|
122
126
|
text: "Commit without Co-Authored-By or Claude-Session trailers.",
|
|
123
|
-
seats: ["commit"],
|
|
127
|
+
seats: ["tester", "coder", "reviewer", "commit"],
|
|
124
128
|
},
|
|
125
129
|
"commit-foreground": {
|
|
126
130
|
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.",
|
|
@@ -138,6 +142,11 @@ const BRIEF_RULES = {
|
|
|
138
142
|
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.",
|
|
139
143
|
seats: ["gate"],
|
|
140
144
|
},
|
|
145
|
+
"gate-reinstall-missing-deps": {
|
|
146
|
+
text: "Before the gate commands, check that node_modules exists in the card worktree. Only when it is missing, run the INSTALL COMMAND and report it as the first commands item, then run the gate. Never install when node_modules is present.",
|
|
147
|
+
seats: ["gate"],
|
|
148
|
+
when: "install",
|
|
149
|
+
},
|
|
141
150
|
"gate-timeout-is-load": {
|
|
142
151
|
text: "When verify.mjs prints `exit timeout` for a command, report that command with timedOut: true; a timeout is load, not a red or a block, so leave blocked unset.",
|
|
143
152
|
seats: ["gate"],
|
|
@@ -422,6 +431,7 @@ const effectiveMutations = (s) => (s.lane === "lean" ? [] : s.material.mutations
|
|
|
422
431
|
|
|
423
432
|
function ruleApplies(rule, seat, s) {
|
|
424
433
|
if (!rule.seats.includes(seat)) return false;
|
|
434
|
+
if (rule.when === "install") return s.install !== null && s.install !== undefined;
|
|
425
435
|
if (rule.when === "full") return s.lane === "full";
|
|
426
436
|
if (rule.when === "tests-only") return s.lane === "tests-only";
|
|
427
437
|
if (rule.when === "mutations") {
|
|
@@ -456,7 +466,12 @@ function buildBrief(s) {
|
|
|
456
466
|
gate: fixing && s.fixFor === "gate" ? cp(s.lastGate) : null,
|
|
457
467
|
redTests: cp(s.redTests),
|
|
458
468
|
answers: (s.answers || [])
|
|
459
|
-
.filter(
|
|
469
|
+
.filter(
|
|
470
|
+
(a) =>
|
|
471
|
+
(a.seat === seat && (a.round === undefined || a.round <= (s.seatRuns[seat] || 0) + 1)) ||
|
|
472
|
+
// A reviewer answer reaches the build seat once the reviewer round it targets has run.
|
|
473
|
+
(seat === buildSeat(s) && a.seat === "reviewer" && (a.round ?? 1) <= (s.seatRuns.reviewer || 0)),
|
|
474
|
+
)
|
|
460
475
|
.map((a) => ({ question: a.question, answer: a.answer })),
|
|
461
476
|
writeTestsFirst: seat === "coder" && s.counters.coderRounds === 0 && (s.lane === "lean" || !s.testerAgent),
|
|
462
477
|
researchNote: s.researchNote,
|
|
@@ -466,6 +481,7 @@ function buildBrief(s) {
|
|
|
466
481
|
brief.install = s.install;
|
|
467
482
|
brief.base = s.base;
|
|
468
483
|
}
|
|
484
|
+
if (seat === "gate" && s.install) brief.install = s.install;
|
|
469
485
|
if (seat === "gate" || seat === "reviewer") brief.gateCommand = s.gateCommand;
|
|
470
486
|
if (seat === "commit") brief.gateTree = s.gateTree;
|
|
471
487
|
return brief;
|
|
@@ -854,6 +870,18 @@ function next(state, handBack) {
|
|
|
854
870
|
// An evidenced major gets the same one fix pass; a major without evidence and a minor stay advisory.
|
|
855
871
|
const majors = findings.filter((f) => f.severity === "major" && nonBlank(f.evidence));
|
|
856
872
|
if (blockers.length === 0 && majors.length === 0) return advance(s, "gate", "ok", { acc });
|
|
873
|
+
// A reviewer round that carried a user answer and still has an open finding gets one extra coder round.
|
|
874
|
+
const answered = (s.answers || []).some(
|
|
875
|
+
(a) => a.seat === "reviewer" && (a.round ?? 1) <= (s.seatRuns.reviewer || 0),
|
|
876
|
+
);
|
|
877
|
+
if (s.counters.fixPasses >= 1 && answered && !s.answerCoderUsed) {
|
|
878
|
+
return advance(s, buildSeat(s), "blockers", {
|
|
879
|
+
acc,
|
|
880
|
+
fixFor: "review",
|
|
881
|
+
answerCoderUsed: true,
|
|
882
|
+
lastFindings: findings.filter((f) => f.severity !== "minor"),
|
|
883
|
+
});
|
|
884
|
+
}
|
|
857
885
|
if (s.counters.fixPasses >= 1) {
|
|
858
886
|
if (blockers.length > 0) {
|
|
859
887
|
return stopWith(
|
|
@@ -879,7 +907,16 @@ function next(state, handBack) {
|
|
|
879
907
|
const lastGate = { commands: cp(H.commands), gateLine: H.gateLine };
|
|
880
908
|
const waited = { ...s.acc, lockWaitMs: (s.acc.lockWaitMs || 0) + (Number.isInteger(H.lockWaitMs) ? H.lockWaitMs : 0) };
|
|
881
909
|
s.acc = waited;
|
|
882
|
-
|
|
910
|
+
const isInstall = (c) => s.install && typeof c.command === "string" && c.command.trim() === `cd ${s.worktree} && ${s.install}`;
|
|
911
|
+
const failedInstall = H.commands.find((c) => isInstall(c) && c.exitCode !== 0);
|
|
912
|
+
if (failedInstall) {
|
|
913
|
+
return stopWith({ ...s, lastGate }, "setup-failed", `gate reinstall exited ${failedInstall.exitCode === null ? "without a code" : failedInstall.exitCode}`);
|
|
914
|
+
}
|
|
915
|
+
const nonInstall = H.commands.filter((c) => !isInstall(c));
|
|
916
|
+
if (nonInstall.every((c) => c.exitCode === 0)) {
|
|
917
|
+
if (!nonInstall.some((c) => typeof c.command === "string" && c.command.includes(s.gateCommand))) {
|
|
918
|
+
return stopWith({ ...s, lastGate }, "hand-back-invalid", `gate hand-back has no run of the gate command ${s.gateCommand}`);
|
|
919
|
+
}
|
|
883
920
|
return advance(s, "commit", "ok", { lastGate, gateTree: H.tree, acc: { ...waited, gateLine: H.gateLine } });
|
|
884
921
|
}
|
|
885
922
|
const redGates = s.counters.redGates + 1;
|
|
@@ -918,15 +955,17 @@ function briefPrompt(brief, { repoRoot, worktreePath } = {}) {
|
|
|
918
955
|
out.push("ANSWERS FROM THE USER (to your design questions; build on them):\n" + bullets(b.answers.map((a) => `Q: ${a.question} A: ${a.answer}`)));
|
|
919
956
|
}
|
|
920
957
|
out.push("WORKTREE: " + worktreePath);
|
|
958
|
+
if (b.seat === "lead" && typeof repoRoot === "string" && repoRoot !== "") out.push("REPO ROOT: " + repoRoot);
|
|
921
959
|
if (b.seat === "setup") {
|
|
922
960
|
out.push(
|
|
923
961
|
"SETUP:\n" +
|
|
924
962
|
`Create the card worktree: git -C ${repoRoot} worktree add -b ${b.branch} ${worktreePath} ${b.base}\n` +
|
|
925
963
|
(b.install
|
|
926
|
-
? `Then run the install command
|
|
964
|
+
? `Then run the install command as this one literal command: cd ${worktreePath} && ${b.install}`
|
|
927
965
|
: "There is no install command; skip the install and report install as null."),
|
|
928
966
|
);
|
|
929
967
|
}
|
|
968
|
+
if (b.seat === "gate" && b.install) out.push(`INSTALL COMMAND: cd ${worktreePath} && ${b.install}`);
|
|
930
969
|
if (b.researchNote) out.push("RESEARCH NOTE:" + (b.researchNote.includes("\n") ? "\n" : " ") + b.researchNote);
|
|
931
970
|
if (b.lessons) out.push("LESSONS BLOCK verbatim:\n" + b.lessons);
|
|
932
971
|
if (b.facts.length > 0) out.push("FACTS (with sources):\n" + bullets(b.facts.map((f) => `${f.text} [${f.source}]`)));
|
|
@@ -3,7 +3,7 @@ export const meta = {
|
|
|
3
3
|
description: 'Implement an approved Doug plan: one implementer per task in its own worktree, then verify, review, and integrate',
|
|
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
|
-
{ 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
|
|
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 rebuilds the own commits of its recorded branch on the current base 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
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' },
|
|
@@ -117,7 +117,7 @@ const roleModels = (plan.models && plan.models.roles) || {}
|
|
|
117
117
|
// The adversary row may name "codex": the review runs on Codex (plan.adversary.command) and the Claude agent
|
|
118
118
|
// that launches it and relays the verdict is only a relay, so it runs on haiku. `relay` is that agent's model.
|
|
119
119
|
const ADVERSARY_RELAY_MODEL = 'haiku'
|
|
120
|
-
//
|
|
120
|
+
// Rebuilding a reused branch is a scripted run of listed git commands, so it runs on the same small model as the adversary relay.
|
|
121
121
|
const CHECKOUT_MODEL = ADVERSARY_RELAY_MODEL
|
|
122
122
|
// The fix-loop supervisor (card fix-loop-supervisor) runs on the `cheap` tier of the Models table; a table without
|
|
123
123
|
// one puts it on the relay model, since reading a task's passes and writing a brief is small work.
|
|
@@ -706,6 +706,10 @@ function droppedWhy(f, task) {
|
|
|
706
706
|
// Applies one pass to the ledger, stage by stage in the order the stages ran, and returns the ids that opened,
|
|
707
707
|
// were confirmed fixed, and came back. A stage confirms an open entry by running and not reporting it: its own
|
|
708
708
|
// stage, the focused check, and the confirmation adversary all count, and only on a pass that made a new commit.
|
|
709
|
+
function explicitMinor(f) {
|
|
710
|
+
return ['minor', 'nit', 'note'].includes(String(f.severity == null ? '' : f.severity).trim().toLowerCase())
|
|
711
|
+
}
|
|
712
|
+
|
|
709
713
|
function updateLedger(ledger, r, pass, newCommit) {
|
|
710
714
|
const opened = []
|
|
711
715
|
const fixed = []
|
|
@@ -734,7 +738,7 @@ function updateLedger(ledger, r, pass, newCommit) {
|
|
|
734
738
|
if (e.status !== 'open' || e.openedPass === pass || touched.has(e.id)) continue
|
|
735
739
|
const covers = e.stage === stage || stage === 'check' || (stage === 'adversary' && pass > 1)
|
|
736
740
|
if (!covers) continue
|
|
737
|
-
if (reReports.some(f => matchFinding(ledger, f) === e)) continue
|
|
741
|
+
if (reReports.some(f => !explicitMinor(f) && matchFinding(ledger, f) === e)) continue
|
|
738
742
|
e.status = 'fixed'
|
|
739
743
|
e.fixedPass = pass
|
|
740
744
|
e.fixedCommit = r.commit || null
|
|
@@ -745,6 +749,10 @@ function updateLedger(ledger, r, pass, newCommit) {
|
|
|
745
749
|
for (const f of reReports) {
|
|
746
750
|
const entry = matchFinding(ledger, f)
|
|
747
751
|
if (entry) {
|
|
752
|
+
// Card stall-on-repeat-minor (2026-10-25 decision, run wf_d9199b24-d9e): a re-report whose severity is
|
|
753
|
+
// explicitly minor/nit/note neither reopens a fixed entry nor keeps an open one open. Free text and
|
|
754
|
+
// blocker/major re-reports (and severity-less ones) still do.
|
|
755
|
+
if (explicitMinor(f)) continue
|
|
748
756
|
touched.add(entry.id)
|
|
749
757
|
if (f.evidence) entry.evidence = f.evidence
|
|
750
758
|
if (entry.status === 'fixed') {
|
|
@@ -848,7 +856,7 @@ function implementPrompt(task, baseBranch, opts) {
|
|
|
848
856
|
`Task ${task.id}: ${task.title}`,
|
|
849
857
|
`Spec:`,
|
|
850
858
|
task.spec,
|
|
851
|
-
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, String(task.lessons)
|
|
859
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, String(task.lessons)] : []),
|
|
852
860
|
...(typeof task.codeContext === 'string' && task.codeContext ? ['', `Code the semantic index found relevant to this task (path:lines; read these first, they are not the files you own):`, String(task.codeContext).slice(0, 2000)] : []),
|
|
853
861
|
``,
|
|
854
862
|
`Files you own (you may create or edit ONLY these): ${task.files.join(', ')}`,
|
|
@@ -876,11 +884,17 @@ function reusePrompt(task, baseBranch) {
|
|
|
876
884
|
`Files the task owns: ${task.files.join(', ')}`,
|
|
877
885
|
``,
|
|
878
886
|
`Steps, all inside your own worktree:`,
|
|
879
|
-
`1.
|
|
887
|
+
`1. Put ${task.reuse} on the current base ${baseBranch}, because the earlier pass may have been cut from an integration branch that no longer exists. If any git checkout below refuses because the branch is checked out in another worktree, that worktree is a leftover of the earlier pass under .claude/worktrees: find it with git worktree list --porcelain, remove it with git worktree remove --force <path>, and run the checkout again.`,
|
|
888
|
+
` a. List the branch's own commits: git log --no-merges --reverse --format=%H ${baseBranch}..${task.reuse}`,
|
|
889
|
+
` b. For each, read its files with git show --name-only --format= <sha>. A commit with no files is dropped. A commit touching only files the task does not own belongs to another task: drop it. A commit touching both files the task owns and files it does not own means the branch carries work the task does not own: change nothing and return blocked=true with a reason naming the commit and the file. A commit touching only owned files is kept.`,
|
|
890
|
+
` Then run git merge-base --is-ancestor ${baseBranch} ${task.reuse}. If it fails with exit 128 (the branch or the base does not exist), return blocked=true naming the missing one. When it exits 0 and step b kept every listed commit, the branch already sits on the current base: just git checkout ${task.reuse} and go to step 2. Otherwise rebuild it:`,
|
|
891
|
+
` c. git checkout --detach ${baseBranch}`,
|
|
892
|
+
` d. git cherry-pick -x each kept commit, in order. When a pick is empty because the base already has the change, run git cherry-pick --skip. On a conflict, run git cherry-pick --abort and return blocked=true with a reason naming the commit and the conflicting files (the branch ref was never moved, so the reviewed work stays intact).`,
|
|
893
|
+
` e. git checkout -B ${task.reuse} to point the branch at the rebuilt HEAD (no base argument).`,
|
|
880
894
|
plan.install
|
|
881
895
|
? `2. Run the project's install command (${plan.install}) before the checks run. Installing is not a file change; do not commit anything it changes. Should you commit at all, ${NO_TRAILERS}`
|
|
882
896
|
: `2. If the task's verification command needs dependencies, run the project's install command. Installing is not a file change; do not commit anything it changes. Should you commit at all, ${NO_TRAILERS}`,
|
|
883
|
-
`3. Return taskId="${task.id}", branch="${task.reuse}", worktreePath = the absolute path of your worktree (git rev-parse --show-toplevel), filesTouched = the output of git diff --name-only ${baseBranch}...HEAD, commandsRun with real exit statuses, commit = the output of git rev-parse HEAD, summary "reused ${task.reuse} at <commit>", blocked=false. Report blocked=true with the reason only if the branch does not exist
|
|
897
|
+
`3. Return taskId="${task.id}", branch="${task.reuse}", worktreePath = the absolute path of your worktree (git rev-parse --show-toplevel), filesTouched = the output of git diff --name-only ${baseBranch}...HEAD, commandsRun with real exit statuses, commit = the output of git rev-parse HEAD, summary "reused ${task.reuse} at <commit>", blocked=false. Report blocked=true with the reason only if the branch does not exist, cannot be checked out, or step 1 told you to.`,
|
|
884
898
|
].join('\n')
|
|
885
899
|
}
|
|
886
900
|
|
|
@@ -902,7 +916,7 @@ function leadPrompt(task, baseBranch, opts) {
|
|
|
902
916
|
`Task ${task.id}: ${task.title}`,
|
|
903
917
|
`Spec:`,
|
|
904
918
|
task.spec,
|
|
905
|
-
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, String(task.lessons)
|
|
919
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, String(task.lessons)] : []),
|
|
906
920
|
...(typeof task.codeContext === 'string' && task.codeContext ? ['', `Code the semantic index found relevant to this task (path:lines; read these first, they are not the files you own):`, String(task.codeContext).slice(0, 2000)] : []),
|
|
907
921
|
``,
|
|
908
922
|
`Files the task owns: ${task.files.join(', ')}`,
|
|
@@ -937,7 +951,7 @@ function workerPrompt(task, brief, n, lead) {
|
|
|
937
951
|
`Brief ${brief.id}: ${brief.title}`,
|
|
938
952
|
`Spec:`,
|
|
939
953
|
brief.spec,
|
|
940
|
-
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, String(task.lessons)
|
|
954
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, String(task.lessons)] : []),
|
|
941
955
|
...(typeof task.codeContext === 'string' && task.codeContext ? ['', `Code the semantic index found relevant to this task (path:lines; read these first, they are not the files you own):`, String(task.codeContext).slice(0, 2000)] : []),
|
|
942
956
|
...(typeof lead.interfaces === 'string' && lead.interfaces ? ['', `Interfaces the briefs export or consume (the lead's design; conform to it, do not redesign it):`, lead.interfaces.slice(0, 2000)] : []),
|
|
943
957
|
...(siblings.length ? ['', `Sibling briefs of this task (their files are not yours to touch; conform to what they export):`, ...siblings.map(s => ` - ${s.id}: ${s.title} (${(Array.isArray(s.files) ? s.files : []).join(', ')})`)] : []),
|
|
@@ -1029,7 +1043,7 @@ function fixPrompt(task, r, pass, baseBranch, ledger, brief) {
|
|
|
1029
1043
|
`Task ${task.id}: ${task.title}`,
|
|
1030
1044
|
`Spec:`,
|
|
1031
1045
|
task.spec,
|
|
1032
|
-
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, String(task.lessons)
|
|
1046
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, String(task.lessons)] : []),
|
|
1033
1047
|
...(typeof task.codeContext === 'string' && task.codeContext ? ['', `Code the semantic index found relevant to this task (path:lines; read these first, they are not the files you own):`, String(task.codeContext).slice(0, 2000)] : []),
|
|
1034
1048
|
``,
|
|
1035
1049
|
`Files you own (you may create or edit ONLY these): ${task.files.join(', ')}`,
|
|
@@ -1139,7 +1153,7 @@ function verifyPrompt(task, impl, baseBranch, seat = 1) {
|
|
|
1139
1153
|
`Task ${task.id}: ${task.title}`,
|
|
1140
1154
|
`Spec:`,
|
|
1141
1155
|
task.spec,
|
|
1142
|
-
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons)
|
|
1156
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons)] : []),
|
|
1143
1157
|
``,
|
|
1144
1158
|
`Plan acceptance criteria (the spec must be consistent with these; a line starting with $ is the command that checks the criterion above it and exits 0 when it holds):`,
|
|
1145
1159
|
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command ? [`- ${a.text}`, ` $ ${a.command}`] : [`- ${a.text}`]),
|
|
@@ -1172,7 +1186,7 @@ function checkPrompt(task, impl, prevCommit, baseBranch, ledger) {
|
|
|
1172
1186
|
`Task ${task.id}: ${task.title}`,
|
|
1173
1187
|
`Spec:`,
|
|
1174
1188
|
task.spec,
|
|
1175
|
-
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons)
|
|
1189
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons)] : []),
|
|
1176
1190
|
``,
|
|
1177
1191
|
`Plan acceptance criteria (the spec must be consistent with these; a line starting with $ is the command that checks the criterion above it and exits 0 when it holds):`,
|
|
1178
1192
|
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command ? [`- ${a.text}`, ` $ ${a.command}`] : [`- ${a.text}`]),
|
|
@@ -1300,7 +1314,7 @@ function reviewPrompt(task, impl, baseBranch, seat = 1) {
|
|
|
1300
1314
|
`Task ${task.id}: ${task.title}`,
|
|
1301
1315
|
`Spec:`,
|
|
1302
1316
|
task.spec,
|
|
1303
|
-
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons)
|
|
1317
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons)] : []),
|
|
1304
1318
|
`Owned files: ${task.files.join(', ')}`,
|
|
1305
1319
|
`Plan acceptance criteria:`,
|
|
1306
1320
|
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command ? [`- ${a.text}`, ` $ ${a.command}`] : [`- ${a.text}`]),
|
|
@@ -1343,7 +1357,7 @@ function adversaryPrompt(task, impl, baseBranch, pass, ledger) {
|
|
|
1343
1357
|
`Task ${task.id}: ${task.title}`,
|
|
1344
1358
|
``,
|
|
1345
1359
|
guard(task.spec),
|
|
1346
|
-
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons)
|
|
1360
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons)] : []).map(guard),
|
|
1347
1361
|
``,
|
|
1348
1362
|
...(pass > 1 ? confirmationLines(pass, ledger, impl).map(guard) : [`This is the first pass of this task: explore freely and try to refute the change however you can.`]),
|
|
1349
1363
|
``,
|
|
@@ -1406,7 +1420,7 @@ function fallbackAdversaryPrompt(task, impl, baseBranch, why, pass, ledger) {
|
|
|
1406
1420
|
`Task ${task.id}: ${task.title}`,
|
|
1407
1421
|
`Spec:`,
|
|
1408
1422
|
task.spec,
|
|
1409
|
-
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons)
|
|
1423
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons)] : []),
|
|
1410
1424
|
`Owned files: ${task.files.join(', ')}`,
|
|
1411
1425
|
`Plan acceptance criteria:`,
|
|
1412
1426
|
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command ? [`- ${a.text}`, ` $ ${a.command}`] : [`- ${a.text}`]),
|
|
@@ -1866,11 +1880,12 @@ function isReady(r) {
|
|
|
1866
1880
|
if (r.impl && r.impl.partial) return false
|
|
1867
1881
|
// A finding the ledger already recorded as fixed and that came back keeps the task out even when every stage
|
|
1868
1882
|
// passed. This is deliberately unconditional: updateLedger reopens on any re-report matched by id or fingerprint,
|
|
1869
|
-
// whatever that re-report's own `blocking` flag
|
|
1870
|
-
//
|
|
1871
|
-
//
|
|
1872
|
-
//
|
|
1873
|
-
//
|
|
1883
|
+
// whatever that re-report's own `blocking` flag says, except that an explicit minor/nit/note severity no longer
|
|
1884
|
+
// reopens (card stall-on-repeat-minor, 2026-10-25) - a passing check's free-text "F1: still returns
|
|
1885
|
+
// 1..." reopens F1 ("still reopens and keeps entries on a real re-report"), and so does an adversary's
|
|
1886
|
+
// blocker-severity issue naming the same id ("blocks the pass when a finding the ledger recorded as fixed comes
|
|
1887
|
+
// back"). Gating this on the reporting finding's own blocking flag would silently let those real reappearances
|
|
1888
|
+
// through. A 'waived' report only stays out of the re-report list (card d-waiver-structural-rule)
|
|
1874
1889
|
// when the ledger's own entry for that id is itself a self-declared non-blocking note (confirmsFinding above); a
|
|
1875
1890
|
// disputed waiver against a blocking entry reaches here as an ordinary re-report and can reopen or keep an entry
|
|
1876
1891
|
// open exactly the same way.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doug-gates",
|
|
3
3
|
"displayName": "Doug Gates",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.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"
|
|
@@ -258,6 +258,15 @@ export function writeAgentStart(dir, cfg, sessionId, agentId) {
|
|
|
258
258
|
try {
|
|
259
259
|
const fp = treeFingerprint(dir, cfg, changedFiles(dir));
|
|
260
260
|
if (!fp) return;
|
|
261
|
+
// card worktree-skip-origin-at-start: origin/HEAD's sha at start (null when unset), for the worktree skip.
|
|
262
|
+
let originHead = null;
|
|
263
|
+
try {
|
|
264
|
+
const o = execFileSync("git", ["rev-parse", "--verify", "-q", "refs/remotes/origin/HEAD^{commit}"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim();
|
|
265
|
+
if (/^[0-9a-f]{7,64}$/.test(o)) originHead = o;
|
|
266
|
+
} catch {
|
|
267
|
+
originHead = null;
|
|
268
|
+
}
|
|
269
|
+
fp.originHead = originHead;
|
|
261
270
|
const file = agentStartFile(dir, sessionId, agentId);
|
|
262
271
|
mkdirSync(join(file, ".."), { recursive: true });
|
|
263
272
|
writeFileSync(file, JSON.stringify(fp));
|
|
@@ -110,13 +110,15 @@ export function failingTestFilesFrom(output, dir) {
|
|
|
110
110
|
// them is in the standing tests_red_by_design claim (`redByDesignClaimFiles`).
|
|
111
111
|
// 2. 'load-timeout' — at least one command timed out or was dropped for budget, and no failed command's
|
|
112
112
|
// captured output names a failing test file.
|
|
113
|
-
// 3. '
|
|
113
|
+
// 3. 'test-count' — the test-count ratchet is the block's only problem (`countProblems > 0` and it equals
|
|
114
|
+
// `totalProblems`).
|
|
115
|
+
// 4. 'race' — the working tree (`changedFiles(dir)`, as a set) differs now from `changedAtStart`. Either
|
|
114
116
|
// side being unknown (null: no git, or git unavailable) is never a race. Skipped entirely once
|
|
115
117
|
// `budgetDeadline` (the gate's own hook-timeout deadline, gateBudget() in stop-gate.mjs) has already
|
|
116
118
|
// passed (review pass 2, item 4): a second `git status` here is exactly the kind of extra work that
|
|
117
119
|
// could push a slow gate past the platform's own hook timeout, discarding its block message (F1) — a
|
|
118
120
|
// deadline already blown is not worth spending on a label, so this falls through to 'real' instead.
|
|
119
|
-
//
|
|
121
|
+
// 5. 'real' — otherwise.
|
|
120
122
|
//
|
|
121
123
|
// Never throws (review pass 2, item 2 MAJOR): this call sits between the gate's decision to block and the
|
|
122
124
|
// blockStop call that carries it out (stop-gate.mjs), so an unguarded throw here would fail the *whole gate*
|
|
@@ -141,10 +143,10 @@ export function classifyFinalBlock({ countProblems, totalProblems, preVerificati
|
|
|
141
143
|
if (allNamed.every((f) => claimSet.has(f))) return "expected-red";
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
// card block-ledger-failing-output: the test-count ratchet is the block's only problem.
|
|
145
146
|
const timedOutOrDropped = (Array.isArray(results) ? results : []).some((r) => r.timedOut || r.skipped === "budget");
|
|
146
147
|
if (timedOutOrDropped && allNamed.length === 0) return "load-timeout";
|
|
147
148
|
|
|
149
|
+
// card block-ledger-failing-output: the test-count ratchet is the block's only problem.
|
|
148
150
|
if (typeof countProblems === "number" && countProblems > 0 && totalProblems === countProblems) return "test-count";
|
|
149
151
|
|
|
150
152
|
if (Array.isArray(changedAtStart) && !(typeof budgetDeadline === "number" && Date.now() >= budgetDeadline)) {
|