@dougharness/cli 1.3.0 → 1.5.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/ablate.js +2 -2
- package/dist/ablate.js.map +1 -1
- package/dist/bin.js +13 -4
- package/dist/bin.js.map +1 -1
- package/dist/board-page.d.ts +5 -1
- package/dist/board-page.js +19 -5
- package/dist/board-page.js.map +1 -1
- package/dist/board-serve.js +34 -10
- package/dist/board-serve.js.map +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +160 -13
- package/dist/board.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +1 -0
- package/node_modules/@dougharness/flow/hooks/register.tsx +18 -2
- package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +94 -7
- package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +31 -4
- package/node_modules/@dougharness/flow/lib/board.mjs +375 -30
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +237 -24
- package/node_modules/@dougharness/flow/lib/cost.mjs +9 -0
- package/node_modules/@dougharness/flow/lib/decisions.mjs +2 -4
- package/node_modules/@dougharness/flow/lib/learn.mjs +95 -54
- package/node_modules/@dougharness/flow/lib/memory.mjs +1403 -44
- package/node_modules/@dougharness/flow/lib/plan.mjs +40 -0
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +355 -13
- package/node_modules/@dougharness/flow/scripts/board.mjs +160 -27
- package/node_modules/@dougharness/flow/scripts/cost.mjs +6 -3
- package/node_modules/@dougharness/flow/scripts/learn.mjs +1 -1
- package/node_modules/@dougharness/flow/scripts/memory.mjs +668 -39
- package/node_modules/@dougharness/flow/scripts/plan.mjs +3 -3
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +11 -4
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +7 -3
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +11 -2
- package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +9 -8
- 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 +3 -3
- package/node_modules/@dougharness/flow/workflows/card-queen.js +238 -25
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +55 -37
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/baseline.mjs +32 -0
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +45 -5
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +6 -2
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +33 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +130 -42
- package/node_modules/@dougharness/gates/scripts/verify.mjs +20 -4
- package/package.json +3 -3
- package/templates/board-app.js +240 -30
|
@@ -10,7 +10,18 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, append
|
|
|
10
10
|
import { join, relative, resolve, sep, dirname } from "node:path";
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
-
import { listOutcomes, listLessons,
|
|
13
|
+
import { listOutcomes, listLessons, readPitfallOccurrences, HISTORY_DIR_RELPATH } from "./memory.mjs";
|
|
14
|
+
|
|
15
|
+
export const RULE_MIN_CARDS = 3;
|
|
16
|
+
export const RULE_MIN_SESSIONS = 2;
|
|
17
|
+
|
|
18
|
+
// The one format of a .claude/rules file (learn's rule proposals and decisions.mjs proposeRule share it): a quoted
|
|
19
|
+
// `paths:` frontmatter list and the body, or the body alone with no paths.
|
|
20
|
+
export function renderRule(paths, body) {
|
|
21
|
+
const list = Array.isArray(paths) ? paths.filter((p) => String(p || "").trim()) : [];
|
|
22
|
+
const trimmed = String(body).replace(/\s+$/, "");
|
|
23
|
+
return list.length ? `---\npaths:\n${list.map((p) => ` - "${p}"`).join("\n")}\n---\n\n${trimmed}\n` : `${trimmed}\n`;
|
|
24
|
+
}
|
|
14
25
|
|
|
15
26
|
// --- collectSignals ----------------------------------------------------------------------------------------
|
|
16
27
|
|
|
@@ -80,50 +91,70 @@ function isGateFailure(row) {
|
|
|
80
91
|
return false;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
94
|
+
// The file a rule proposal's `paths:` frontmatter is scoped by (card memory-failure-lessons, ADR 0019 line 60): the
|
|
95
|
+
// union over a card's history `report` entries of `filesChanged`. An absolute path counts only when it lies under
|
|
96
|
+
// that report's `worktree` and is then made relative to it; a relative path is kept; any path with a `..` segment,
|
|
97
|
+
// and any other absolute path, is dropped. Sorted, de-duplicated; never throws.
|
|
98
|
+
export function cardFilesTouched(dir, card) {
|
|
99
|
+
const out = new Set();
|
|
100
|
+
try {
|
|
101
|
+
if (typeof card !== "string" || !/^[A-Za-z0-9_.-]+$/.test(card) || card.includes("..")) return [];
|
|
102
|
+
const cardDir = join(dir, HISTORY_DIR_RELPATH, card);
|
|
103
|
+
if (!existsSync(cardDir)) return [];
|
|
104
|
+
for (const file of readdirSync(cardDir).filter((f) => f.endsWith(".jsonl")).sort()) {
|
|
105
|
+
let text;
|
|
106
|
+
try {
|
|
107
|
+
text = readFileSync(join(cardDir, file), "utf8");
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const raw of text.split("\n")) {
|
|
112
|
+
if (raw === "") continue;
|
|
113
|
+
let entry;
|
|
114
|
+
let report;
|
|
115
|
+
try {
|
|
116
|
+
entry = JSON.parse(raw);
|
|
117
|
+
if (!entry || entry.kind !== "report") continue;
|
|
118
|
+
report = JSON.parse(entry.text);
|
|
119
|
+
} catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!report || !Array.isArray(report.filesChanged)) continue;
|
|
123
|
+
const worktree = typeof report.worktree === "string" && report.worktree ? report.worktree.replace(/\/+$/, "") : null;
|
|
124
|
+
for (const f of report.filesChanged) {
|
|
125
|
+
if (typeof f !== "string" || f === "" || f.split("/").includes("..")) continue;
|
|
126
|
+
if (f.startsWith("/")) {
|
|
127
|
+
if (worktree && f.startsWith(`${worktree}/`)) out.add(f.slice(worktree.length + 1));
|
|
128
|
+
} else out.add(f.replace(/^\.\//, ""));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
107
131
|
}
|
|
132
|
+
} catch {
|
|
133
|
+
// fail open: whatever was read so far
|
|
108
134
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
135
|
+
return [...out].sort();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// A pitfall lesson's distinct cards, sessions and files from the occurrence ledger (card memory-failure-lessons).
|
|
139
|
+
// Only occurrences count: helpful counters and text similarity play no part (ADR 0019 lines 39 and 60). A null
|
|
140
|
+
// session never counts toward the sessions.
|
|
141
|
+
function collectPitfallSignals(dir, live) {
|
|
142
|
+
const pitfalls = new Map(live.filter((l) => l.kind === "pitfall").map((l) => [l.id, l]));
|
|
143
|
+
const byLesson = new Map();
|
|
144
|
+
for (const o of readPitfallOccurrences(dir)) {
|
|
145
|
+
if (!pitfalls.has(o.lesson)) continue;
|
|
146
|
+
const cur = byLesson.get(o.lesson) || { cards: new Set(), sessions: new Set(), citations: [] };
|
|
147
|
+
if (typeof o.card === "string" && o.card) cur.cards.add(o.card);
|
|
148
|
+
if (typeof o.session === "string" && o.session) cur.sessions.add(o.session);
|
|
149
|
+
cur.citations.push(o.citation);
|
|
150
|
+
byLesson.set(o.lesson, cur);
|
|
124
151
|
}
|
|
125
|
-
|
|
126
|
-
|
|
152
|
+
return [...byLesson.entries()]
|
|
153
|
+
.map(([id, v]) => {
|
|
154
|
+
const cards = [...v.cards].sort();
|
|
155
|
+
return { id, text: pitfalls.get(id).text, cards, sessions: [...v.sessions].sort(), citations: v.citations, files: [...new Set(cards.flatMap((c) => cardFilesTouched(dir, c)))].sort() };
|
|
156
|
+
})
|
|
157
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
127
158
|
}
|
|
128
159
|
|
|
129
160
|
function listTraceFiles(traceDir) {
|
|
@@ -278,7 +309,7 @@ export function collectSignals(m, { dir, traceDir, now = new Date() } = {}) {
|
|
|
278
309
|
helpful: live.filter((l) => l.helpful > 0).length,
|
|
279
310
|
harmful: live.filter((l) => l.harmful > 0).length,
|
|
280
311
|
stale: live.filter((l) => l.stale).length,
|
|
281
|
-
|
|
312
|
+
pitfalls: collectPitfallSignals(dir, live),
|
|
282
313
|
},
|
|
283
314
|
trace,
|
|
284
315
|
};
|
|
@@ -499,15 +530,25 @@ export function proposeChanges(signals, { dir, claudeMd, config, skills = [] } =
|
|
|
499
530
|
let n = 0;
|
|
500
531
|
const nextId = () => String(++n).padStart(2, "0");
|
|
501
532
|
|
|
502
|
-
// promote: a
|
|
503
|
-
|
|
504
|
-
|
|
533
|
+
// promote: a pitfall cited by RULE_MIN_CARDS distinct cards over RULE_MIN_SESSIONS distinct sessions (card
|
|
534
|
+
// memory-failure-lessons, ADR 0019 line 60). With known files it becomes a path-scoped .claude/rules file; with none, a
|
|
535
|
+
// CLAUDE.md Gotchas bullet (refused at the 60-line cap). Feedback lessons have no trigger.
|
|
536
|
+
for (const r of signals.lessons.pitfalls ?? []) {
|
|
537
|
+
if (r.cards.length < RULE_MIN_CARDS || r.sessions.length < RULE_MIN_SESSIONS) continue;
|
|
538
|
+
const evidence = { id: r.id, cards: r.cards, sessions: r.sessions, citations: r.citations, files: r.files };
|
|
539
|
+
const counted = `pitfall ${r.id} cited by ${r.cards.length} cards over ${r.sessions.length} sessions`;
|
|
540
|
+
if (r.files.length) {
|
|
541
|
+
const relPath = `.claude/rules/pitfall-${r.id}.md`;
|
|
542
|
+
const body = `${r.text}\n\nEvidence:\n${r.citations.map((c) => `- ${c}`).join("\n")}`;
|
|
543
|
+
const abs = join(dir, relPath);
|
|
544
|
+
const oldText = existsSync(abs) ? readFileSync(abs, "utf8") : "";
|
|
545
|
+
proposals.push({ id: nextId(), kind: "rule", target: relPath, reason: `${counted}; proposing a rule scoped to the files those cards touched`, evidence, diff: unifiedDiff(relPath, oldText, renderRule(r.files, body)) });
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
505
548
|
if (normalize(claudeMd).includes(normalize(r.text))) continue;
|
|
506
|
-
const heading =
|
|
507
|
-
const
|
|
508
|
-
const
|
|
509
|
-
const reason = `lesson repeated ${r.count}× (ids: ${r.ids.join(", ")}); promoting to CLAUDE.md's ${heading.replace("## ", "")}`;
|
|
510
|
-
const updated = insertBulletUnderHeading(claudeMd, heading, bulletLine);
|
|
549
|
+
const heading = "## Gotchas";
|
|
550
|
+
const reason = `${counted}; no file known, promoting to CLAUDE.md's Gotchas`;
|
|
551
|
+
const updated = insertBulletUnderHeading(claudeMd, heading, `- ${r.text}`);
|
|
511
552
|
if (updated === null) {
|
|
512
553
|
proposals.push({ id: nextId(), kind: "promote", target: "CLAUDE.md", reason: `${reason}, but CLAUDE.md has no "${heading}" section`, evidence, diff: null });
|
|
513
554
|
continue;
|
|
@@ -541,7 +582,7 @@ export function proposeChanges(signals, { dir, claudeMd, config, skills = [] } =
|
|
|
541
582
|
}
|
|
542
583
|
}
|
|
543
584
|
|
|
544
|
-
// delete: a CLAUDE.md rule line referenced by no denial, no
|
|
585
|
+
// delete: a CLAUDE.md rule line referenced by no denial, no pitfall lesson, and no block description, with
|
|
545
586
|
// enough signal density to trust the absence (>=10 outcome rows, >=5 trace files).
|
|
546
587
|
if (signals.outcomes.rows.length >= 10 && signals.trace.files.length >= 5) {
|
|
547
588
|
const blockDescriptions = signals.outcomes.rows.flatMap((r) => (r.blocks || []).map((b) => b.description || ""));
|
|
@@ -556,7 +597,7 @@ export function proposeChanges(signals, { dir, claudeMd, config, skills = [] } =
|
|
|
556
597
|
if (!tokens.length) continue;
|
|
557
598
|
const referenced =
|
|
558
599
|
signals.trace.denials.some((d) => textReferencesTokens(d.detail, tokens)) ||
|
|
559
|
-
signals.lessons.
|
|
600
|
+
(signals.lessons.pitfalls ?? []).some((r) => textReferencesTokens(r.text, tokens)) ||
|
|
560
601
|
blockDescriptions.some((d) => textReferencesTokens(d, tokens));
|
|
561
602
|
if (referenced) continue;
|
|
562
603
|
const updated = removeExactLine(claudeMd, line);
|
|
@@ -564,7 +605,7 @@ export function proposeChanges(signals, { dir, claudeMd, config, skills = [] } =
|
|
|
564
605
|
id: nextId(),
|
|
565
606
|
kind: "delete",
|
|
566
607
|
target: "CLAUDE.md",
|
|
567
|
-
reason: `no denial,
|
|
608
|
+
reason: `no denial, pitfall lesson, or block description across ${signals.outcomes.rows.length} outcome rows and ${signals.trace.files.length} trace files mentions "${tokens.join(", ")}" (rule: ${line.slice(2)})`,
|
|
568
609
|
evidence: { outcomeRows: signals.outcomes.rows.length, traceFiles: signals.trace.files.length, keyPhrase: tokens },
|
|
569
610
|
diff: updated === null ? null : unifiedDiff("CLAUDE.md", claudeMd, updated),
|
|
570
611
|
});
|