@dougharness/cli 1.1.1 → 1.2.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/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 +75 -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 +746 -0
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +368 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +15 -5
- 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 +48 -12
- package/node_modules/@dougharness/flow/lib/plan.mjs +32 -4
- 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 +407 -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 +20 -9
- package/node_modules/@dougharness/flow/scripts/plan.mjs +100 -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/run-report/SKILL.md +6 -6
- package/node_modules/@dougharness/flow/workflows/card-queen.js +807 -0
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +7 -7
- 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/stop-gate.mjs +138 -10
- package/node_modules/@dougharness/gates/scripts/verify.mjs +8 -4
- package/package.json +3 -3
- package/templates/board-app.js +46 -0
|
@@ -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
|
}
|
|
@@ -55,7 +55,8 @@ const LESSON_KINDS = ["feedback", "project", "pitfall", "pattern"];
|
|
|
55
55
|
// row. On a hand row the following columns are NULL because they do not exist on that track — never because
|
|
56
56
|
// they measured zero: run_id, spec_hash, files, size, shape, models, implemented, verified, reviewed,
|
|
57
57
|
// review_issues, review_issue_count, adversary_verdict, adversary_blocked, adversary_source, blocks, passes,
|
|
58
|
-
// fix_passes, stop_reason, in_scope, budget_spent, tokens,
|
|
58
|
+
// fix_passes, stop_reason, stop_class, in_scope, budget_spent, tokens, codex_usd, report_hash, report_path, partial (usd too,
|
|
59
|
+
// except that a hand landing recorded with --cost carries its measured session cost there; card usd-budget-size-class). In particular
|
|
59
60
|
// verified is null on a hand row even though the gate passed: no independent verifier ran on that track, and
|
|
60
61
|
// the gate's own text lives in the `gate` column instead. A consumer that averages fix_passes or counts
|
|
61
62
|
// blocks across both tracks must filter on track. wall_clock, gate, and note only exist on a hand row; they
|
|
@@ -108,6 +109,7 @@ const OUTCOMES_COLUMNS_SQL = `
|
|
|
108
109
|
tokens TEXT,
|
|
109
110
|
usd REAL,
|
|
110
111
|
codex_usd REAL,
|
|
112
|
+
stop_class TEXT,
|
|
111
113
|
commit_sha TEXT,
|
|
112
114
|
track TEXT NOT NULL DEFAULT 'flow',
|
|
113
115
|
wall_clock TEXT,
|
|
@@ -399,6 +401,27 @@ function migrateV7ToV8(db) {
|
|
|
399
401
|
}
|
|
400
402
|
}
|
|
401
403
|
|
|
404
|
+
// v8 had no stop_class column on outcomes (card memory-stop-class): the typed cause of a stop, from
|
|
405
|
+
// task.stopClass, so stops can be counted by cause across runs. Additive like v7->v8: one guarded ALTER, then
|
|
406
|
+
// the version bump; old rows stay null. OUTCOMES_COLUMNS_SQL carries stop_class for a v1 store's rebuild, so
|
|
407
|
+
// this step is a no-op there instead of a duplicate-column error.
|
|
408
|
+
const V9_STOP_COLUMNS = [["stop_class", "TEXT"]];
|
|
409
|
+
|
|
410
|
+
function migrateV8ToV9(db) {
|
|
411
|
+
db.exec("BEGIN");
|
|
412
|
+
try {
|
|
413
|
+
const cols = db.prepare("PRAGMA table_info(outcomes)").all().map((c) => c.name);
|
|
414
|
+
for (const [name, type] of V9_STOP_COLUMNS) {
|
|
415
|
+
if (!cols.includes(name)) db.exec(`ALTER TABLE outcomes ADD COLUMN ${name} ${type};`);
|
|
416
|
+
}
|
|
417
|
+
db.exec("PRAGMA user_version = 9");
|
|
418
|
+
db.exec("COMMIT");
|
|
419
|
+
} catch (err) {
|
|
420
|
+
db.exec("ROLLBACK");
|
|
421
|
+
throw err;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
402
425
|
export function openMemory(dir) {
|
|
403
426
|
const file = memoryPath(dir);
|
|
404
427
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -407,7 +430,7 @@ export function openMemory(dir) {
|
|
|
407
430
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
408
431
|
if (version === 0) {
|
|
409
432
|
db.exec(SCHEMA_SQL);
|
|
410
|
-
db.exec("PRAGMA user_version =
|
|
433
|
+
db.exec("PRAGMA user_version = 9");
|
|
411
434
|
} else if (version === 1) {
|
|
412
435
|
migrateV1ToV2(db);
|
|
413
436
|
migrateV2ToV3(db);
|
|
@@ -416,6 +439,7 @@ export function openMemory(dir) {
|
|
|
416
439
|
migrateV5ToV6(db);
|
|
417
440
|
migrateV6ToV7(db);
|
|
418
441
|
migrateV7ToV8(db);
|
|
442
|
+
migrateV8ToV9(db);
|
|
419
443
|
} else if (version === 2) {
|
|
420
444
|
migrateV2ToV3(db);
|
|
421
445
|
migrateV3ToV4(db);
|
|
@@ -423,29 +447,37 @@ export function openMemory(dir) {
|
|
|
423
447
|
migrateV5ToV6(db);
|
|
424
448
|
migrateV6ToV7(db);
|
|
425
449
|
migrateV7ToV8(db);
|
|
450
|
+
migrateV8ToV9(db);
|
|
426
451
|
} else if (version === 3) {
|
|
427
452
|
migrateV3ToV4(db);
|
|
428
453
|
migrateV4ToV5(db);
|
|
429
454
|
migrateV5ToV6(db);
|
|
430
455
|
migrateV6ToV7(db);
|
|
431
456
|
migrateV7ToV8(db);
|
|
457
|
+
migrateV8ToV9(db);
|
|
432
458
|
} else if (version === 4) {
|
|
433
459
|
migrateV4ToV5(db);
|
|
434
460
|
migrateV5ToV6(db);
|
|
435
461
|
migrateV6ToV7(db);
|
|
436
462
|
migrateV7ToV8(db);
|
|
463
|
+
migrateV8ToV9(db);
|
|
437
464
|
} else if (version === 5) {
|
|
438
465
|
migrateV5ToV6(db);
|
|
439
466
|
migrateV6ToV7(db);
|
|
440
467
|
migrateV7ToV8(db);
|
|
468
|
+
migrateV8ToV9(db);
|
|
441
469
|
} else if (version === 6) {
|
|
442
470
|
migrateV6ToV7(db);
|
|
443
471
|
migrateV7ToV8(db);
|
|
472
|
+
migrateV8ToV9(db);
|
|
444
473
|
} else if (version === 7) {
|
|
445
474
|
migrateV7ToV8(db);
|
|
446
|
-
|
|
475
|
+
migrateV8ToV9(db);
|
|
476
|
+
} else if (version === 8) {
|
|
477
|
+
migrateV8ToV9(db);
|
|
478
|
+
} else if (version !== 9) {
|
|
447
479
|
db.close();
|
|
448
|
-
throw new Error(`memory store ${file} is user_version ${version}; this code understands up to version
|
|
480
|
+
throw new Error(`memory store ${file} is user_version ${version}; this code understands up to version 9`);
|
|
449
481
|
}
|
|
450
482
|
return {
|
|
451
483
|
file,
|
|
@@ -479,7 +511,7 @@ export function probeSqliteFts5() {
|
|
|
479
511
|
// Read-only look at the store for `doug doctor`: creates nothing, migrates nothing, never throws.
|
|
480
512
|
export function inspectMemory(dir) {
|
|
481
513
|
const file = memoryPath(dir);
|
|
482
|
-
const out = { file, exists: false, userVersion: null, maxUserVersion:
|
|
514
|
+
const out = { file, exists: false, userVersion: null, maxUserVersion: 9, quickCheck: null, error: null };
|
|
483
515
|
if (!existsSync(file)) return out;
|
|
484
516
|
out.exists = true;
|
|
485
517
|
let db;
|
|
@@ -719,7 +751,7 @@ export function checkCitation(lesson, checkoutDir, { report } = {}) {
|
|
|
719
751
|
}
|
|
720
752
|
const taskId = rest.slice(0, slash);
|
|
721
753
|
const blockId = rest.slice(slash + 1);
|
|
722
|
-
const found = adversaryBlocks(report).some((b) => b.task === taskId && b.id === blockId);
|
|
754
|
+
const found = adversaryBlocks(report).some((b) => b.task === taskId && (b.id === blockId || b.id === `${taskId}/${blockId}`));
|
|
723
755
|
return found ? { ok: true, reason: "block holds" } : { ok: false, reason: `no block "${blockId}" of task "${taskId}" in this report` };
|
|
724
756
|
}
|
|
725
757
|
const match = citation.match(CITATION_RE);
|
|
@@ -1259,6 +1291,7 @@ export function outcomeRows(report, opts) {
|
|
|
1259
1291
|
passes,
|
|
1260
1292
|
fix_passes: fixPasses,
|
|
1261
1293
|
stop_reason: task.stopReason ?? null,
|
|
1294
|
+
stop_class: task.stopClass ?? null,
|
|
1262
1295
|
in_scope: toIntOrNull(task.inScope),
|
|
1263
1296
|
budget_spent: (task.budget && task.budget.spent) || null,
|
|
1264
1297
|
tokens,
|
|
@@ -1329,7 +1362,7 @@ export function conditionConfigHash(dir) {
|
|
|
1329
1362
|
// null when none was found: null fills every one of the seven condition columns with null, exactly like a row
|
|
1330
1363
|
// that has never been stamped. `backfilled` is always 0 here — only `memory.mjs condition backfill`, which never
|
|
1331
1364
|
// goes through this function, ever sets it to 1.
|
|
1332
|
-
export function handOutcomeRow({ card, commit, wallClock = null, gate = null, note = null, now = nowIso(), condition = null } = {}) {
|
|
1365
|
+
export function handOutcomeRow({ card, commit, wallClock = null, gate = null, note = null, now = nowIso(), condition = null, cost = null } = {}) {
|
|
1333
1366
|
if (!card) throw new Error("handOutcomeRow requires card");
|
|
1334
1367
|
if (!commit) throw new Error("handOutcomeRow requires commit");
|
|
1335
1368
|
return {
|
|
@@ -1357,10 +1390,11 @@ export function handOutcomeRow({ card, commit, wallClock = null, gate = null, no
|
|
|
1357
1390
|
passes: null,
|
|
1358
1391
|
fix_passes: null,
|
|
1359
1392
|
stop_reason: null,
|
|
1393
|
+
stop_class: null,
|
|
1360
1394
|
in_scope: null,
|
|
1361
1395
|
budget_spent: null,
|
|
1362
1396
|
tokens: null,
|
|
1363
|
-
usd: null,
|
|
1397
|
+
usd: typeof cost === "number" && Number.isFinite(cost) ? cost : null,
|
|
1364
1398
|
codex_usd: null,
|
|
1365
1399
|
commit_sha: commit,
|
|
1366
1400
|
track: "hand",
|
|
@@ -1381,7 +1415,7 @@ export function handOutcomeRow({ card, commit, wallClock = null, gate = null, no
|
|
|
1381
1415
|
const OUTCOME_COLUMNS = [
|
|
1382
1416
|
"recorded", "run", "run_id", "report_hash", "report_path", "card", "task", "spec_hash", "files", "size", "shape",
|
|
1383
1417
|
"models", "implemented", "verified", "reviewed", "review_issues", "review_issue_count", "adversary_verdict",
|
|
1384
|
-
"adversary_blocked", "adversary_source", "blocks", "passes", "fix_passes", "stop_reason", "in_scope",
|
|
1418
|
+
"adversary_blocked", "adversary_source", "blocks", "passes", "fix_passes", "stop_reason", "stop_class", "in_scope",
|
|
1385
1419
|
"budget_spent", "tokens", "usd", "codex_usd", "commit_sha", "track", "wall_clock", "gate", "note", "partial",
|
|
1386
1420
|
"harness_commit", "config_hash", "class", "arm", "assigned_by", "explore_probability", "backfilled",
|
|
1387
1421
|
];
|
|
@@ -1507,7 +1541,7 @@ function reflectionRowToObject(row) {
|
|
|
1507
1541
|
// Deterministic, no LLM. harmful is an explicit mark (card reflect-harmful-irrelevant-lessons): only an id in
|
|
1508
1542
|
// `harmfulIds` (the lead's judgement that the lesson misled the work) gets harmful +1, once however often it is
|
|
1509
1543
|
// named or used; a real block alone bumps nothing. For each task whose memoryUsed is non-empty, a real block earns
|
|
1510
|
-
// no helpful (real wins even when the task also verified), else a first-verified task bumps helpful on every used
|
|
1544
|
+
// no helpful (real wins even when the task also verified; except on the hand track, whose reviewer blocks never withhold it), else a first-verified task bumps helpful on every used
|
|
1511
1545
|
// lesson not named harmful; neither verified nor real does nothing. An id naming no lesson is reported in
|
|
1512
1546
|
// `unknown`, never an error. apply:false (a dry run's preview) computes the same result without writing anything.
|
|
1513
1547
|
function computeReflectCounters(m, blocks, report, { apply, harmfulIds = [] }) {
|
|
@@ -1527,7 +1561,9 @@ function computeReflectCounters(m, blocks, report, { apply, harmfulIds = [] }) {
|
|
|
1527
1561
|
for (const task of level.tasks || []) {
|
|
1528
1562
|
const used = Array.isArray(task.memoryUsed) ? task.memoryUsed : [];
|
|
1529
1563
|
if (!used.length) continue;
|
|
1530
|
-
|
|
1564
|
+
// A hand report's blocks are reviewer blockers a later round fixed (card reflect-review-key-text): they are no
|
|
1565
|
+
// evidence the lesson misled, so they never withhold helpful; only --harmful marks that.
|
|
1566
|
+
const real = report.track !== "hand" && blocks.some((b) => b.task === task.id && b.class === "real");
|
|
1531
1567
|
if (real) {
|
|
1532
1568
|
for (const id of used) {
|
|
1533
1569
|
if (!getLesson(m, id) && !named.has(id)) unknown.push(id);
|
|
@@ -1595,7 +1631,7 @@ export function buildReflectPrompt(report, { blocks = [], plan = null, existingL
|
|
|
1595
1631
|
if (Array.isArray(task.verifierFindings) && task.verifierFindings.length) lines.push(` verifier findings: ${JSON.stringify(task.verifierFindings)}`);
|
|
1596
1632
|
if (taskBlocks.length) {
|
|
1597
1633
|
lines.push(" blocks:");
|
|
1598
|
-
for (const b of taskBlocks) lines.push(` - report:${task.id}/${b.id} class=${b.class || "unclassified"} reason=${b.reason || "none"}`);
|
|
1634
|
+
for (const b of taskBlocks) lines.push(` - report:${b.id.startsWith(`${task.id}/`) ? b.id : `${task.id}/${b.id}`} class=${b.class || "unclassified"} reason=${b.reason || "none"}`);
|
|
1599
1635
|
}
|
|
1600
1636
|
lines.push(` memoryUsed: ${used.length ? used.join(", ") : "none"}`);
|
|
1601
1637
|
}
|
|
@@ -9,6 +9,13 @@ import { isModelName, EFFORTS } from "./models.mjs";
|
|
|
9
9
|
|
|
10
10
|
export const PLAN_RELPATH = ".doug/plan.json";
|
|
11
11
|
export const REPORT_RELPATH = ".doug/.state/last-report.json";
|
|
12
|
+
// Per-card plans (card parallel-card-queens): a card run as its own queen keeps its plan at the tracked
|
|
13
|
+
// .doug/plans/<id>.json and its report at .doug/.state/reports/<id>/last-report.json.
|
|
14
|
+
export const PLANS_RELPATH = ".doug/plans";
|
|
15
|
+
export const REPORTS_RELPATH = ".doug/.state/reports";
|
|
16
|
+
export const DEFAULT_INTEGRATION_WORKTREE = ".claude/worktrees/doug-integration";
|
|
17
|
+
export const planPathFor = (card) => `${PLANS_RELPATH}/${card}.json`;
|
|
18
|
+
export const reportPathFor = (card) => `${REPORTS_RELPATH}/${card}/last-report.json`;
|
|
12
19
|
// One draft per card of a batch (card parallel-cards): the planner for card <id> writes .doug/.state/drafts/<id>.json
|
|
13
20
|
// and `plan.mjs merge` joins them into the plan file.
|
|
14
21
|
export const DRAFTS_RELPATH = ".doug/.state/drafts";
|
|
@@ -55,8 +62,24 @@ export function draftPath(card) {
|
|
|
55
62
|
return `${DRAFTS_RELPATH}/${card}.json`;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
// The plan a card's commands read: its per-card plan when one exists; else the single .doug/plan.json, but only
|
|
66
|
+
// when no card is given or that plan is this card's (plan.card equals it, or plan.cards includes it), so a stale
|
|
67
|
+
// plan for another card is never read. Null otherwise, and when neither exists.
|
|
68
|
+
export function planFor(dir, card) {
|
|
69
|
+
const hasCard = typeof card === "string" && card !== "";
|
|
70
|
+
if (hasCard && existsSync(join(dir, planPathFor(card)))) return loadPlan(dir, planPathFor(card));
|
|
71
|
+
const plan = loadPlan(dir);
|
|
72
|
+
if (!plan || !hasCard) return plan;
|
|
73
|
+
return plan.card === card || (Array.isArray(plan.cards) && plan.cards.includes(card)) ? plan : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The plan's integration worktree: its own when set (a per-card run), else the fixed shared path.
|
|
77
|
+
export function integrationWorktreeFor(plan) {
|
|
78
|
+
return (plan && plan.integrationWorktree) || DEFAULT_INTEGRATION_WORKTREE;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function savePlan(dir, plan, relpath = PLAN_RELPATH) {
|
|
82
|
+
const file = join(dir, relpath);
|
|
60
83
|
mkdirSync(dirname(file), { recursive: true });
|
|
61
84
|
const tmp = file + "." + process.pid + ".tmp";
|
|
62
85
|
writeFileSync(tmp, JSON.stringify(plan, null, 2) + "\n");
|
|
@@ -76,8 +99,8 @@ export function withSpecHashes(plan) {
|
|
|
76
99
|
return { ...plan, tasks: plan.tasks.map((t) => ({ ...t, specHash: specHash(t) })) };
|
|
77
100
|
}
|
|
78
101
|
|
|
79
|
-
export function loadReport(dir) {
|
|
80
|
-
const file = join(dir,
|
|
102
|
+
export function loadReport(dir, relpath = REPORT_RELPATH) {
|
|
103
|
+
const file = join(dir, relpath);
|
|
81
104
|
if (!existsSync(file)) return null;
|
|
82
105
|
try {
|
|
83
106
|
return unwrapReport(JSON.parse(readFileSync(file, "utf8")));
|
|
@@ -396,6 +419,11 @@ export function validatePlan(plan) {
|
|
|
396
419
|
});
|
|
397
420
|
}
|
|
398
421
|
if (!Array.isArray(plan.verify) || !plan.verify.every((v) => typeof v === "string" && v.trim())) errors.push("verify must be an array of command strings");
|
|
422
|
+
if (plan.integrationWorktree !== undefined) {
|
|
423
|
+
const w = plan.integrationWorktree;
|
|
424
|
+
const under = typeof w === "string" && w.startsWith(".claude/worktrees/") && w.length > ".claude/worktrees/".length && !w.split("/").includes("..");
|
|
425
|
+
if (!under) errors.push("integrationWorktree must be a path under .claude/worktrees/ (the gate only recognises worktrees there)");
|
|
426
|
+
}
|
|
399
427
|
if (plan.card !== undefined && (typeof plan.card !== "string" || !plan.card.trim())) errors.push("card must be a non-empty string when present");
|
|
400
428
|
// A batch (several cards in one plan) lists its cards; every task then names the card it belongs to.
|
|
401
429
|
let cards = null;
|
|
@@ -198,15 +198,15 @@ export function retireTaskWorktrees(dir, { tasks = [], saveDir }) {
|
|
|
198
198
|
return result;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
// Retires the integration worktree at .claude/worktrees/doug-integration, if one is registered (card
|
|
201
|
+
// Retires the integration worktree at `path` (default .claude/worktrees/doug-integration; a per-card run passes its own), if one is registered (card
|
|
202
202
|
// integration-worktree-stale, 2026-09-08): a leftover from an abandoned run must not survive into the next one,
|
|
203
203
|
// since the workflow's own leftover-vs-this-run's-own check (integratePrompt's firstIntegration) only runs inside
|
|
204
204
|
// a run already underway. Removed with --force; the branch is never renamed or deleted (the workflow's
|
|
205
205
|
// `git worktree add -B` on the next run's first integration resets it from the base branch). A worktree that is
|
|
206
206
|
// not there is not an error.
|
|
207
207
|
// Returns { removed: string | null } - the path removed, or null when none was registered.
|
|
208
|
-
export function retireIntegrationWorktree(dir) {
|
|
209
|
-
const target =
|
|
208
|
+
export function retireIntegrationWorktree(dir, path = ".claude/worktrees/doug-integration") {
|
|
209
|
+
const target = resolve(realpathOrSelf(dir), path);
|
|
210
210
|
const match = taskWorktrees(dir).find((wt) => realpathOrSelf(wt.path) === realpathOrSelf(target));
|
|
211
211
|
if (!match) return { removed: null };
|
|
212
212
|
const removal = tryGit(dir, ["worktree", "remove", "--force", match.path]);
|