@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.
- package/dist/board-serve.js +40 -6
- package/dist/board-serve.js.map +1 -1
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/memory-setup.d.ts +7 -0
- package/dist/memory-setup.js +70 -46
- package/dist/memory-setup.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +6 -2
- package/node_modules/@dougharness/flow/agents/coder.md +23 -0
- 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/agents/spec-writer.md +23 -0
- package/node_modules/@dougharness/flow/agents/tester.md +21 -0
- package/node_modules/@dougharness/flow/hooks/hooks.json +4 -1
- package/node_modules/@dougharness/flow/hooks/register.tsx +94 -0
- package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +117 -0
- package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +17 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +91 -10
- package/node_modules/@dougharness/flow/lib/card-land.mjs +115 -0
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +778 -0
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +393 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +15 -5
- package/node_modules/@dougharness/flow/lib/decisions.mjs +4 -2
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +144 -3
- package/node_modules/@dougharness/flow/lib/land.mjs +27 -8
- package/node_modules/@dougharness/flow/lib/memory.mjs +406 -76
- package/node_modules/@dougharness/flow/lib/plan.mjs +33 -5
- package/node_modules/@dougharness/flow/lib/worktrees.mjs +3 -3
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +440 -0
- package/node_modules/@dougharness/flow/scripts/board.mjs +107 -32
- package/node_modules/@dougharness/flow/scripts/cost.mjs +16 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +33 -10
- package/node_modules/@dougharness/flow/scripts/plan.mjs +104 -30
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +8 -2
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +30 -10
- package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +80 -0
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +6 -6
- package/node_modules/@dougharness/flow/workflows/card-queen.js +846 -0
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +63 -23
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/anchor.mjs +29 -4
- package/node_modules/@dougharness/gates/lib/bash-rules.mjs +196 -6
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +40 -8
- package/node_modules/@dougharness/gates/lib/proposals.mjs +41 -5
- package/node_modules/@dougharness/gates/lib/scope.mjs +43 -4
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/reanchor.mjs +2 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +119 -15
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +156 -14
- package/node_modules/@dougharness/gates/scripts/verify.mjs +8 -4
- package/package.json +3 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
}
|