@dougharness/cli 1.0.1

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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ablate.d.ts +96 -0
  3. package/dist/ablate.js +440 -0
  4. package/dist/ablate.js.map +1 -0
  5. package/dist/apply.d.ts +2 -0
  6. package/dist/apply.js +19 -0
  7. package/dist/apply.js.map +1 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +80 -0
  10. package/dist/bin.js.map +1 -0
  11. package/dist/board-page.d.ts +15 -0
  12. package/dist/board-page.js +27 -0
  13. package/dist/board-page.js.map +1 -0
  14. package/dist/board-serve-process.d.ts +24 -0
  15. package/dist/board-serve-process.js +227 -0
  16. package/dist/board-serve-process.js.map +1 -0
  17. package/dist/board-serve.d.ts +9 -0
  18. package/dist/board-serve.js +302 -0
  19. package/dist/board-serve.js.map +1 -0
  20. package/dist/board.d.ts +6 -0
  21. package/dist/board.js +515 -0
  22. package/dist/board.js.map +1 -0
  23. package/dist/detect/fs.d.ts +6 -0
  24. package/dist/detect/fs.js +50 -0
  25. package/dist/detect/fs.js.map +1 -0
  26. package/dist/detect/index.d.ts +3 -0
  27. package/dist/detect/index.js +13 -0
  28. package/dist/detect/index.js.map +1 -0
  29. package/dist/detect/node.d.ts +3 -0
  30. package/dist/detect/node.js +179 -0
  31. package/dist/detect/node.js.map +1 -0
  32. package/dist/detect/python.d.ts +2 -0
  33. package/dist/detect/python.js +388 -0
  34. package/dist/detect/python.js.map +1 -0
  35. package/dist/detect/repo.d.ts +2 -0
  36. package/dist/detect/repo.js +102 -0
  37. package/dist/detect/repo.js.map +1 -0
  38. package/dist/detect/types.d.ts +98 -0
  39. package/dist/detect/types.js +2 -0
  40. package/dist/detect/types.js.map +1 -0
  41. package/dist/diff.d.ts +12 -0
  42. package/dist/diff.js +105 -0
  43. package/dist/diff.js.map +1 -0
  44. package/dist/generate/agents.d.ts +19 -0
  45. package/dist/generate/agents.js +184 -0
  46. package/dist/generate/agents.js.map +1 -0
  47. package/dist/generate/claude-md.d.ts +9 -0
  48. package/dist/generate/claude-md.js +111 -0
  49. package/dist/generate/claude-md.js.map +1 -0
  50. package/dist/generate/config.d.ts +51 -0
  51. package/dist/generate/config.js +155 -0
  52. package/dist/generate/config.js.map +1 -0
  53. package/dist/generate/proposal.d.ts +11 -0
  54. package/dist/generate/proposal.js +175 -0
  55. package/dist/generate/proposal.js.map +1 -0
  56. package/dist/generate/settings.d.ts +35 -0
  57. package/dist/generate/settings.js +213 -0
  58. package/dist/generate/settings.js.map +1 -0
  59. package/dist/generate/skills.d.ts +8 -0
  60. package/dist/generate/skills.js +264 -0
  61. package/dist/generate/skills.js.map +1 -0
  62. package/dist/init.d.ts +10 -0
  63. package/dist/init.js +69 -0
  64. package/dist/init.js.map +1 -0
  65. package/dist/trace.d.ts +9 -0
  66. package/dist/trace.js +106 -0
  67. package/dist/trace.js.map +1 -0
  68. package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +10 -0
  69. package/node_modules/@dougharness/flow/LICENSE +21 -0
  70. package/node_modules/@dougharness/flow/agents/adversary-claude.md +33 -0
  71. package/node_modules/@dougharness/flow/agents/adversary.md +29 -0
  72. package/node_modules/@dougharness/flow/agents/implementer.md +23 -0
  73. package/node_modules/@dougharness/flow/agents/lead.md +35 -0
  74. package/node_modules/@dougharness/flow/agents/planner.md +70 -0
  75. package/node_modules/@dougharness/flow/agents/researcher.md +31 -0
  76. package/node_modules/@dougharness/flow/agents/reviewer.md +26 -0
  77. package/node_modules/@dougharness/flow/agents/verifier.md +24 -0
  78. package/node_modules/@dougharness/flow/lib/board.mjs +840 -0
  79. package/node_modules/@dougharness/flow/lib/code-index.mjs +629 -0
  80. package/node_modules/@dougharness/flow/lib/cost.mjs +392 -0
  81. package/node_modules/@dougharness/flow/lib/decisions.mjs +314 -0
  82. package/node_modules/@dougharness/flow/lib/embeddings.mjs +539 -0
  83. package/node_modules/@dougharness/flow/lib/land.mjs +219 -0
  84. package/node_modules/@dougharness/flow/lib/learn.mjs +742 -0
  85. package/node_modules/@dougharness/flow/lib/memory-import.mjs +145 -0
  86. package/node_modules/@dougharness/flow/lib/memory.mjs +1641 -0
  87. package/node_modules/@dougharness/flow/lib/models.mjs +148 -0
  88. package/node_modules/@dougharness/flow/lib/plan.mjs +773 -0
  89. package/node_modules/@dougharness/flow/lib/rehearse.mjs +825 -0
  90. package/node_modules/@dougharness/flow/lib/worktrees.mjs +215 -0
  91. package/node_modules/@dougharness/flow/package.json +19 -0
  92. package/node_modules/@dougharness/flow/scripts/board.mjs +260 -0
  93. package/node_modules/@dougharness/flow/scripts/cost.mjs +66 -0
  94. package/node_modules/@dougharness/flow/scripts/learn.mjs +161 -0
  95. package/node_modules/@dougharness/flow/scripts/memory.mjs +1274 -0
  96. package/node_modules/@dougharness/flow/scripts/plan.mjs +547 -0
  97. package/node_modules/@dougharness/flow/scripts/rehearse.mjs +129 -0
  98. package/node_modules/@dougharness/flow/skills/core-next/SKILL.md +92 -0
  99. package/node_modules/@dougharness/flow/skills/doug-approve/SKILL.md +15 -0
  100. package/node_modules/@dougharness/flow/skills/doug-decide/SKILL.md +26 -0
  101. package/node_modules/@dougharness/flow/skills/doug-implement/SKILL.md +22 -0
  102. package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +20 -0
  103. package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +87 -0
  104. package/node_modules/@dougharness/flow/skills/doug-plan/SKILL.md +22 -0
  105. package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +55 -0
  106. package/node_modules/@dougharness/flow/skills/harness-fix/SKILL.md +85 -0
  107. package/node_modules/@dougharness/flow/skills/research/SKILL.md +41 -0
  108. package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +51 -0
  109. package/node_modules/@dougharness/flow/skills/swarm-launch/SKILL.md +27 -0
  110. package/node_modules/@dougharness/flow/workflows/doug-implement.js +2687 -0
  111. package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +10 -0
  112. package/node_modules/@dougharness/gates/LICENSE +21 -0
  113. package/node_modules/@dougharness/gates/hooks/hooks.json +184 -0
  114. package/node_modules/@dougharness/gates/lib/agent-context.mjs +111 -0
  115. package/node_modules/@dougharness/gates/lib/anchor.mjs +110 -0
  116. package/node_modules/@dougharness/gates/lib/baseline.mjs +160 -0
  117. package/node_modules/@dougharness/gates/lib/bash-rules.mjs +1522 -0
  118. package/node_modules/@dougharness/gates/lib/block-ledger.mjs +127 -0
  119. package/node_modules/@dougharness/gates/lib/checkpoint.mjs +97 -0
  120. package/node_modules/@dougharness/gates/lib/config.mjs +168 -0
  121. package/node_modules/@dougharness/gates/lib/evidence.mjs +59 -0
  122. package/node_modules/@dougharness/gates/lib/glob.mjs +86 -0
  123. package/node_modules/@dougharness/gates/lib/io.mjs +91 -0
  124. package/node_modules/@dougharness/gates/lib/paths.mjs +101 -0
  125. package/node_modules/@dougharness/gates/lib/proposals.mjs +201 -0
  126. package/node_modules/@dougharness/gates/lib/run.mjs +147 -0
  127. package/node_modules/@dougharness/gates/lib/scope.mjs +117 -0
  128. package/node_modules/@dougharness/gates/lib/secret-rules.mjs +268 -0
  129. package/node_modules/@dougharness/gates/lib/state.mjs +114 -0
  130. package/node_modules/@dougharness/gates/lib/trace.mjs +196 -0
  131. package/node_modules/@dougharness/gates/package.json +17 -0
  132. package/node_modules/@dougharness/gates/scripts/edit-loop.mjs +39 -0
  133. package/node_modules/@dougharness/gates/scripts/format-on-edit.mjs +45 -0
  134. package/node_modules/@dougharness/gates/scripts/guard-bash.mjs +32 -0
  135. package/node_modules/@dougharness/gates/scripts/protect-paths.mjs +49 -0
  136. package/node_modules/@dougharness/gates/scripts/reanchor.mjs +81 -0
  137. package/node_modules/@dougharness/gates/scripts/research-cap.mjs +295 -0
  138. package/node_modules/@dougharness/gates/scripts/secret-scan.mjs +26 -0
  139. package/node_modules/@dougharness/gates/scripts/session-start-baseline.mjs +57 -0
  140. package/node_modules/@dougharness/gates/scripts/statusline.mjs +133 -0
  141. package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +1422 -0
  142. package/node_modules/@dougharness/gates/scripts/trace.mjs +75 -0
  143. package/package.json +33 -0
  144. package/templates/board-app.js +533 -0
@@ -0,0 +1,219 @@
1
+ // plan.mjs land: merge the plan's integration branch into the base branch, only when the plan is done
2
+ // and the plan's verify commands pass on that branch right now. Code decides; the workflow report is
3
+ // not trusted for this. Refuses, with a reason and no partial state, otherwise.
4
+ //
5
+ // 1. plan status must be "done" (the user's call after a green run)
6
+ // 2. the integration branch must exist and be ahead of the base
7
+ // 3. the base branch must be the one checked out here; the user's branch is never switched
8
+ // 4. no tracked file outside .doug/ may be modified (git would refuse or half-apply the merge)
9
+ // 4b. no commit on the branch since the base carries a Co-Authored-By or Claude-Session line (this repository
10
+ // records attribution nowhere in git; the workflow's prompts say so, and land is where it is enforced)
11
+ // 5. plan.install (if any), every plan.verify command, and then every acceptance command must pass
12
+ // in a worktree of the branch
13
+ // 6. git merge --no-ff; a conflict is aborted and reported
14
+ // 7. the integration worktree is force-removed (it is land's own verify sandbox); then every other
15
+ // worktree under .claude/worktrees whose branch is one of the plan's task branches or is already
16
+ // merged into the base is removed too (kept, named, and never forced when it has local changes);
17
+ // task branches are kept unless opts.deleteBranches (or config land.deleteBranches) asks to delete
18
+ // them with `git branch -d` (never forced); the integration branch is never deleted; plan.landed
19
+ // records the result
20
+
21
+ import { execFileSync, spawnSync } from "node:child_process";
22
+ import { existsSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+ import { acceptanceEntries, integrationBranchFor, savePlan } from "./plan.mjs";
26
+ import { removeFinishedWorktrees } from "./worktrees.mjs";
27
+
28
+ export const INTEGRATION_WORKTREE = ".claude/worktrees/doug-integration";
29
+
30
+ function git(dir, args) {
31
+ return execFileSync("git", args, { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 60000 }).trim();
32
+ }
33
+
34
+ function tryGit(dir, args) {
35
+ try {
36
+ return git(dir, args);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ // Like tryGit, but keeps the error (with its stderr) instead of discarding it on failure.
43
+ function tryGitCapture(dir, args) {
44
+ try {
45
+ return { ok: true, output: git(dir, args) };
46
+ } catch (err) {
47
+ return { ok: false, error: err };
48
+ }
49
+ }
50
+
51
+ export const TRAILER_RE = /^(Co-Authored-By|Claude-Session):/im;
52
+
53
+ // Every commit in base..branch whose message carries a Co-Authored-By or Claude-Session line: { sha, subject, trailers }.
54
+ export function trailerCommits(dir, base, branch) {
55
+ const log = tryGit(dir, ["log", "--format=%x1e%h%x1f%s%x1f%B", `${base}..${branch}`]);
56
+ if (!log) return [];
57
+ const out = [];
58
+ for (const rec of log.split("\x1e")) {
59
+ if (!rec.trim()) continue;
60
+ const [sha, subject, body] = rec.split("\x1f");
61
+ const trailers = [...new Set((body || "").split("\n").map((l) => (TRAILER_RE.exec(l) || [])[1]).filter(Boolean).map((t) => (t.toLowerCase() === "co-authored-by" ? "Co-Authored-By" : "Claude-Session")))];
62
+ if (trailers.length) out.push({ sha, subject, trailers });
63
+ }
64
+ return out;
65
+ }
66
+
67
+ function tail(text, lines = 30) {
68
+ return String(text || "").trimEnd().split("\n").slice(-lines).join("\n");
69
+ }
70
+
71
+ // NO_COLOR, not FORCE_COLOR: colour libraries (picocolors/tinyrainbow, used by vitest) treat any FORCE_COLOR
72
+ // key in the environment as colour enabled, even "0", so a spawned command's acceptance-grep output carried
73
+ // ANSI escapes and missed a plain-text pattern (card land-force-color-enables-color). CI is kept when the
74
+ // caller already set it, defaulted to "1" otherwise; an inherited FORCE_COLOR is dropped, never passed through.
75
+ export function commandEnv(base = process.env) {
76
+ const env = { ...base, CI: base.CI ?? "1", NO_COLOR: "1" };
77
+ delete env.FORCE_COLOR;
78
+ return env;
79
+ }
80
+
81
+ function run(command, cwd, timeoutMs) {
82
+ const started = Date.now();
83
+ const r = spawnSync(command, { cwd, shell: true, encoding: "utf8", timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, env: commandEnv() });
84
+ const timedOut = r.error && r.error.code === "ETIMEDOUT";
85
+ return { command, ok: !timedOut && r.status === 0, status: r.status, timedOut: !!timedOut, durationMs: Date.now() - started, outputTail: tail((r.stdout || "") + "\n" + (r.stderr || "")) };
86
+ }
87
+
88
+ // Returns { ok: true, base, branch, mergeCommit, verify } or { ok: false, reason, verify? }. Never throws on a refusal.
89
+ export function landPlan(dir, plan, { timeoutMs = 600000, now = () => new Date(), deleteBranches = false } = {}) {
90
+ if (!plan) return { ok: false, reason: "no plan" };
91
+ if (plan.status !== "done") return { ok: false, reason: `plan status is "${plan.status}", not "done"; run plan.mjs done after a green doug-implement run` };
92
+ if (plan.landed) return { ok: false, reason: `plan was already landed as ${plan.landed.mergeCommit} on ${plan.landed.at}` };
93
+ const branch = integrationBranchFor(plan);
94
+ if (tryGit(dir, ["rev-parse", "--verify", "-q", `refs/heads/${branch}`]) === null) return { ok: false, reason: `integration branch ${branch} does not exist; nothing to land` };
95
+
96
+ const current = tryGit(dir, ["symbolic-ref", "--short", "-q", "HEAD"]);
97
+ if (!current) return { ok: false, reason: "detached HEAD; check out the base branch first" };
98
+ const base = plan.baseBranch || current;
99
+ if (base !== current) return { ok: false, reason: `base branch is ${base} but ${current} is checked out; check out ${base} first (land never switches your branch)` };
100
+
101
+ const baseTip = git(dir, ["rev-parse", base]);
102
+ const branchTip = git(dir, ["rev-parse", branch]);
103
+ if (tryGit(dir, ["merge-base", "--is-ancestor", branch, base]) !== null) return { ok: false, reason: `${branch} is already contained in ${base}; nothing to land` };
104
+
105
+ const trailered = trailerCommits(dir, base, branch);
106
+ if (trailered.length) return { ok: false, reason: `commits on ${branch} carry attribution trailers (Co-Authored-By or Claude-Session), which this repository does not record; rewrite them first:\n${trailered.map((c) => ` ${c.sha} ${c.subject} (${c.trailers.join(", ")})`).join("\n")}` };
107
+
108
+ // Not through git(): its trim() would eat the leading status column of the first line.
109
+ const dirty = execFileSync("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
110
+ .split("\n")
111
+ .filter(Boolean)
112
+ .map((l) => l.slice(3).replace(/^"|"$/g, ""))
113
+ .filter((p) => !p.startsWith(".doug/"));
114
+ if (dirty.length) return { ok: false, reason: `tracked files are modified: ${dirty.join(", ")}; commit or stash them first` };
115
+
116
+ // Verify on the branch itself, in the integration worktree if the workflow left it, else a temporary one.
117
+ const existing = join(dir, INTEGRATION_WORKTREE);
118
+ const reuse = existsSync(existing) && tryGit(existing, ["symbolic-ref", "--short", "-q", "HEAD"]) === branch;
119
+ let work = reuse ? existing : null;
120
+ let temp = null;
121
+ if (!work) {
122
+ temp = mkdtempSync(join(tmpdir(), "doug-land-"));
123
+ work = join(temp, "wt");
124
+ try {
125
+ git(dir, ["worktree", "add", "--detach", work, branch]);
126
+ } catch (err) {
127
+ rmSync(temp, { recursive: true, force: true });
128
+ return { ok: false, reason: `could not create a worktree for ${branch}: ${tail(err.stderr || err.message, 3)}` };
129
+ }
130
+ }
131
+ const verify = [];
132
+ const acceptance = [];
133
+ try {
134
+ const commands = [...(plan.install ? [plan.install] : []), ...(plan.verify || [])];
135
+ for (const command of commands) {
136
+ const r = run(command, work, timeoutMs);
137
+ verify.push(r);
138
+ if (!r.ok) {
139
+ const why = r.timedOut ? `timed out after ${timeoutMs} ms` : `exit ${r.status}`;
140
+ return { ok: false, reason: `verify failed on ${branch}: \`${command}\` (${why})\n${r.outputTail}`, verify, acceptance: [] };
141
+ }
142
+ }
143
+
144
+ const acceptanceCommands = acceptanceEntries(plan.acceptance).filter((a) => a.command);
145
+ for (const a of acceptanceCommands) {
146
+ const r = run(a.command, work, timeoutMs);
147
+ const entry = { text: a.text, command: a.command, ok: r.ok, status: r.status, timedOut: r.timedOut, durationMs: r.durationMs, outputTail: r.outputTail };
148
+ acceptance.push(entry);
149
+ if (!entry.ok) {
150
+ const why = entry.timedOut ? `timed out after ${timeoutMs} ms` : `exit ${entry.status}`;
151
+ return { ok: false, reason: `acceptance failed on ${branch}: ${entry.text}\n\`${entry.command}\` (${why})\n${entry.outputTail}`, verify, acceptance };
152
+ }
153
+ }
154
+ } finally {
155
+ if (temp) {
156
+ tryGit(dir, ["worktree", "remove", "--force", work]);
157
+ rmSync(temp, { recursive: true, force: true });
158
+ }
159
+ }
160
+
161
+ const summary = [
162
+ ...verify.map((v) => `${v.command}: ${v.ok ? "ok" : "failed"}`),
163
+ ...acceptance.map((a) => `${a.text} (${a.command}): ${a.ok ? "ok" : "failed"}`),
164
+ ].join("\n");
165
+ const message = `doug: land ${plan.title}\n\n${branch} -> ${base}\n${summary}`;
166
+ try {
167
+ git(dir, ["merge", "--no-ff", "--no-edit", "-m", message, branch]);
168
+ } catch (err) {
169
+ tryGit(dir, ["merge", "--abort"]);
170
+ return { ok: false, reason: `merge of ${branch} into ${base} failed and was aborted:\n${tail(err.stdout + "\n" + err.stderr, 10)}`, verify };
171
+ }
172
+ const mergeCommit = git(dir, ["rev-parse", "--short", "HEAD"]);
173
+
174
+ const removedWorktrees = [];
175
+ if (existsSync(existing)) {
176
+ // Resolve before removing (git deletes the directory, so realpath must run first); reported in
177
+ // the same resolved form removeFinishedWorktrees uses, so removed never mixes /var and /private/var.
178
+ let existingReal;
179
+ try {
180
+ existingReal = realpathSync(existing);
181
+ } catch {
182
+ existingReal = existing;
183
+ }
184
+ tryGit(dir, ["worktree", "remove", "--force", existing]);
185
+ removedWorktrees.push(existingReal);
186
+ }
187
+ const taskBranches = (plan.tasks || []).flatMap((t) => [`doug/task-${t.id}`, ...(typeof t.reuse === "string" ? [t.reuse] : [])]);
188
+ const wtResult = removeFinishedWorktrees(dir, { branches: taskBranches, mergedInto: base });
189
+ const worktrees = { removed: [...removedWorktrees, ...wtResult.removed], kept: wtResult.kept };
190
+
191
+ const branchesResult = { deleted: [], kept: [] };
192
+ if (deleteBranches) {
193
+ for (const name of taskBranches) {
194
+ if (name === branch) continue;
195
+ if (tryGit(dir, ["rev-parse", "--verify", "-q", `refs/heads/${name}`]) === null) continue;
196
+ const del = tryGitCapture(dir, ["branch", "-d", name]);
197
+ if (!del.ok) {
198
+ const stderr = del.error.stderr || del.error.message || "";
199
+ const lines = String(stderr).trimEnd().split("\n").filter((l) => l.trim());
200
+ branchesResult.kept.push({ name, reason: lines.length ? lines[lines.length - 1].trim() : "git branch -d failed" });
201
+ } else {
202
+ branchesResult.deleted.push(name);
203
+ }
204
+ }
205
+ }
206
+
207
+ const landed = {
208
+ at: now().toISOString(),
209
+ base,
210
+ branch,
211
+ baseWas: baseTip.slice(0, 7),
212
+ branchTip: branchTip.slice(0, 7),
213
+ mergeCommit,
214
+ verify: verify.map((v) => ({ command: v.command, ok: v.ok, durationMs: v.durationMs })),
215
+ acceptance: acceptance.map((a) => ({ text: a.text, command: a.command, ok: a.ok, durationMs: a.durationMs })),
216
+ };
217
+ savePlan(dir, { ...plan, landed });
218
+ return { ok: true, base, branch, mergeCommit, verify, acceptance, worktrees, branches: branchesResult };
219
+ }