@dougharness/cli 1.1.1 → 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.
Files changed (57) hide show
  1. package/dist/board-serve.js +40 -6
  2. package/dist/board-serve.js.map +1 -1
  3. package/dist/init.js +1 -1
  4. package/dist/init.js.map +1 -1
  5. package/dist/memory-setup.d.ts +7 -0
  6. package/dist/memory-setup.js +70 -46
  7. package/dist/memory-setup.js.map +1 -1
  8. package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +6 -2
  9. package/node_modules/@dougharness/flow/agents/coder.md +23 -0
  10. package/node_modules/@dougharness/flow/agents/implementer.md +1 -1
  11. package/node_modules/@dougharness/flow/agents/planner.md +1 -1
  12. package/node_modules/@dougharness/flow/agents/researcher.md +2 -1
  13. package/node_modules/@dougharness/flow/agents/reviewer.md +3 -1
  14. package/node_modules/@dougharness/flow/agents/spec-writer.md +23 -0
  15. package/node_modules/@dougharness/flow/agents/tester.md +21 -0
  16. package/node_modules/@dougharness/flow/hooks/hooks.json +4 -1
  17. package/node_modules/@dougharness/flow/hooks/register.tsx +94 -0
  18. package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +117 -0
  19. package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +17 -0
  20. package/node_modules/@dougharness/flow/lib/board.mjs +91 -10
  21. package/node_modules/@dougharness/flow/lib/card-land.mjs +115 -0
  22. package/node_modules/@dougharness/flow/lib/card-loop.mjs +778 -0
  23. package/node_modules/@dougharness/flow/lib/coordinator.mjs +393 -0
  24. package/node_modules/@dougharness/flow/lib/cost.mjs +15 -5
  25. package/node_modules/@dougharness/flow/lib/decisions.mjs +4 -2
  26. package/node_modules/@dougharness/flow/lib/embeddings.mjs +144 -3
  27. package/node_modules/@dougharness/flow/lib/land.mjs +27 -8
  28. package/node_modules/@dougharness/flow/lib/memory.mjs +406 -76
  29. package/node_modules/@dougharness/flow/lib/plan.mjs +33 -5
  30. package/node_modules/@dougharness/flow/lib/worktrees.mjs +3 -3
  31. package/node_modules/@dougharness/flow/package.json +1 -1
  32. package/node_modules/@dougharness/flow/scripts/autopilot.mjs +440 -0
  33. package/node_modules/@dougharness/flow/scripts/board.mjs +107 -32
  34. package/node_modules/@dougharness/flow/scripts/cost.mjs +16 -3
  35. package/node_modules/@dougharness/flow/scripts/memory.mjs +33 -10
  36. package/node_modules/@dougharness/flow/scripts/plan.mjs +104 -30
  37. package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +8 -2
  38. package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +30 -10
  39. package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +80 -0
  40. package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
  41. package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
  42. package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +6 -6
  43. package/node_modules/@dougharness/flow/workflows/card-queen.js +846 -0
  44. package/node_modules/@dougharness/flow/workflows/doug-implement.js +63 -23
  45. package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
  46. package/node_modules/@dougharness/gates/lib/anchor.mjs +29 -4
  47. package/node_modules/@dougharness/gates/lib/bash-rules.mjs +196 -6
  48. package/node_modules/@dougharness/gates/lib/gate-lock.mjs +40 -8
  49. package/node_modules/@dougharness/gates/lib/proposals.mjs +41 -5
  50. package/node_modules/@dougharness/gates/lib/scope.mjs +43 -4
  51. package/node_modules/@dougharness/gates/package.json +1 -1
  52. package/node_modules/@dougharness/gates/scripts/reanchor.mjs +2 -1
  53. package/node_modules/@dougharness/gates/scripts/research-cap.mjs +119 -15
  54. package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +156 -14
  55. package/node_modules/@dougharness/gates/scripts/verify.mjs +8 -4
  56. package/package.json +3 -3
  57. package/templates/board-app.js +46 -0
@@ -3,12 +3,39 @@
3
3
  // /v1/embeddings endpoint (Ollama, llama-server, vLLM, LM Studio, a hosted OpenAI-shaped key) and Voyage AI's
4
4
  // own endpoint. Product first: memory.embeddings in .doug/config.json is null by default, and every function
5
5
  // here treats "no provider" or "provider unreachable" as a normal outcome, never a thrown error.
6
- import { readFileSync, existsSync } from "node:fs";
6
+ import { readFileSync, existsSync, mkdirSync } from "node:fs";
7
7
  import { join, dirname } from "node:path";
8
- import { fileURLToPath } from "node:url";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+ import { homedir } from "node:os";
10
+ import { createRequire } from "node:module";
11
+ import { spawn } from "node:child_process";
9
12
 
10
13
  const CONFIG_RELPATH = ".doug/config.json";
11
14
  const VALID_PROVIDERS = new Set(["openai-compatible", "voyage"]);
15
+
16
+ // The bundled local model (card memory-bundled-embedding, ADR 0006 amendment 2026-10-08): the runtime is
17
+ // installed into a user-level cache and loaded by dynamic import, never a dependency of any Doug package.
18
+ export const LOCAL_MODEL_ID = "onnx-community/embeddinggemma-300m-ONNX";
19
+ export const LOCAL_DTYPE = "q8";
20
+ export const LOCAL_RUNTIME_PACKAGE = "@huggingface/transformers@4.3.1";
21
+ export const LOCAL_QUERY_PREFIX = "task: search result | query: ";
22
+ export const LOCAL_DOCUMENT_PREFIX = "title: none | text: ";
23
+ const LOCAL_DIMS = 512;
24
+
25
+ export function localCacheDir(env = process.env, home = homedir()) {
26
+ if (env.DOUG_CACHE_DIR) return env.DOUG_CACHE_DIR;
27
+ return join(env.XDG_CACHE_HOME || join(home, ".cache"), "doug");
28
+ }
29
+
30
+ export function localPaths(cacheDir) {
31
+ const modelsDir = join(cacheDir, "models");
32
+ return {
33
+ cacheDir,
34
+ runtimeDir: join(cacheDir, "runtime"),
35
+ modelsDir,
36
+ modelFile: join(modelsDir, "onnx-community", "embeddinggemma-300m-ONNX", "onnx", "model_quantized.onnx"),
37
+ };
38
+ }
12
39
  const DEFAULT_TIMEOUT_MS = 2000;
13
40
  const OLLAMA_BASE_URL = "http://localhost:11434";
14
41
  const DEFAULT_MODEL = "embeddinggemma";
@@ -21,6 +48,7 @@ const PRESETS = {
21
48
  unsloth: { provider: "openai-compatible", baseUrl: "http://127.0.0.1:8888", apiKeyEnv: "UNSLOTH_API_KEY", sendsDims: false, requiresKey: true, modelsList: "v1" },
22
49
  openai: { provider: "openai-compatible", baseUrl: "https://api.openai.com", apiKeyEnv: "OPENAI_API_KEY", sendsDims: true, requiresKey: true, modelsList: "v1" },
23
50
  voyage: { provider: "voyage", baseUrl: "https://api.voyageai.com", apiKeyEnv: "VOYAGE_API_KEY", sendsDims: true, requiresKey: true, modelsList: null },
51
+ local: { provider: "local", baseUrl: undefined, apiKeyEnv: undefined, sendsDims: false, requiresKey: false, modelsList: null },
24
52
  custom: { provider: undefined, baseUrl: undefined, apiKeyEnv: undefined, sendsDims: true, requiresKey: false, modelsList: null },
25
53
  };
26
54
 
@@ -60,7 +88,8 @@ export function readMemoryConfig(dir) {
60
88
  const e = memory && typeof memory === "object" ? memory.embeddings : undefined;
61
89
  if (e === null || e === undefined) return { embeddings: null };
62
90
  const resolved = applyPreset(e);
63
- if (!isValidEmbeddingsConfig(resolved)) {
91
+ const isLocal = e.preset === "local";
92
+ if (isLocal ? !isValidLocalConfig(resolved) : !isValidEmbeddingsConfig(resolved)) {
64
93
  process.stderr.write(`[doug] ${CONFIG_RELPATH} memory.embeddings is not a valid shape; treating as unset (keyword-only)\n`);
65
94
  return { embeddings: null };
66
95
  }
@@ -80,9 +109,18 @@ function applyPreset(e) {
80
109
  for (const k of ["provider", "baseUrl", "apiKeyEnv"]) {
81
110
  if (out[k] === undefined && p[k] !== undefined) out[k] = p[k];
82
111
  }
112
+ if (e.preset === "local") {
113
+ if (out.model === undefined) out.model = LOCAL_MODEL_ID;
114
+ if (out.dims === undefined) out.dims = LOCAL_DIMS;
115
+ }
83
116
  return out;
84
117
  }
85
118
 
119
+ // The local runtime runs one model at one width: a config that names anything else is invalid.
120
+ function isValidLocalConfig(r) {
121
+ return !!(r && r.provider === "local" && r.model === LOCAL_MODEL_ID && r.dims === LOCAL_DIMS);
122
+ }
123
+
86
124
  function isValidIndexConfig(idx) {
87
125
  return !!(
88
126
  idx &&
@@ -452,11 +490,96 @@ async function embedBatched(texts, inputType, sendBatch) {
452
490
  return { ok: true, vectors };
453
491
  }
454
492
 
493
+ async function defaultLoadTransformers(runtimeDir) {
494
+ const req = createRequire(join(runtimeDir, "noop.js"));
495
+ return import(pathToFileURL(req.resolve("@huggingface/transformers")).href);
496
+ }
497
+
498
+ // The in-process provider: the transformers module comes from opts.loadTransformers (the test seam) or the
499
+ // runtime dir under the cache. Loads lazily on the first embed and keeps the model for the provider's life.
500
+ // Recall never reaches the network (allowRemoteModels false); only setup's download passes allowRemote.
501
+ function createLocalProvider(e, opts) {
502
+ const paths = localPaths(opts.cacheDir ?? localCacheDir());
503
+ const loadTransformers = opts.loadTransformers ?? (() => defaultLoadTransformers(paths.runtimeDir));
504
+ let loaded = null;
505
+ const load = async () => {
506
+ if (loaded) return loaded;
507
+ let mod;
508
+ try {
509
+ mod = await loadTransformers();
510
+ } catch {
511
+ return { failure: { ok: false, reason: `local embedding runtime not installed at ${paths.runtimeDir}: run doug memory setup` } };
512
+ }
513
+ mod.env.cacheDir = paths.modelsDir;
514
+ mod.env.allowRemoteModels = opts.allowRemote === true;
515
+ const tokenizer = await mod.AutoTokenizer.from_pretrained(LOCAL_MODEL_ID);
516
+ const model = await mod.AutoModel.from_pretrained(LOCAL_MODEL_ID, { dtype: LOCAL_DTYPE });
517
+ loaded = { tokenizer, model };
518
+ return loaded;
519
+ };
520
+ const sendBatch = async (texts, inputType) => {
521
+ const l = await load();
522
+ if (l.failure) return l.failure;
523
+ const prefix = inputType === "query" ? LOCAL_QUERY_PREFIX : LOCAL_DOCUMENT_PREFIX;
524
+ const inputs = l.tokenizer(texts.map((t) => prefix + t), { padding: true, truncation: true });
525
+ const { sentence_embedding } = await l.model(inputs);
526
+ const vectors = [];
527
+ for (const row of sentence_embedding.tolist()) {
528
+ if (row.length < LOCAL_DIMS) return { ok: false, reason: `wrong dims: got ${row.length}, expected ${LOCAL_DIMS}` };
529
+ vectors.push(l2normalize(Float32Array.from(row.slice(0, LOCAL_DIMS))));
530
+ }
531
+ return { ok: true, vectors };
532
+ };
533
+ return {
534
+ name: "local",
535
+ model: LOCAL_MODEL_ID,
536
+ dims: LOCAL_DIMS,
537
+ async embed(texts, { inputType = "document" } = {}) {
538
+ try {
539
+ return await embedBatched(texts, inputType, sendBatch);
540
+ } catch (err) {
541
+ return { ok: false, reason: `local embedding failed: ${err.message}` };
542
+ }
543
+ },
544
+ };
545
+ }
546
+
547
+ const LOCAL_CONFIG = { embeddings: { preset: "local", provider: "local", model: LOCAL_MODEL_ID, dims: LOCAL_DIMS } };
548
+
549
+ // Installs the pinned runtime into <cacheDir>/runtime with the npm CLI, CPU only (the CUDA download is skipped on
550
+ // every platform). Never throws.
551
+ export function installLocalRuntime({ cacheDir, spawnImpl = spawn, platform = process.platform } = {}) {
552
+ return new Promise((resolve) => {
553
+ try {
554
+ const { runtimeDir } = localPaths(cacheDir ?? localCacheDir());
555
+ mkdirSync(runtimeDir, { recursive: true });
556
+ const child = spawnImpl(
557
+ platform === "win32" ? "npm.cmd" : "npm",
558
+ ["install", "--prefix", runtimeDir, "--no-audit", "--no-fund", LOCAL_RUNTIME_PACKAGE],
559
+ { stdio: "inherit", env: { ...process.env, ONNXRUNTIME_NODE_INSTALL: "skip" } },
560
+ );
561
+ child.on("error", (err) => resolve({ ok: false, reason: `npm install failed to start: ${err.message}` }));
562
+ child.on("close", (code) => resolve(code === 0 ? { ok: true } : { ok: false, reason: `npm install exited with code ${code}` }));
563
+ } catch (err) {
564
+ resolve({ ok: false, reason: `npm install failed: ${err.message}` });
565
+ }
566
+ });
567
+ }
568
+
569
+ // One document embed with remote models allowed: from_pretrained downloads the tokenizer and q8 model into the
570
+ // models dir. Never throws.
571
+ export async function downloadLocalModel({ cacheDir, loadTransformers } = {}) {
572
+ const provider = createProvider(LOCAL_CONFIG, { cacheDir, loadTransformers, allowRemote: true });
573
+ const r = await provider.embed(["probe"], { inputType: "document" });
574
+ return r.ok ? { ok: true } : { ok: false, reason: r.reason };
575
+ }
576
+
455
577
  // cfg is the { embeddings } shape readMemoryConfig returns. Returns null when nothing is configured (the
456
578
  // caller's cue to run keyword-only); otherwise { name, model, dims, embed(texts, { inputType }) }.
457
579
  export function createProvider(cfg, opts = {}) {
458
580
  const e = cfg && cfg.embeddings;
459
581
  if (!e) return null;
582
+ if (e.provider === "local") return createLocalProvider(e, opts);
460
583
  const fetchImpl = opts.fetch || globalThis.fetch;
461
584
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
462
585
  const state = { omitDims: false };
@@ -569,6 +692,14 @@ export async function probeProvider(cfg, opts = {}) {
569
692
  const baseUrl = e && e.baseUrl ? e.baseUrl : process.env.DOUG_OLLAMA_BASE_URL || OLLAMA_BASE_URL;
570
693
  const wantModel = e && e.model ? e.model : DEFAULT_MODEL;
571
694
  const root = baseUrl.replace(/\/$/, "");
695
+ if (e && e.provider === "local") {
696
+ const { cacheDir, modelFile } = localPaths(opts.cacheDir ?? localCacheDir());
697
+ const modelPresent = existsSync(modelFile);
698
+ const base = { kind: "local", cacheDir, modelFile, modelPresent };
699
+ if (!modelPresent) return { ...base, reachable: false, embedOk: false, reason: "model file missing" };
700
+ const result = await createProvider(cfg, { cacheDir, loadTransformers: opts.loadTransformers }).embed(["probe"]);
701
+ return { ...base, reachable: result.ok, embedOk: result.ok, reason: result.ok ? null : result.reason };
702
+ }
572
703
  const preset = presetOf(e);
573
704
  if (preset && preset.modelsList === "v1") return probeV1Models(e, root, wantModel, { fetchImpl, timeoutMs });
574
705
  if (preset && e.provider === "voyage") {
@@ -675,6 +806,16 @@ export function doctorReport(cfg, probe, platform = process.platform) {
675
806
  return lines.join("\n");
676
807
  }
677
808
 
809
+ if (embeddings.provider === "local") {
810
+ lines.push(`memory.embeddings is configured: local ${embeddings.model}/${embeddings.dims} (bundled, in-process).`);
811
+ lines.push(`Cache: ${probe.cacheDir}`);
812
+ lines.push(`Model file: ${probe.modelFile} (${probe.modelPresent ? "present" : "missing"})`);
813
+ lines.push(`Dims: ${embeddings.dims}`);
814
+ if (!probe.modelPresent) lines.push("Run doug memory setup to install it.");
815
+ else if (probe.embedOk) lines.push("Test embed: ok");
816
+ else lines.push(`Test embed failed: ${probe.reason || "unknown error"}`);
817
+ return lines.join("\n");
818
+ }
678
819
  lines.push(`memory.embeddings is configured: ${embeddings.provider} ${embeddings.model}/${embeddings.dims} at ${embeddings.baseUrl}.`);
679
820
  if (presetOf(embeddings) && PRESETS[embeddings.preset].modelsList === "v1") {
680
821
  if (!probe.reachable) lines.push(`Not reachable: ${probe.reason || "unknown error"}.`);
@@ -22,7 +22,7 @@ import { execFileSync, spawnSync } from "node:child_process";
22
22
  import { existsSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
23
23
  import { tmpdir } from "node:os";
24
24
  import { join } from "node:path";
25
- import { acceptanceEntries, integrationBranchFor, savePlan } from "./plan.mjs";
25
+ import { acceptanceEntries, integrationBranchFor, integrationWorktreeFor, savePlan, PLAN_RELPATH } from "./plan.mjs";
26
26
  import { removeFinishedWorktrees } from "./worktrees.mjs";
27
27
 
28
28
  export const INTEGRATION_WORKTREE = ".claude/worktrees/doug-integration";
@@ -109,7 +109,7 @@ function verifyViaVendored(script, dir, work, commands, timeoutMs) {
109
109
  const why = r.error && r.error.code === "ETIMEDOUT" ? "timed out" : "gave no readable result";
110
110
  return { error: `the vendored verify.mjs ${why}; land does not re-run the commands` };
111
111
  }
112
- return out.results.map((x) => ({
112
+ const rows = out.results.map((x) => ({
113
113
  command: x.command,
114
114
  ok: x.ok === true,
115
115
  status: x.status,
@@ -118,13 +118,20 @@ function verifyViaVendored(script, dir, work, commands, timeoutMs) {
118
118
  outputTail: x.outputTail || "",
119
119
  ...(out.credited ? { credited: out.credited } : {}),
120
120
  }));
121
+ if (typeof out.lockWaitMs === "number") rows.lockWaitMs = out.lockWaitMs;
122
+ return rows;
121
123
  } catch (err) {
122
124
  return { error: `the vendored verify.mjs could not run: ${err && err.message}` };
123
125
  }
124
126
  }
125
127
 
126
128
  // Returns { ok: true, base, branch, mergeCommit, verify } or { ok: false, reason, verify? }. Never throws on a refusal.
127
- export function landPlan(dir, plan, { timeoutMs = 600000, now = () => new Date(), deleteBranches = false } = {}) {
129
+ // Card parallel-card-queens B5: rebase (set by plan.mjs land --card) rebases the integration branch onto the base
130
+ // when the base has moved, in the worktree that has the branch checked out, and verifies the rebased tree; a
131
+ // conflict is aborted and refused as "rebase-conflict: <files>". ownOnly removes only this plan's own task
132
+ // branches' worktrees and its own integration worktree (no sweep of worktrees merged into base). planRelpath is
133
+ // where plan.landed is saved (the per-card plan under --card). The result carries lockWaitMs from verify.mjs.
134
+ export function landPlan(dir, plan, { timeoutMs = 600000, now = () => new Date(), deleteBranches = false, rebase = false, ownOnly = false, planRelpath = PLAN_RELPATH } = {}) {
128
135
  if (!plan) return { ok: false, reason: "no plan" };
129
136
  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` };
130
137
  if (plan.landed) return { ok: false, reason: `plan was already landed as ${plan.landed.mergeCommit} on ${plan.landed.at}` };
@@ -152,15 +159,17 @@ export function landPlan(dir, plan, { timeoutMs = 600000, now = () => new Date()
152
159
  if (dirty.length) return { ok: false, reason: `tracked files are modified: ${dirty.join(", ")}; commit or stash them first` };
153
160
 
154
161
  // Verify on the branch itself, in the integration worktree if the workflow left it, else a temporary one.
155
- const existing = join(dir, INTEGRATION_WORKTREE);
162
+ const existing = join(dir, integrationWorktreeFor(plan));
156
163
  const reuse = existsSync(existing) && tryGit(existing, ["symbolic-ref", "--short", "-q", "HEAD"]) === branch;
157
164
  let work = reuse ? existing : null;
158
165
  let temp = null;
166
+ const needsRebase = rebase && tryGit(dir, ["merge-base", "--is-ancestor", base, branch]) === null;
159
167
  if (!work) {
160
168
  temp = mkdtempSync(join(tmpdir(), "doug-land-"));
161
169
  work = join(temp, "wt");
162
170
  try {
163
- git(dir, ["worktree", "add", "--detach", work, branch]);
171
+ // A rebase moves the branch ref, so the temporary worktree then has the branch itself checked out.
172
+ git(dir, needsRebase ? ["worktree", "add", work, branch] : ["worktree", "add", "--detach", work, branch]);
164
173
  } catch (err) {
165
174
  rmSync(temp, { recursive: true, force: true });
166
175
  return { ok: false, reason: `could not create a worktree for ${branch}: ${tail(err.stderr || err.message, 3)}` };
@@ -168,7 +177,16 @@ export function landPlan(dir, plan, { timeoutMs = 600000, now = () => new Date()
168
177
  }
169
178
  const verify = [];
170
179
  const acceptance = [];
180
+ let lockWaitMs;
171
181
  try {
182
+ if (needsRebase) {
183
+ const rb = tryGitCapture(work, ["rebase", base]);
184
+ if (!rb.ok) {
185
+ const files = (tryGit(work, ["diff", "--name-only", "--diff-filter=U"]) || "").split("\n").filter(Boolean);
186
+ tryGit(work, ["rebase", "--abort"]);
187
+ return { ok: false, reason: `rebase-conflict: ${files.length ? files.join(", ") : tail(rb.error.stderr || rb.error.message, 3)}` };
188
+ }
189
+ }
172
190
  // card verified-tree-record: install always runs through run(). plan.verify goes through the vendored
173
191
  // verify.mjs when the project has one (it credits a verified-tree record of the branch's tree and writes one
174
192
  // for the main checkout after a green run); otherwise, or when it cannot be read, today's loop runs.
@@ -189,6 +207,7 @@ export function landPlan(dir, plan, { timeoutMs = 600000, now = () => new Date()
189
207
  return { ok: false, reason: `verify failed on ${branch}: ${viaVerify.error}`, verify, acceptance: [] };
190
208
  }
191
209
  if (viaVerify) {
210
+ if (typeof viaVerify.lockWaitMs === "number") lockWaitMs = viaVerify.lockWaitMs;
192
211
  for (const r of viaVerify) {
193
212
  verify.push(r);
194
213
  if (!r.ok) return failVerify(r);
@@ -249,7 +268,7 @@ export function landPlan(dir, plan, { timeoutMs = 600000, now = () => new Date()
249
268
  removedWorktrees.push(existingReal);
250
269
  }
251
270
  const taskBranches = (plan.tasks || []).flatMap((t) => [`doug/task-${t.id}`, ...(typeof t.reuse === "string" ? [t.reuse] : [])]);
252
- const wtResult = removeFinishedWorktrees(dir, { branches: taskBranches, mergedInto: base });
271
+ const wtResult = removeFinishedWorktrees(dir, { branches: taskBranches, mergedInto: ownOnly ? null : base });
253
272
  const worktrees = { removed: [...removedWorktrees, ...wtResult.removed], kept: wtResult.kept };
254
273
 
255
274
  const branchesResult = { deleted: [], kept: [] };
@@ -278,6 +297,6 @@ export function landPlan(dir, plan, { timeoutMs = 600000, now = () => new Date()
278
297
  verify: verify.map((v) => ({ command: v.command, ok: v.ok, durationMs: v.durationMs, ...(v.credited ? { credited: v.credited } : {}) })),
279
298
  acceptance: acceptance.map((a) => ({ text: a.text, command: a.command, ok: a.ok, durationMs: a.durationMs, ...(a.reused ? { reused: a.reused } : {}) })),
280
299
  };
281
- savePlan(dir, { ...plan, landed });
282
- return { ok: true, base, branch, mergeCommit, verify, acceptance, worktrees, branches: branchesResult };
300
+ savePlan(dir, { ...plan, landed }, planRelpath);
301
+ return { ok: true, base, branch, mergeCommit, verify, acceptance, worktrees, branches: branchesResult, ...(lockWaitMs !== undefined ? { lockWaitMs } : {}) };
283
302
  }