dual-brain 7.1.23 → 7.1.24
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/bin/dual-brain.mjs +112 -111
- package/package.json +6 -2
- package/src/calibration.mjs +148 -0
- package/src/ledger.mjs +196 -0
- package/src/living-docs.mjs +210 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -1483,9 +1483,9 @@ function detectInterruptedWork(sessions, cwd) {
|
|
|
1483
1483
|
* Shows: "● Claude ● OpenAI ⚖️ Balanced"
|
|
1484
1484
|
* Uses ANSI color codes for the dots — no dollar amounts or usage bars.
|
|
1485
1485
|
*/
|
|
1486
|
-
function buildProviderStatusLine(profile, auth
|
|
1487
|
-
const GREEN = '
|
|
1488
|
-
const RED = '
|
|
1486
|
+
function buildProviderStatusLine(profile, auth) {
|
|
1487
|
+
const GREEN = '\x1b[32m●\x1b[0m';
|
|
1488
|
+
const RED = '\x1b[31m●\x1b[0m';
|
|
1489
1489
|
|
|
1490
1490
|
const claudeDot = auth.claude.found ? GREEN : RED;
|
|
1491
1491
|
const openaiDot = auth.openai.found ? GREEN : RED;
|
|
@@ -1498,30 +1498,11 @@ function buildProviderStatusLine(profile, auth, maxWidth = 54) {
|
|
|
1498
1498
|
'solo-claude': '⚡ Fast',
|
|
1499
1499
|
'solo-openai': '⚡ Fast',
|
|
1500
1500
|
};
|
|
1501
|
-
const WORK_STYLE_TIPS = {
|
|
1502
|
-
'auto': 'adapts routing by task risk',
|
|
1503
|
-
'cost-saver': 'single model, minimal reviews',
|
|
1504
|
-
'balanced': 'smart routing, reviews when needed',
|
|
1505
|
-
'quality-first': 'dual-brain on everything important',
|
|
1506
|
-
'solo-claude': 'Claude only, no GPT dispatch',
|
|
1507
|
-
'solo-openai': 'OpenAI only, no Claude dispatch',
|
|
1508
|
-
};
|
|
1509
1501
|
const bias = profile?.bias || profile?.mode || 'balanced';
|
|
1510
1502
|
const label = WORK_STYLE_LABELS[bias] || '⚖️ Balanced';
|
|
1511
|
-
const fullTip = WORK_STYLE_TIPS[bias] || 'smart routing, reviews when needed';
|
|
1512
|
-
|
|
1513
|
-
// Trim tip to fit within box width (measure visible chars: strip ANSI + variation selectors)
|
|
1514
|
-
const labelPlain = label.replace(/[︀-️]/g, '').replace(/[[0-9;]*m/g, '');
|
|
1515
|
-
const prefixLen = ('● Claude ● OpenAI ' + labelPlain + ' — ').length;
|
|
1516
|
-
const tipMax = maxWidth - prefixLen;
|
|
1517
|
-
const tip = tipMax >= 6
|
|
1518
|
-
? (fullTip.length > tipMax ? fullTip.slice(0, tipMax - 1) + '…' : fullTip)
|
|
1519
|
-
: '';
|
|
1520
1503
|
|
|
1521
|
-
|
|
1522
|
-
return `${claudeDot} Claude ${openaiDot} OpenAI ${label}${suffix}`;
|
|
1504
|
+
return `${claudeDot} Claude ${openaiDot} OpenAI ${label}`;
|
|
1523
1505
|
}
|
|
1524
|
-
|
|
1525
1506
|
/**
|
|
1526
1507
|
* Render a box row padded to inner width W (stripping ANSI for length calculation).
|
|
1527
1508
|
* Returns a string like: "│ content padded to W │"
|
|
@@ -1609,25 +1590,6 @@ async function mainScreen(rl, ask) {
|
|
|
1609
1590
|
|
|
1610
1591
|
const row = (content) => makeBoxRow(content, W);
|
|
1611
1592
|
|
|
1612
|
-
// ── Header: one line above the box ────────────────────────────────────────
|
|
1613
|
-
process.stdout.write(`\n🧠 dual-brain v${version}\n`);
|
|
1614
|
-
{
|
|
1615
|
-
let gitName = '';
|
|
1616
|
-
try {
|
|
1617
|
-
const { execSync } = await import('node:child_process');
|
|
1618
|
-
gitName = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
|
|
1619
|
-
} catch { /* ignore */ }
|
|
1620
|
-
if (gitName) {
|
|
1621
|
-
const hour = new Date().getHours();
|
|
1622
|
-
let greet;
|
|
1623
|
-
if (hour >= 5 && hour <= 11) greet = 'Good morning';
|
|
1624
|
-
else if (hour >= 12 && hour <= 16) greet = 'Good afternoon';
|
|
1625
|
-
else if (hour >= 17 && hour <= 21) greet = 'Good evening';
|
|
1626
|
-
else greet = 'Late night';
|
|
1627
|
-
process.stdout.write(`\x1b[2m${greet}, ${gitName}\x1b[0m\n`);
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
1593
|
// ── Continuation card (interrupted work) ─────────────────────────────────
|
|
1632
1594
|
if (interrupted) {
|
|
1633
1595
|
const ctop = `┌${'─'.repeat(boxW - 2)}┐`;
|
|
@@ -1706,15 +1668,74 @@ async function mainScreen(rl, ask) {
|
|
|
1706
1668
|
// 's' → fall through to normal dashboard
|
|
1707
1669
|
}
|
|
1708
1670
|
|
|
1709
|
-
// ──
|
|
1710
|
-
const providerLine = buildProviderStatusLine(profile, auth
|
|
1671
|
+
// ── Box 1 — Header row data ─────────────────────────────────────────────
|
|
1672
|
+
const providerLine = buildProviderStatusLine(profile, auth);
|
|
1711
1673
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1674
|
+
// ── Box 2 — Workspace: gather git data ───────────────────────────────────
|
|
1675
|
+
let gitBranch = 'unknown';
|
|
1676
|
+
let gitUncommitted = 0;
|
|
1677
|
+
let gitAheadCount = 0;
|
|
1678
|
+
let gitLastMsg = '';
|
|
1679
|
+
let gitLastAgo = '';
|
|
1680
|
+
|
|
1681
|
+
try {
|
|
1682
|
+
gitBranch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
|
|
1683
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1684
|
+
}).trim() || 'unknown';
|
|
1685
|
+
} catch {}
|
|
1686
|
+
|
|
1687
|
+
try {
|
|
1688
|
+
const status = execSync('git status --porcelain 2>/dev/null', {
|
|
1689
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1690
|
+
});
|
|
1691
|
+
gitUncommitted = status.trim().split('\n').filter(Boolean).length;
|
|
1692
|
+
} catch {}
|
|
1693
|
+
|
|
1694
|
+
try {
|
|
1695
|
+
const aheadOut = execSync('git rev-list @{u}..HEAD 2>/dev/null | wc -l', {
|
|
1696
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1697
|
+
});
|
|
1698
|
+
gitAheadCount = parseInt(aheadOut.trim(), 10) || 0;
|
|
1699
|
+
} catch {}
|
|
1700
|
+
|
|
1701
|
+
try {
|
|
1702
|
+
const logOut = execSync('git log -1 --format="%s|%ct" 2>/dev/null', {
|
|
1703
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1704
|
+
}).trim();
|
|
1705
|
+
if (logOut) {
|
|
1706
|
+
const [msg, ts] = logOut.split('|');
|
|
1707
|
+
gitLastMsg = (msg || '').slice(0, 38);
|
|
1708
|
+
const ageMs = Date.now() - (parseInt(ts, 10) * 1000);
|
|
1709
|
+
const ageMin = Math.floor(ageMs / 60000);
|
|
1710
|
+
if (ageMin < 60) gitLastAgo = `${ageMin}m ago`;
|
|
1711
|
+
else if (ageMin < 1440) gitLastAgo = `${Math.floor(ageMin / 60)}h ago`;
|
|
1712
|
+
else gitLastAgo = `${Math.floor(ageMin / 1440)}d ago`;
|
|
1713
|
+
}
|
|
1714
|
+
} catch {}
|
|
1715
|
+
|
|
1716
|
+
// ── Box 2 rows ────────────────────────────────────────────────────────────
|
|
1717
|
+
const uncommittedPart = gitUncommitted > 0 ? ` · ${gitUncommitted} uncommitted` : '';
|
|
1718
|
+
const aheadPart = gitAheadCount > 0 ? ` · ${gitAheadCount} ahead` : '';
|
|
1719
|
+
const workspaceLine1 = `${gitBranch}${uncommittedPart}${aheadPart}`;
|
|
1720
|
+
const workspaceLine2 = gitLastMsg
|
|
1721
|
+
? `Last: ${gitLastMsg} (${gitLastAgo})`
|
|
1722
|
+
: '';
|
|
1723
|
+
|
|
1724
|
+
// Open PRs
|
|
1725
|
+
const repoState = detectRepoState(cwd);
|
|
1726
|
+
const openPRs = await detectOpenPRs(cwd);
|
|
1727
|
+
|
|
1728
|
+
const workspaceRows = [row(workspaceLine1)];
|
|
1729
|
+
if (workspaceLine2) workspaceRows.push(row(workspaceLine2));
|
|
1730
|
+
if (openPRs.length > 0) {
|
|
1731
|
+
workspaceRows.push(row(`${openPRs.length} open PR${openPRs.length === 1 ? '' : 's'}`));
|
|
1715
1732
|
}
|
|
1716
1733
|
|
|
1717
|
-
// ──
|
|
1734
|
+
// ── Box 3 — Awareness: observer + roadmap + risk ──────────────────────────
|
|
1735
|
+
let awarenessLine1 = '\x1b[2m💡\x1b[0m Ready to work';
|
|
1736
|
+
let awarenessLine2 = '\x1b[2m📋 No roadmap yet\x1b[0m';
|
|
1737
|
+
let awarenessLine3 = '\x1b[32m✓\x1b[0m No risk flags';
|
|
1738
|
+
|
|
1718
1739
|
let quickObservations = [];
|
|
1719
1740
|
try {
|
|
1720
1741
|
const observerMod = await import('../src/observer.mjs');
|
|
@@ -1724,64 +1745,39 @@ async function mainScreen(rl, ask) {
|
|
|
1724
1745
|
const sorted = [...quickState.observations].sort(
|
|
1725
1746
|
(a, b) => (PRIO[a.priority] ?? 2) - (PRIO[b.priority] ?? 2)
|
|
1726
1747
|
);
|
|
1727
|
-
quickObservations = sorted.slice(0,
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1748
|
+
quickObservations = sorted.slice(0, 3);
|
|
1749
|
+
const top = quickObservations[0];
|
|
1750
|
+
if (top) {
|
|
1751
|
+
const prefix = top.priority === 'high' ? '🔴' : top.priority === 'medium' ? '🟡' : '\x1b[2m💡\x1b[0m';
|
|
1752
|
+
awarenessLine1 = `${prefix} ${top.message}`;
|
|
1753
|
+
}
|
|
1754
|
+
const hasHighRisk = quickObservations.some(o => o.priority === 'high');
|
|
1755
|
+
if (hasHighRisk) {
|
|
1756
|
+
awarenessLine3 = '\x1b[31m⚠\x1b[0m Risk flags detected — run: dual-brain review';
|
|
1734
1757
|
}
|
|
1735
1758
|
}
|
|
1736
|
-
} catch { /* non-fatal —
|
|
1737
|
-
|
|
1738
|
-
// ── Action cards (git state + open PRs) ──────────────────────────────────
|
|
1739
|
-
const repoState = detectRepoState(cwd);
|
|
1740
|
-
const openPRs = await detectOpenPRs(cwd);
|
|
1741
|
-
const actionRows = buildActionRows(repoState, row, openPRs);
|
|
1742
|
-
|
|
1743
|
-
// ── High-priority observer action cards ───────────────────────────────────
|
|
1744
|
-
if (quickObservations.some(o => o.priority === 'high')) {
|
|
1745
|
-
const DIM = '\x1b[2m';
|
|
1746
|
-
const RESET = '\x1b[0m';
|
|
1747
|
-
actionRows.push(row(`${DIM}[r] Security review [t] Run tests [c] Commit${RESET}`));
|
|
1748
|
-
}
|
|
1759
|
+
} catch { /* non-fatal — observer may not exist */ }
|
|
1749
1760
|
|
|
1750
|
-
//
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
const idx = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
1762
|
-
recentFiles = idx[mostRecent.id]?.files || [];
|
|
1763
|
-
} catch {}
|
|
1764
|
-
const related = findRelatedSessions(recentPrompt, recentFiles, cwd);
|
|
1765
|
-
if (related.length > 0) {
|
|
1766
|
-
const relAgeLabel = (isoDate) => {
|
|
1767
|
-
if (!isoDate) return '';
|
|
1768
|
-
const diff = Date.now() - Date.parse(isoDate);
|
|
1769
|
-
const days = Math.floor(diff / 86400000);
|
|
1770
|
-
const hours = Math.floor(diff / 3600000);
|
|
1771
|
-
if (days >= 1) return `${days}d`;
|
|
1772
|
-
return `${hours}h ago`;
|
|
1773
|
-
};
|
|
1774
|
-
const relatedParts = related.slice(0, 2).map(r => {
|
|
1775
|
-
const age = relAgeLabel(r.date);
|
|
1776
|
-
return age ? `${r.smartName} (${age})` : r.smartName;
|
|
1777
|
-
});
|
|
1778
|
-
const DIM = '\x1b[2m';
|
|
1779
|
-
const RESET = '\x1b[0m';
|
|
1780
|
-
actionRows.push(row(`${DIM}📎 Related: ${relatedParts.join(', ')}${RESET}`));
|
|
1761
|
+
// Try roadmap file
|
|
1762
|
+
try {
|
|
1763
|
+
const roadmapPath = join(cwd, '.dual-brain', 'roadmap.md');
|
|
1764
|
+
if (existsSync(roadmapPath)) {
|
|
1765
|
+
const roadmapText = readFileSync(roadmapPath, 'utf8');
|
|
1766
|
+
const lines = roadmapText.split('\n').filter(Boolean);
|
|
1767
|
+
// Skip heading lines, grab first non-heading line
|
|
1768
|
+
const firstItem = lines.find(l => !l.startsWith('#') && l.trim().length > 0);
|
|
1769
|
+
if (firstItem) {
|
|
1770
|
+
const clean = firstItem.replace(/^[-*>]+\s*/, '').trim().slice(0, 45);
|
|
1771
|
+
awarenessLine2 = `\x1b[2m📋\x1b[0m ${clean}`;
|
|
1781
1772
|
}
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1773
|
+
}
|
|
1774
|
+
} catch { /* non-fatal */ }
|
|
1775
|
+
|
|
1776
|
+
const awarenessRows = [
|
|
1777
|
+
row(awarenessLine1),
|
|
1778
|
+
row(awarenessLine2),
|
|
1779
|
+
row(awarenessLine3),
|
|
1780
|
+
];
|
|
1785
1781
|
|
|
1786
1782
|
// ── Sessions section ──────────────────────────────────────────────────────
|
|
1787
1783
|
const sessionRows = [];
|
|
@@ -1837,23 +1833,28 @@ async function mainScreen(rl, ask) {
|
|
|
1837
1833
|
});
|
|
1838
1834
|
}
|
|
1839
1835
|
|
|
1840
|
-
// ──
|
|
1841
|
-
const actionsContent = '
|
|
1836
|
+
// ── Box 5 — Input bar ──────────────────────────────────────────────────
|
|
1837
|
+
const actionsContent = '> type anything... [s] settings [/] search [q] quit';
|
|
1842
1838
|
const actionsRow = row(actionsContent);
|
|
1843
1839
|
|
|
1844
|
-
// ── Print the full box
|
|
1845
|
-
//
|
|
1846
|
-
|
|
1840
|
+
// ── Print the full 5-box layout ───────────────────────────────────────────
|
|
1841
|
+
// Box 1: header (title + provider dots + work style)
|
|
1842
|
+
// Box 2: workspace (branch · uncommitted · ahead, last commit, open PRs)
|
|
1843
|
+
// Box 3: awareness (observer, roadmap, risk)
|
|
1844
|
+
// Box 4: sessions
|
|
1845
|
+
// Box 5: input bar
|
|
1847
1846
|
const lines = [
|
|
1848
1847
|
top,
|
|
1849
|
-
|
|
1850
|
-
|
|
1848
|
+
row(`🧠 dual-brain v${version}`),
|
|
1849
|
+
row(providerLine),
|
|
1850
|
+
sep,
|
|
1851
|
+
...workspaceRows,
|
|
1852
|
+
sep,
|
|
1853
|
+
...awarenessRows,
|
|
1851
1854
|
sep,
|
|
1852
1855
|
...sessionRows,
|
|
1853
1856
|
sep,
|
|
1854
1857
|
actionsRow,
|
|
1855
|
-
sep,
|
|
1856
|
-
poweredByRow,
|
|
1857
1858
|
bot,
|
|
1858
1859
|
];
|
|
1859
1860
|
// ── Stale session hint ──────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.24",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"./session": "./src/session.mjs",
|
|
20
20
|
"./decompose": "./src/decompose.mjs",
|
|
21
21
|
"./brief": "./src/brief.mjs",
|
|
22
|
-
"./redact": "./src/redact.mjs"
|
|
22
|
+
"./redact": "./src/redact.mjs",
|
|
23
|
+
"./calibration": "./src/calibration.mjs"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
25
26
|
"claude-code",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"src/decompose.mjs",
|
|
59
60
|
"src/brief.mjs",
|
|
60
61
|
"src/redact.mjs",
|
|
62
|
+
"src/calibration.mjs",
|
|
61
63
|
"src/pipeline.mjs",
|
|
62
64
|
"src/context.mjs",
|
|
63
65
|
"src/outcome.mjs",
|
|
@@ -67,8 +69,10 @@
|
|
|
67
69
|
"src/receipt.mjs",
|
|
68
70
|
"src/failure-memory.mjs",
|
|
69
71
|
"src/index.mjs",
|
|
72
|
+
"src/ledger.mjs",
|
|
70
73
|
"src/intelligence.mjs",
|
|
71
74
|
"src/tui.mjs",
|
|
75
|
+
"src/living-docs.mjs",
|
|
72
76
|
"src/install-hooks.mjs",
|
|
73
77
|
"src/update-check.mjs",
|
|
74
78
|
"bin/*.mjs",
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// User calibration module — tracks specificity, corrections, and autonomy signals
|
|
2
|
+
// to adapt dual-brain behavior to any user from vague vibe-coder to precise expert.
|
|
3
|
+
|
|
4
|
+
const FILE_REF_RE = /(?:src\/|\.mjs|\.tsx?|\.jsx?|\.json|\.ya?ml|\.sh|line\s+\d+|\bL\d+\b)/i;
|
|
5
|
+
const TECH_TERM_RE = /\b(?:regex|middleware|api|endpoint|refactor|migration|schema|auth(?:entication)?|jwt|token|hook|dispatch|pipeline|module|function|class|interface|type|import|export|async|await|promise|callback|handler|router|controller|service|repository|factory|singleton|decorator|mixin|proxy|guard|interceptor|serializer|validator|transformer|adapter|facade|strategy|observer|subscriber|emitter|stream|buffer|cache|queue|worker|thread|mutex|semaphore|socket|websocket|http|grpc|graphql|rest|orm|sql|nosql|index|query|transaction|migration|seed|fixture|mock|stub|spy|assertion|coverage|lint|typecheck|bundle|compile|transpile|minify|tree.shake|dead.code|chunk|lazy.load|ssr|csr|hydrat)\b/i;
|
|
6
|
+
const VAGUE_RE = /\b(?:idk|just|make\s+it|fix\s+it|whatever|vibes?|better|nicer|faster|cleaner|improve|help|do\s+it|yeah|sure|ok|okay|hmm|uh|er|um)\b/i;
|
|
7
|
+
const AUTONOMY_HIGH_RE = /\b(?:just\s+do\s+it|go(?:\s+ahead)?|build\s+it|ship\s+it|run\s+it|execute|do\s+it|proceed|continue|carry\s+on|handle\s+it|take\s+care|make\s+it\s+happen|yolo)\b/i;
|
|
8
|
+
const QUESTION_RE = /\?|^(?:how|what|why|when|where|which|should|can|could|would|is|are|do|does|did|will|won't|don't)\b/i;
|
|
9
|
+
const CORRECTION_RE = /^(?:no[,.]?|not\s|wrong|stop|don't|that'?s?\s+not|i\s+said|i\s+meant)\b|(?:\binstead\b|\brather\b|\bactually\b)/i;
|
|
10
|
+
|
|
11
|
+
export function analyzeInput(input) {
|
|
12
|
+
const text = (input || '').trim();
|
|
13
|
+
const lower = text.toLowerCase();
|
|
14
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
15
|
+
const wordCount = words.length;
|
|
16
|
+
|
|
17
|
+
const hasFileRefs = FILE_REF_RE.test(text);
|
|
18
|
+
const hasTechTerms = TECH_TERM_RE.test(text);
|
|
19
|
+
const hasExactInstructions = /\b(?:step\s+\d|first[,\s]|then[,\s]|finally[,\s]|specifically|exactly|must|should\s+(?:use|call|return|handle)|(?:use|call|return|throw|emit|dispatch)\s+\w)/i.test(text);
|
|
20
|
+
const isVague = VAGUE_RE.test(lower) || wordCount <= 3;
|
|
21
|
+
|
|
22
|
+
let specificity;
|
|
23
|
+
if (hasFileRefs || /\bline\s*\d+\b|\bL\d+\b/i.test(text) || (hasTechTerms && hasExactInstructions)) {
|
|
24
|
+
specificity = 5;
|
|
25
|
+
} else if (hasTechTerms && wordCount >= 6) {
|
|
26
|
+
specificity = 4;
|
|
27
|
+
} else if (!isVague && wordCount >= 8) {
|
|
28
|
+
specificity = 3;
|
|
29
|
+
} else if (wordCount >= 4 && !isVague) {
|
|
30
|
+
specificity = 2;
|
|
31
|
+
} else {
|
|
32
|
+
specificity = 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
specificity,
|
|
37
|
+
signals: {
|
|
38
|
+
hasFileRefs,
|
|
39
|
+
hasTechTerms,
|
|
40
|
+
hasExactInstructions,
|
|
41
|
+
isVague,
|
|
42
|
+
wordCount
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function detectCorrection(input) {
|
|
48
|
+
return CORRECTION_RE.test((input || '').trim());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clamp(value, min = 1, max = 5) {
|
|
52
|
+
return Math.min(max, Math.max(min, value));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function round1(value) {
|
|
56
|
+
return Math.round(value * 10) / 10;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function detectAutonomySignal(input) {
|
|
60
|
+
const text = (input || '').trim();
|
|
61
|
+
if (AUTONOMY_HIGH_RE.test(text)) return 5;
|
|
62
|
+
if (QUESTION_RE.test(text)) return 2;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function updateCalibration(calibration, input, correction = false) {
|
|
67
|
+
const { specificity: newSpec } = analyzeInput(input);
|
|
68
|
+
const autonomySignal = detectAutonomySignal(input);
|
|
69
|
+
|
|
70
|
+
const prev = {
|
|
71
|
+
specificity: calibration.specificity ?? 3,
|
|
72
|
+
corrections: calibration.corrections ?? 3,
|
|
73
|
+
autonomy: calibration.autonomy ?? 3,
|
|
74
|
+
interactions: calibration.interactions ?? 0
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const specificity = round1(clamp(prev.specificity * 0.7 + newSpec * 0.3));
|
|
78
|
+
|
|
79
|
+
const corrections = round1(clamp(
|
|
80
|
+
correction ? prev.corrections - 0.3 : prev.corrections + 0.1
|
|
81
|
+
));
|
|
82
|
+
|
|
83
|
+
let autonomy = prev.autonomy;
|
|
84
|
+
if (autonomySignal !== null) {
|
|
85
|
+
autonomy = round1(clamp(prev.autonomy * 0.7 + autonomySignal * 0.3));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
specificity,
|
|
90
|
+
corrections,
|
|
91
|
+
autonomy,
|
|
92
|
+
interactions: prev.interactions + 1,
|
|
93
|
+
lastUpdated: new Date().toISOString()
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getAdaptation(calibration) {
|
|
98
|
+
const s = calibration.specificity ?? 3;
|
|
99
|
+
const c = calibration.corrections ?? 3;
|
|
100
|
+
const a = calibration.autonomy ?? 3;
|
|
101
|
+
|
|
102
|
+
const clarifyBeforeActing = s < 2.5 || c > 3;
|
|
103
|
+
const explainReasoning = a < 3;
|
|
104
|
+
const suggestNextSteps = a < 4;
|
|
105
|
+
const askForApproval = c < 2.5;
|
|
106
|
+
const autoExecute = a > 4 && c > 3.5;
|
|
107
|
+
|
|
108
|
+
let responseStyle;
|
|
109
|
+
const combined = (s + c + a) / 3;
|
|
110
|
+
if (combined >= 4) {
|
|
111
|
+
responseStyle = 'terse';
|
|
112
|
+
} else if (combined >= 2.5) {
|
|
113
|
+
responseStyle = 'normal';
|
|
114
|
+
} else {
|
|
115
|
+
responseStyle = 'detailed';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let userLevel;
|
|
119
|
+
if (s >= 4) {
|
|
120
|
+
userLevel = 'advanced';
|
|
121
|
+
} else if (s >= 2.5) {
|
|
122
|
+
userLevel = 'intermediate';
|
|
123
|
+
} else {
|
|
124
|
+
userLevel = 'beginner';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
clarifyBeforeActing,
|
|
129
|
+
explainReasoning,
|
|
130
|
+
suggestNextSteps,
|
|
131
|
+
askForApproval,
|
|
132
|
+
autoExecute,
|
|
133
|
+
responseStyle,
|
|
134
|
+
userLevel
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function formatCalibration(calibration) {
|
|
139
|
+
const { userLevel, responseStyle } = getAdaptation(calibration);
|
|
140
|
+
const s = calibration.specificity ?? 3;
|
|
141
|
+
const c = calibration.corrections ?? 3;
|
|
142
|
+
const a = calibration.autonomy ?? 3;
|
|
143
|
+
|
|
144
|
+
const autonomyLabel = a >= 4 ? 'high autonomy' : a >= 2.5 ? 'normal autonomy' : 'low autonomy';
|
|
145
|
+
const trustLabel = c >= 4 ? 'high trust' : c >= 2.5 ? 'good trust' : 'low trust';
|
|
146
|
+
|
|
147
|
+
return `User: ${userLevel} · ${autonomyLabel} · ${trustLabel}\n (specificity: ${s}, corrections: ${c}, autonomy: ${a})`;
|
|
148
|
+
}
|
package/src/ledger.mjs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Task ledger: append-only accountability store for HEAD's promises. Every tracked task has a full snapshot per state change.
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
const LEDGER_PATH = '.dual-brain/ledger.jsonl';
|
|
7
|
+
|
|
8
|
+
function ledgerPath(cwd) {
|
|
9
|
+
return join(cwd || process.cwd(), LEDGER_PATH);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readAllEntries(cwd) {
|
|
13
|
+
const p = ledgerPath(cwd);
|
|
14
|
+
if (!existsSync(p)) return [];
|
|
15
|
+
const raw = readFileSync(p, 'utf8').trim();
|
|
16
|
+
if (!raw) return [];
|
|
17
|
+
return raw.split('\n').map(line => JSON.parse(line));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function appendEntry(entry, cwd) {
|
|
21
|
+
const p = ledgerPath(cwd);
|
|
22
|
+
mkdirSync(join(cwd || process.cwd(), '.dual-brain'), { recursive: true });
|
|
23
|
+
writeFileSync(p, JSON.stringify(entry) + '\n', { flag: 'a' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getCurrentTasks(cwd) {
|
|
27
|
+
const entries = readAllEntries(cwd);
|
|
28
|
+
const map = new Map();
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
map.set(entry.id, entry);
|
|
31
|
+
}
|
|
32
|
+
return Array.from(map.values());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createTask(task, cwd) {
|
|
36
|
+
const entry = {
|
|
37
|
+
id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
38
|
+
created: new Date().toISOString(),
|
|
39
|
+
updated: new Date().toISOString(),
|
|
40
|
+
intent: task.intent || '',
|
|
41
|
+
status: 'open',
|
|
42
|
+
owner: task.owner || 'head',
|
|
43
|
+
priority: task.priority || 'medium',
|
|
44
|
+
blockers: task.blockers || [],
|
|
45
|
+
proof: task.proof || null,
|
|
46
|
+
subtasks: task.subtasks || [],
|
|
47
|
+
parentTask: task.parentTask || null,
|
|
48
|
+
files: task.files || [],
|
|
49
|
+
result: task.result || null,
|
|
50
|
+
cost: task.cost || null,
|
|
51
|
+
};
|
|
52
|
+
appendEntry(entry, cwd);
|
|
53
|
+
return entry;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function updateTask(taskId, updates, cwd) {
|
|
57
|
+
const current = getTask(taskId, cwd);
|
|
58
|
+
if (!current) throw new Error(`Task not found: ${taskId}`);
|
|
59
|
+
|
|
60
|
+
if (updates.status === 'done') {
|
|
61
|
+
const proof = updates.proof ?? current.proof;
|
|
62
|
+
const result = updates.result ?? current.result;
|
|
63
|
+
if (!proof) throw new Error(`Cannot mark task done without proof: ${taskId}`);
|
|
64
|
+
if (!result) throw new Error(`Cannot mark task done without result: ${taskId}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const updated = {
|
|
68
|
+
...current,
|
|
69
|
+
...updates,
|
|
70
|
+
id: taskId,
|
|
71
|
+
updated: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
appendEntry(updated, cwd);
|
|
74
|
+
return updated;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function failTask(taskId, reason, cwd) {
|
|
78
|
+
const current = getTask(taskId, cwd);
|
|
79
|
+
if (!current) throw new Error(`Task not found: ${taskId}`);
|
|
80
|
+
const updated = {
|
|
81
|
+
...current,
|
|
82
|
+
status: 'failed',
|
|
83
|
+
result: reason || 'failed',
|
|
84
|
+
updated: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
appendEntry(updated, cwd);
|
|
87
|
+
return updated;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function blockTask(taskId, blocker, cwd) {
|
|
91
|
+
const current = getTask(taskId, cwd);
|
|
92
|
+
if (!current) throw new Error(`Task not found: ${taskId}`);
|
|
93
|
+
const updated = {
|
|
94
|
+
...current,
|
|
95
|
+
status: 'blocked',
|
|
96
|
+
blockers: [...(current.blockers || []), blocker],
|
|
97
|
+
updated: new Date().toISOString(),
|
|
98
|
+
};
|
|
99
|
+
appendEntry(updated, cwd);
|
|
100
|
+
return updated;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getTask(taskId, cwd) {
|
|
104
|
+
const entries = readAllEntries(cwd);
|
|
105
|
+
let latest = null;
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (entry.id === taskId) latest = entry;
|
|
108
|
+
}
|
|
109
|
+
return latest;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getOpenTasks(cwd) {
|
|
113
|
+
return getCurrentTasks(cwd).filter(t =>
|
|
114
|
+
t.status === 'open' || t.status === 'in_progress' || t.status === 'blocked'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getTaskHistory(taskId, cwd) {
|
|
119
|
+
return readAllEntries(cwd).filter(e => e.id === taskId);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getTaskSummary(cwd) {
|
|
123
|
+
const tasks = getCurrentTasks(cwd);
|
|
124
|
+
const summary = { open: 0, inProgress: 0, blocked: 0, done: 0, failed: 0, total: tasks.length };
|
|
125
|
+
for (const t of tasks) {
|
|
126
|
+
if (t.status === 'open') summary.open++;
|
|
127
|
+
else if (t.status === 'in_progress') summary.inProgress++;
|
|
128
|
+
else if (t.status === 'blocked') summary.blocked++;
|
|
129
|
+
else if (t.status === 'done') summary.done++;
|
|
130
|
+
else if (t.status === 'failed') summary.failed++;
|
|
131
|
+
}
|
|
132
|
+
return summary;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function formatTaskList(tasks) {
|
|
136
|
+
const all = tasks;
|
|
137
|
+
const open = all.filter(t => t.status === 'open' || t.status === 'in_progress').length;
|
|
138
|
+
const blocked = all.filter(t => t.status === 'blocked').length;
|
|
139
|
+
const done = all.filter(t => t.status === 'done').length;
|
|
140
|
+
|
|
141
|
+
const lines = [`TASKS (${open} open, ${blocked} blocked, ${done} done)`];
|
|
142
|
+
|
|
143
|
+
for (const t of all) {
|
|
144
|
+
const pri = t.priority === 'critical' ? 'crit' : t.priority === 'high' ? 'high' : t.priority === 'low' ? 'low' : 'med';
|
|
145
|
+
const label = t.intent.length > 48 ? t.intent.slice(0, 45) + '...' : t.intent;
|
|
146
|
+
|
|
147
|
+
if (t.status === 'done') {
|
|
148
|
+
lines.push(` ✓ [${pri}] ${label} (done)`);
|
|
149
|
+
} else if (t.status === 'failed') {
|
|
150
|
+
lines.push(` ✗ [${pri}] ${label} (failed)`);
|
|
151
|
+
} else if (t.status === 'blocked') {
|
|
152
|
+
const blockerNote = t.blockers && t.blockers.length ? `: ${t.blockers[t.blockers.length - 1]}` : '';
|
|
153
|
+
lines.push(` ◌ [${pri}] ${label} (blocked${blockerNote})`);
|
|
154
|
+
} else {
|
|
155
|
+
lines.push(` ● [${pri}] ${label} (${t.status})`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function reconcile(cwd) {
|
|
163
|
+
const tasks = getCurrentTasks(cwd);
|
|
164
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
165
|
+
return tasks.filter(t =>
|
|
166
|
+
(t.status === 'open' || t.status === 'in_progress') &&
|
|
167
|
+
new Date(t.updated).getTime() < cutoff
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function decompose(taskId, subtasks, cwd) {
|
|
172
|
+
const parent = getTask(taskId, cwd);
|
|
173
|
+
if (!parent) throw new Error(`Task not found: ${taskId}`);
|
|
174
|
+
|
|
175
|
+
const created = subtasks.map(sub =>
|
|
176
|
+
createTask(
|
|
177
|
+
{
|
|
178
|
+
...sub,
|
|
179
|
+
parentTask: taskId,
|
|
180
|
+
owner: sub.owner || parent.owner,
|
|
181
|
+
priority: sub.priority || parent.priority,
|
|
182
|
+
},
|
|
183
|
+
cwd
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const subtaskIds = created.map(s => s.id);
|
|
188
|
+
const updatedParent = {
|
|
189
|
+
...parent,
|
|
190
|
+
subtasks: [...(parent.subtasks || []), ...subtaskIds],
|
|
191
|
+
updated: new Date().toISOString(),
|
|
192
|
+
};
|
|
193
|
+
appendEntry(updatedParent, cwd);
|
|
194
|
+
|
|
195
|
+
return { parent: updatedParent, subtasks: created };
|
|
196
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// living-docs.mjs — Living document system for .dual-brain/.
|
|
2
|
+
// Manages project.json, vision.md, roadmap.md, state.md, actions.jsonl, decisions.jsonl, checkpoints.jsonl.
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const DIR = '.dual-brain';
|
|
9
|
+
|
|
10
|
+
function docsDir(cwd = process.cwd()) {
|
|
11
|
+
return join(cwd, DIR);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureDir(cwd) {
|
|
15
|
+
mkdirSync(docsDir(cwd), { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function filePath(name, cwd) {
|
|
19
|
+
return join(docsDir(cwd), name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readFileSafe(name, cwd, fallback = '') {
|
|
23
|
+
try {
|
|
24
|
+
return readFileSync(filePath(name, cwd), 'utf8');
|
|
25
|
+
} catch {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJsonSafe(name, cwd, fallback = {}) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(filePath(name, cwd), 'utf8'));
|
|
33
|
+
} catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readPackageJson(cwd) {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function gitExec(cmd, cwd) {
|
|
47
|
+
try {
|
|
48
|
+
return execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function initLivingDocs(cwd = process.cwd()) {
|
|
55
|
+
const dir = docsDir(cwd);
|
|
56
|
+
const existed = existsSync(dir);
|
|
57
|
+
ensureDir(cwd);
|
|
58
|
+
|
|
59
|
+
const pkg = readPackageJson(cwd);
|
|
60
|
+
|
|
61
|
+
if (!existsSync(filePath('project.json', cwd))) {
|
|
62
|
+
const project = {
|
|
63
|
+
name: pkg.name ?? '',
|
|
64
|
+
version: pkg.version ?? '0.0.1',
|
|
65
|
+
created: new Date().toISOString(),
|
|
66
|
+
workStyle: 'balanced',
|
|
67
|
+
userCalibration: { specificity: 3, corrections: 3, autonomy: 3 },
|
|
68
|
+
team: [],
|
|
69
|
+
providers: {},
|
|
70
|
+
};
|
|
71
|
+
writeFileSync(filePath('project.json', cwd), JSON.stringify(project, null, 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!existsSync(filePath('vision.md', cwd))) {
|
|
75
|
+
writeFileSync(filePath('vision.md', cwd), '# Vision\n\n_Not yet defined. Type your vision and HEAD will maintain this document._\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!existsSync(filePath('roadmap.md', cwd))) {
|
|
79
|
+
writeFileSync(filePath('roadmap.md', cwd), '# Roadmap\n\n_No roadmap yet. As you work, HEAD will build this from your actions._\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!existsSync(filePath('state.md', cwd))) {
|
|
83
|
+
writeFileSync(filePath('state.md', cwd), '# Current State\n\n_Fresh project. No history yet._\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const log of ['actions.jsonl', 'decisions.jsonl', 'checkpoints.jsonl']) {
|
|
87
|
+
if (!existsSync(filePath(log, cwd))) {
|
|
88
|
+
writeFileSync(filePath(log, cwd), '');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { created: !existed, path: dir };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function appendAction(action, cwd = process.cwd()) {
|
|
96
|
+
ensureDir(cwd);
|
|
97
|
+
const entry = {
|
|
98
|
+
id: `act_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
99
|
+
timestamp: new Date().toISOString(),
|
|
100
|
+
type: 'task',
|
|
101
|
+
intent: '',
|
|
102
|
+
status: 'started',
|
|
103
|
+
owner: 'head',
|
|
104
|
+
files: [],
|
|
105
|
+
proof: null,
|
|
106
|
+
cost: null,
|
|
107
|
+
result: null,
|
|
108
|
+
...action,
|
|
109
|
+
};
|
|
110
|
+
appendFileSync(filePath('actions.jsonl', cwd), JSON.stringify(entry) + '\n');
|
|
111
|
+
return entry;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function appendDecision(decision, cwd = process.cwd()) {
|
|
115
|
+
ensureDir(cwd);
|
|
116
|
+
const entry = {
|
|
117
|
+
id: `dec_${Date.now()}`,
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
question: '',
|
|
120
|
+
decision: '',
|
|
121
|
+
reasoning: '',
|
|
122
|
+
participants: [],
|
|
123
|
+
supersedes: null,
|
|
124
|
+
...decision,
|
|
125
|
+
};
|
|
126
|
+
appendFileSync(filePath('decisions.jsonl', cwd), JSON.stringify(entry) + '\n');
|
|
127
|
+
return entry;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createCheckpoint(summary, cwd = process.cwd()) {
|
|
131
|
+
ensureDir(cwd);
|
|
132
|
+
const gitRef = gitExec('git rev-parse HEAD', cwd) ?? 'unknown';
|
|
133
|
+
const branch = gitExec('git rev-parse --abbrev-ref HEAD', cwd) ?? 'unknown';
|
|
134
|
+
const stateSnapshot = readFileSafe('state.md', cwd, '');
|
|
135
|
+
const entry = {
|
|
136
|
+
id: `cp_${Date.now()}`,
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
gitRef,
|
|
139
|
+
branch,
|
|
140
|
+
summary,
|
|
141
|
+
stateSnapshot,
|
|
142
|
+
};
|
|
143
|
+
appendFileSync(filePath('checkpoints.jsonl', cwd), JSON.stringify(entry) + '\n');
|
|
144
|
+
return entry;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function updateState(newContent, cwd = process.cwd()) {
|
|
148
|
+
ensureDir(cwd);
|
|
149
|
+
writeFileSync(filePath('state.md', cwd), newContent);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function updateRoadmap(newContent, cwd = process.cwd()) {
|
|
153
|
+
ensureDir(cwd);
|
|
154
|
+
writeFileSync(filePath('roadmap.md', cwd), newContent);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function updateVision(newContent, cwd = process.cwd()) {
|
|
158
|
+
ensureDir(cwd);
|
|
159
|
+
const prev = readFileSafe('vision.md', cwd, '');
|
|
160
|
+
writeFileSync(filePath('vision.md', cwd), newContent);
|
|
161
|
+
if (prev !== newContent) {
|
|
162
|
+
appendDecision({
|
|
163
|
+
question: 'What is the project vision?',
|
|
164
|
+
decision: newContent.slice(0, 200),
|
|
165
|
+
reasoning: 'Vision document updated.',
|
|
166
|
+
participants: ['head'],
|
|
167
|
+
supersedes: null,
|
|
168
|
+
}, cwd);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getProjectState(cwd = process.cwd()) {
|
|
173
|
+
const project = readJsonSafe('project.json', cwd, {});
|
|
174
|
+
const state = readFileSafe('state.md', cwd, '');
|
|
175
|
+
const actions = getRecentActions(cwd, 20);
|
|
176
|
+
const decisions = readLastLines('decisions.jsonl', cwd, 5);
|
|
177
|
+
const checkpoints = readLastLines('checkpoints.jsonl', cwd, 1);
|
|
178
|
+
return {
|
|
179
|
+
project,
|
|
180
|
+
state,
|
|
181
|
+
recentActions: actions,
|
|
182
|
+
recentDecisions: decisions,
|
|
183
|
+
lastCheckpoint: checkpoints[0] ?? null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function readLastLines(name, cwd, n) {
|
|
188
|
+
const raw = readFileSafe(name, cwd, '');
|
|
189
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
190
|
+
return lines.slice(-n).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getRecentActions(cwd = process.cwd(), limit = 20) {
|
|
194
|
+
return readLastLines('actions.jsonl', cwd, limit);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getOpenTasks(cwd = process.cwd()) {
|
|
198
|
+
const raw = readFileSafe('actions.jsonl', cwd, '');
|
|
199
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
200
|
+
const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
201
|
+
return entries.filter(e => e.status === 'started' || e.status === 'blocked');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function updateProject(updates, cwd = process.cwd()) {
|
|
205
|
+
ensureDir(cwd);
|
|
206
|
+
const current = readJsonSafe('project.json', cwd, {});
|
|
207
|
+
const merged = { ...current, ...updates };
|
|
208
|
+
writeFileSync(filePath('project.json', cwd), JSON.stringify(merged, null, 2));
|
|
209
|
+
return merged;
|
|
210
|
+
}
|