@dougharness/cli 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/implementer.md +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +1 -1
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -1
- package/node_modules/@dougharness/flow/agents/reviewer.md +3 -1
- package/node_modules/@dougharness/flow/hooks/register.tsx +50 -31
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +35 -3
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +25 -0
- package/node_modules/@dougharness/flow/lib/decisions.mjs +4 -2
- package/node_modules/@dougharness/flow/lib/memory.mjs +358 -64
- package/node_modules/@dougharness/flow/lib/plan.mjs +1 -1
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +37 -4
- package/node_modules/@dougharness/flow/scripts/memory.mjs +13 -1
- package/node_modules/@dougharness/flow/scripts/plan.mjs +5 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/workflows/card-queen.js +43 -4
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +56 -16
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +119 -15
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +18 -4
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doug-flow",
|
|
3
3
|
"displayName": "Doug Flow",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"description": "Plan, approve, implement one task per worktree, verify, review. A deterministic Workflow-tool template plus planner, implementer, verifier, and reviewer subagents. The plan is a file; nothing is implemented until you approve it.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Doug contributors"
|
|
@@ -16,7 +16,7 @@ You implement one task. You get a spec, a list of files you own, and a verificat
|
|
|
16
16
|
2. Edit or create only the files you own. If the task truly needs another file, stop, do not touch it, and return `blocked: true` with a reason that names exactly what you need - the file, the symbol or interface, or the decision. A swarm worker may get one re-brief on that reason; a vague one wastes the chance. The planner or user will re-plan otherwise.
|
|
17
17
|
3. Write the test before or with the change when the task adds behavior. Run the task's verification command. Report every command you ran with its real exit status. A command you did not run does not appear in `commandsRun`.
|
|
18
18
|
4. Do not refactor, rename, reformat, or "clean up" anything outside the spec. The reviewer marks any of that as out of scope.
|
|
19
|
-
5. Commit on your branch with a message starting `<id>: `. Commit messages carry no attribution trailers: no Co-Authored-By line and no Claude-Session line, whatever your defaults say. A commit with either is a finding and the plan cannot land. Get `filesTouched` from `git diff --name-only <base>...HEAD`, not from memory.
|
|
19
|
+
5. Commit on your branch with a message starting `<id>: `. Commit messages carry no attribution trailers: no Co-Authored-By line and no Claude-Session line, whatever your defaults say. A commit with either is a finding and the plan cannot land. Get `filesTouched` from `git diff --name-only <base>...HEAD`, not from memory. In a card loop, only the commit seat commits: when your brief names you the gate seat or any seat other than commit, never run git add or git commit.
|
|
20
20
|
6. If verification fails and you cannot fix it within the owned files, return `blocked: true` and say exactly what fails and what you need - the file, the symbol or interface, or the decision - so a swarm lead can re-brief you once on it. Do not report success you have not seen.
|
|
21
21
|
7. Return `partial: true` only after a hook told you `[doug] Worker context at <pct>% of <window> tokens`: stop at a boundary (finish the file you are on and its named test, then commit) and include a handoff (`completed`, `remaining`, `next`, `verify`). Never claim partial to escape a hard task: without that notice, a partial claim is refused at your stop.
|
|
22
22
|
|
|
@@ -52,7 +52,7 @@ You are the planner. You produce a plan, not code. The plan is a file that a hum
|
|
|
52
52
|
|
|
53
53
|
Size decides the pipeline shape. Give every task a `size`: `S` for a bounded change with an exact spec in one or two files (a one-line fix, a new option, a small helper with its test), where one focused check on the verify row does the verifier's and the reviewer's work and the adversary reviews the level's S tasks together on the integration branch after the merge; `M` or `L` for anything that needs judgment, touches several files, or changes behavior other tasks depend on, which keeps the full shape (implement, verify and review together, its own adversary). An unsized task runs the full shape. Say the size in the spec's first sentence when it is S, so the reviewer knows no separate review is coming.
|
|
54
54
|
|
|
55
|
-
Crew: a task (or the plan) may carry `"crew": { "researchers": n, "reviewers": n, "adversaries": n }`, one each by default. Two reviewers read the diff in parallel with distinct briefs; two adversaries are Codex and the Claude adversary together; researchers run before planning through the research step. Do not set a `crew` unless the request names one ("two reviewers on the parser task"): every extra seat is an agent the task budget pays for, and the default crew is the user's choice. More coders is never a crew: split the work into more tasks. Say the crew in the spec's first sentence when you set one.
|
|
55
|
+
Crew: a task (or the plan) may carry `"crew": { "researchers": n, "testers": n, "reviewers": n, "adversaries": n }`, one each by default. Two testers are two verifiers running the task command in parallel (a size S task keeps its one check); two reviewers read the diff in parallel with distinct briefs; two adversaries are Codex and the Claude adversary together; researchers run before planning through the research step. Do not set a `crew` unless the request names one ("two reviewers on the parser task", "two testers"): every extra seat is an agent the task budget pays for, and the default crew is the user's choice. More coders is never a crew: split the work into more tasks. Say the crew in the spec's first sentence when you set one.
|
|
56
56
|
|
|
57
57
|
Gates: a task may carry `"gate": "human"` (default `auto`). The run pauses after that task's level has integrated, writes its report, and waits for the user to open the gate (`plan.mjs gate open <level>`) and resume the run with its id; the levels already run replay. Set it only when the request asks for a pause between phases (a review of the foundation before the features build on it); a gate on the last level is meaningless, since landing is already the user's decision.
|
|
58
58
|
|
|
@@ -3,7 +3,7 @@ name: researcher
|
|
|
3
3
|
description: The research step's default researcher. Answers one distinct question about a fact outside the repository (a CLI's output format, a third-party API, a package's version constraints) from primary sources, quoting each source with its URL or the command and its output, and marks anything it could not confirm unverified. Read-only; never writes to the checkout and never installs anything. Spawned by the research skill; Claude Code questions go to claude-code-guide instead.
|
|
4
4
|
model: inherit
|
|
5
5
|
effort: high
|
|
6
|
-
maxTurns:
|
|
6
|
+
maxTurns: 40
|
|
7
7
|
tools: Read, Grep, Glob, Bash, WebFetch, WebSearch
|
|
8
8
|
disallowedTools: Edit, Write, MultiEdit, NotebookEdit
|
|
9
9
|
memory: none
|
|
@@ -19,6 +19,7 @@ You are the researcher. You were given one question, and a plan will be built on
|
|
|
19
19
|
4. Never install, never write, never change the machine: no package installs, no global config, no files in the checkout, no network calls beyond reading pages. If a fact needs an install to observe, mark it unverified and name the command that would show it.
|
|
20
20
|
5. Date what you read. A documentation page carries the date you fetched it and, when the page shows one, its version.
|
|
21
21
|
6. Budget: at most 6 WebSearch, WebFetch, curl, or wget calls for your question, counted together (research.maxFetches; a hook denies the next one). Try sources in the order the question lists them, and write your findings before the budget runs out, marking anything still unanswered unverified.
|
|
22
|
+
7. Turns: you have at most 40 tool calls (maxTurns). Stop with at least 5 calls to spare and return what you have, marking every part you did not reach unverified: a capped run returns a partial report, never an empty one.
|
|
22
23
|
|
|
23
24
|
## Output
|
|
24
25
|
|
|
@@ -3,7 +3,7 @@ name: reviewer
|
|
|
3
3
|
description: Two-stage reviewer for one task's diff: spec compliance first (does it do all of the spec and nothing else, within the owned files), then quality (correctness risks, missing tests, error handling). Never edits files. Spawned by the doug-implement workflow; also useful standalone on any branch.
|
|
4
4
|
model: inherit
|
|
5
5
|
effort: high
|
|
6
|
-
maxTurns:
|
|
6
|
+
maxTurns: 40
|
|
7
7
|
tools: Read, Grep, Glob, Bash
|
|
8
8
|
disallowedTools: Edit, Write, MultiEdit, NotebookEdit
|
|
9
9
|
memory: none
|
|
@@ -24,3 +24,5 @@ Review the diff, not the description of the diff. Get it with `git diff <base>..
|
|
|
24
24
|
- Consistency: naming and error handling match the surrounding code.
|
|
25
25
|
|
|
26
26
|
Severity: `blocker` fails the review; `major` should be fixed before merge but does not fail it alone; `minor` is advisory. `approve` is true only with no blockers, `specCompliant` true, and `inScope` true. Do not ask for work beyond the task.
|
|
27
|
+
|
|
28
|
+
Budget your turns: you have at most 40 tool calls (maxTurns). Read the diff and run the checks your prompt names first, then any deeper probes; stop with at least 5 calls to spare and return your review with what you found: a capped run returns a partial review, never an empty one.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The live autopilot pane (card autopilot-live-queue). It opens itself when a run starts (card autopilot-pane-auto-open); /doug-autopilot reopens it. It polls
|
|
2
2
|
// .doug/.state/autopilot.json (written by scripts/autopilot.mjs) and shows the same lines the board's panel shows.
|
|
3
|
+
// It also refreshes on prompt submit, turn end, and after a Bash call, because a reload drops the session.start timer (card autopilot-pane-current).
|
|
3
4
|
import { atom, read, update } from 'claude-code'
|
|
4
5
|
import type { Register } from 'claude-code'
|
|
5
6
|
|
|
@@ -11,6 +12,35 @@ const lines = atom({ plugin: 'doug-flow', key: 'autopilot' } as const, [] as str
|
|
|
11
12
|
// The startedAt of the run the pane was last opened for, so a pane the user closed stays closed for that run.
|
|
12
13
|
const opened = atom({ plugin: 'doug-flow', key: 'autopilot-opened' } as const, null as string | null)
|
|
13
14
|
|
|
15
|
+
const refresh = async ($: any) => {
|
|
16
|
+
try {
|
|
17
|
+
const root = await $.session.root()
|
|
18
|
+
let text = ''
|
|
19
|
+
try {
|
|
20
|
+
text = await $.fs.read(`${root}/${AUTOPILOT_FILE}`)
|
|
21
|
+
} catch {
|
|
22
|
+
// no file: no run
|
|
23
|
+
}
|
|
24
|
+
const state = parseAutopilot(text)
|
|
25
|
+
const fresh = autopilotLines(autopilotView(state, new Date()))
|
|
26
|
+
await update($, lines, old => (JSON.stringify(old) === JSON.stringify(fresh) ? old : fresh))
|
|
27
|
+
// Only `autopilot.autoOpenPane: false` in .doug/config.json turns the auto-open off.
|
|
28
|
+
let autoOpen = true
|
|
29
|
+
try {
|
|
30
|
+
autoOpen = JSON.parse(await $.fs.read(`${root}/.doug/config.json`))?.autopilot?.autoOpenPane !== false
|
|
31
|
+
} catch {
|
|
32
|
+
// missing or unparsable config: flag on
|
|
33
|
+
}
|
|
34
|
+
const last = await read($, opened)
|
|
35
|
+
if (state && shouldOpenPane(state, last, autoOpen, new Date())) {
|
|
36
|
+
await update($, opened, () => state.startedAt as string)
|
|
37
|
+
await $.ui.open({ id: PANE, title: 'Autopilot' })
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// a failed poll keeps the last drawing
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
14
44
|
export const register: Register = on => {
|
|
15
45
|
on('session.start', async ($, e, next) => {
|
|
16
46
|
await $.command.register({
|
|
@@ -18,43 +48,32 @@ export const register: Register = on => {
|
|
|
18
48
|
description: 'Show the autopilot queue live in a pane',
|
|
19
49
|
})
|
|
20
50
|
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const root = await $.session.root()
|
|
24
|
-
let text = ''
|
|
25
|
-
try {
|
|
26
|
-
text = await $.fs.read(`${root}/${AUTOPILOT_FILE}`)
|
|
27
|
-
} catch {
|
|
28
|
-
// no file: no run
|
|
29
|
-
}
|
|
30
|
-
const state = parseAutopilot(text)
|
|
31
|
-
const fresh = autopilotLines(autopilotView(state, new Date()))
|
|
32
|
-
await update($, lines, old => (JSON.stringify(old) === JSON.stringify(fresh) ? old : fresh))
|
|
33
|
-
// Only `autopilot.autoOpenPane: false` in .doug/config.json turns the auto-open off.
|
|
34
|
-
let autoOpen = true
|
|
35
|
-
try {
|
|
36
|
-
autoOpen = JSON.parse(await $.fs.read(`${root}/.doug/config.json`))?.autopilot?.autoOpenPane !== false
|
|
37
|
-
} catch {
|
|
38
|
-
// missing or unparsable config: flag on
|
|
39
|
-
}
|
|
40
|
-
const last = await read($, opened)
|
|
41
|
-
if (state && shouldOpenPane(state, last, autoOpen, new Date())) {
|
|
42
|
-
await update($, opened, () => state.startedAt as string)
|
|
43
|
-
await $.ui.open({ id: PANE, title: 'Autopilot' })
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
// a failed poll keeps the last drawing
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// A -p run or the SDK has nobody at the prompt: register the command, start no poll.
|
|
51
|
+
// The timer starts only in an interactive session; the event hooks run in any session (two small reads per prompt, turn end, and Bash call).
|
|
50
52
|
if (e.isInteractive) {
|
|
51
|
-
await
|
|
52
|
-
$.clock.every(POLL_MS,
|
|
53
|
+
await refresh($)
|
|
54
|
+
$.clock.every(POLL_MS, () => refresh($))
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
return next(e)
|
|
56
58
|
})
|
|
57
59
|
|
|
60
|
+
// A reload drops the session.start timer, so these hooks also refresh the pane (card autopilot-pane-current).
|
|
61
|
+
on('prompt.submit', async ($, e, next) => {
|
|
62
|
+
await refresh($)
|
|
63
|
+
return next(e)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
on('turn.complete', async ($, e, next) => {
|
|
67
|
+
await refresh($)
|
|
68
|
+
return next(e)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
|
|
72
|
+
const ran = await next(e)
|
|
73
|
+
await refresh($)
|
|
74
|
+
return ran
|
|
75
|
+
})
|
|
76
|
+
|
|
58
77
|
on('command.run', { command: 'doug-autopilot' }, async $ => {
|
|
59
78
|
await $.ui.open({ id: PANE, title: 'Autopilot' })
|
|
60
79
|
return { text: 'Autopilot pane opened.' }
|
|
@@ -14,6 +14,7 @@ export const STOP_REASONS = [
|
|
|
14
14
|
"setup-failed",
|
|
15
15
|
"tester-red-claim-missing",
|
|
16
16
|
"reviewer-blocker-open",
|
|
17
|
+
"reviewer-major-open",
|
|
17
18
|
"gate-red-twice",
|
|
18
19
|
];
|
|
19
20
|
// The Models-table row each seat runs on. setup, gate and commit have no sourced row; cheap is a choice.
|
|
@@ -86,6 +87,22 @@ export const BRIEF_RULES = {
|
|
|
86
87
|
text: "Commit without Co-Authored-By or Claude-Session trailers.",
|
|
87
88
|
seats: ["commit"],
|
|
88
89
|
},
|
|
90
|
+
"commit-foreground": {
|
|
91
|
+
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.",
|
|
92
|
+
seats: ["commit"],
|
|
93
|
+
},
|
|
94
|
+
"commit-no-background": {
|
|
95
|
+
text: "Never use run_in_background, a Monitor, or a sleep loop for git commit: a background commit dies when your seat ends. Wait for the command to return.",
|
|
96
|
+
seats: ["commit"],
|
|
97
|
+
},
|
|
98
|
+
"commit-sha-from-git-log": {
|
|
99
|
+
text: "Report the sha only from git log -1 --format=%H after git commit has returned; never report a sha you did not read from it.",
|
|
100
|
+
seats: ["commit"],
|
|
101
|
+
},
|
|
102
|
+
"gate-no-git-writes": {
|
|
103
|
+
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.",
|
|
104
|
+
seats: ["gate"],
|
|
105
|
+
},
|
|
89
106
|
"leave-plan-anchor-out": {
|
|
90
107
|
text: "Leave .doug/plan.json and .doug/anchor.md out of the commit unless the card is about them.",
|
|
91
108
|
seats: ["commit"],
|
|
@@ -516,6 +533,9 @@ function validate(state, hb) {
|
|
|
516
533
|
}
|
|
517
534
|
case "gate": {
|
|
518
535
|
checkCmds(hb.commands, "commands", errs, true);
|
|
536
|
+
if (Array.isArray(hb.commands) && hb.commands.some((c) => c && isStr(c.command) && GIT_WRITE_RE.test(c.command))) {
|
|
537
|
+
errs.push("gate seat must not run git add or git commit");
|
|
538
|
+
}
|
|
519
539
|
if (!nonBlank(hb.gateLine)) errs.push("gateLine must be a non-empty string");
|
|
520
540
|
if (hb.lockWaitMs !== undefined && hb.lockWaitMs !== null && !Number.isInteger(hb.lockWaitMs)) {
|
|
521
541
|
errs.push("lockWaitMs must be an integer or null");
|
|
@@ -535,6 +555,9 @@ function validate(state, hb) {
|
|
|
535
555
|
|
|
536
556
|
// ---- next ----
|
|
537
557
|
|
|
558
|
+
// git add / git commit at the start of a command or after a shell separator, global options (-C <path>, -c k=v, --flag) allowed before the verb.
|
|
559
|
+
const GIT_WRITE_RE = /(?:^|[;&|(\n]\s*)git(?:\s+(?:-[Cc]\s+(?:"[^"]*"|'[^']*'|\S+)|--[\w-]+(?:=\S+)?))*\s+(?:add|commit)\b/;
|
|
560
|
+
|
|
538
561
|
function stopWith(s, reason, detail) {
|
|
539
562
|
const seat = s.pending;
|
|
540
563
|
const state = {
|
|
@@ -654,12 +677,21 @@ export function next(state, handBack) {
|
|
|
654
677
|
mutations: cp(H.mutationResults),
|
|
655
678
|
};
|
|
656
679
|
const blockers = findings.filter((f) => f.severity === "blocker");
|
|
657
|
-
|
|
680
|
+
// An evidenced major gets the same one fix pass; a major without evidence and a minor stay advisory.
|
|
681
|
+
const majors = findings.filter((f) => f.severity === "major" && nonBlank(f.evidence));
|
|
682
|
+
if (blockers.length === 0 && majors.length === 0) return advance(s, "gate", "ok", { acc });
|
|
658
683
|
if (s.counters.fixPasses >= 1) {
|
|
684
|
+
if (blockers.length > 0) {
|
|
685
|
+
return stopWith(
|
|
686
|
+
{ ...s, acc },
|
|
687
|
+
"reviewer-blocker-open",
|
|
688
|
+
"blocker still open after one fix pass: " + blockers.map((f) => f.id).join(", "),
|
|
689
|
+
);
|
|
690
|
+
}
|
|
659
691
|
return stopWith(
|
|
660
692
|
{ ...s, acc },
|
|
661
|
-
"reviewer-
|
|
662
|
-
"
|
|
693
|
+
"reviewer-major-open",
|
|
694
|
+
"major with evidence still open after one fix pass: " + majors.map((f) => f.id).join(", "),
|
|
663
695
|
);
|
|
664
696
|
}
|
|
665
697
|
return advance(s, buildSeat(s), "blockers", {
|
|
@@ -366,3 +366,28 @@ export function capsReached(caps, used) {
|
|
|
366
366
|
}
|
|
367
367
|
return out;
|
|
368
368
|
}
|
|
369
|
+
|
|
370
|
+
// The queen's short run label (card queen-run-name): the first two hyphen segments of the card id (the id itself
|
|
371
|
+
// with fewer), or the full id when another id in `others` (the card's own id excluded) shortens to the same form.
|
|
372
|
+
const shortForm = (id) => String(id).split("-").slice(0, 2).join("-");
|
|
373
|
+
export function queenLabel(id, others = []) {
|
|
374
|
+
const short = shortForm(id);
|
|
375
|
+
return others.some((o) => o !== id && shortForm(o) === short) ? String(id) : short;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// The queen script's source with only its meta literal renamed: name becomes "<label> card-queen" (hand) or
|
|
379
|
+
// "<label> flow-queen" (flow), description gets "<label>: " in front. Throws when either is missing from the meta.
|
|
380
|
+
export function renameQueenMeta(source, { kind, label }) {
|
|
381
|
+
const start = source.indexOf("export const meta = {");
|
|
382
|
+
if (start < 0) throw new Error("no meta literal in the queen script");
|
|
383
|
+
const e = source.indexOf("\n}", start);
|
|
384
|
+
const end = e < 0 ? source.length : e;
|
|
385
|
+
let meta = source.slice(start, end);
|
|
386
|
+
const nameRe = /^(\s*name: ')[^']*(',)/m;
|
|
387
|
+
const descRe = /^(\s*description: ')/m;
|
|
388
|
+
if (!nameRe.test(meta)) throw new Error("no meta name in the queen script");
|
|
389
|
+
if (!descRe.test(meta)) throw new Error("no meta description in the queen script");
|
|
390
|
+
const suffix = kind === "flow" ? "flow-queen" : "card-queen";
|
|
391
|
+
meta = meta.replace(nameRe, (_m, a, b) => `${a}${label} ${suffix}${b}`).replace(descRe, (_m, a) => `${a}${label}: `);
|
|
392
|
+
return source.slice(0, start) + meta + source.slice(end);
|
|
393
|
+
}
|
|
@@ -250,8 +250,9 @@ function commandsBlockText(text) {
|
|
|
250
250
|
// writes it - only ever calls writeOne/writeProposals (learn.mjs), which writes under .doug/.state/learn/. Every
|
|
251
251
|
// refusal returns { ok:false, reason } with nothing written; `edits` can be ANY value JSON.parse can produce
|
|
252
252
|
// (null, an array, a string, a number, or an object whose fields are the wrong shape) and this never throws -
|
|
253
|
-
// every field is type-checked before use (lesson 1ee7d426432aca82).
|
|
254
|
-
|
|
253
|
+
// every field is type-checked before use (lesson 1ee7d426432aca82). With `dryRun: true` every check still runs
|
|
254
|
+
// and the result is { ok:true, reason, diff } with nothing written (card reflect-offers-claude-md).
|
|
255
|
+
export function proposeClaudeMd({ dir, edits, now = new Date(), dryRun = false } = {}) {
|
|
255
256
|
const target = join(dir, CLAUDE_MD_RELPATH);
|
|
256
257
|
if (!existsSync(target)) return { ok: false, reason: `${CLAUDE_MD_RELPATH} does not exist under ${dir}` };
|
|
257
258
|
|
|
@@ -310,5 +311,6 @@ export function proposeClaudeMd({ dir, edits, now = new Date() } = {}) {
|
|
|
310
311
|
lines: { before, after },
|
|
311
312
|
};
|
|
312
313
|
const proposal = { id: "01", kind: "claude-md", target: CLAUDE_MD_RELPATH, reason, rationale, diff: diffResult.diff };
|
|
314
|
+
if (dryRun) return { ok: true, reason, diff: diffResult.diff };
|
|
313
315
|
return writeOne(proposal, { dir, now });
|
|
314
316
|
}
|