linksee-memory 0.8.0 → 0.11.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/README.md +923 -782
- package/dist/bin/guard-hook.d.ts +2 -0
- package/dist/bin/guard-hook.js +96 -0
- package/dist/bin/import-sessions.js +40 -7
- package/dist/bin/map-import.d.ts +2 -0
- package/dist/bin/map-import.js +493 -0
- package/dist/bin/setup.js +94 -5
- package/dist/db/migrate.js +36 -0
- package/dist/db/schema.sql +96 -2
- package/dist/lib/drift-detection.js +38 -2
- package/dist/lib/guard.d.ts +81 -0
- package/dist/lib/guard.js +321 -0
- package/dist/lib/lexical-match.d.ts +6 -0
- package/dist/lib/lexical-match.js +87 -0
- package/dist/lib/map-import.d.ts +55 -0
- package/dist/lib/map-import.js +150 -0
- package/dist/lib/map-reconcile.d.ts +29 -0
- package/dist/lib/map-reconcile.js +245 -0
- package/dist/lib/map-view.d.ts +103 -0
- package/dist/lib/map-view.js +201 -0
- package/dist/lib/session-extractor.d.ts +1 -0
- package/dist/lib/session-extractor.js +62 -11
- package/dist/mcp/server.js +455 -5
- package/dist/skill/SKILL.md +103 -0
- package/package.json +12 -5
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// linksee-memory-map — load the Current Truth Map (map.yaml) into the runtime index
|
|
3
|
+
// and answer topology questions. map.yaml is the desired-state source of truth (anchor
|
|
4
|
+
// #58); this reconciles it into map_nodes/map_edges (full rebuild per project).
|
|
5
|
+
//
|
|
6
|
+
// Usage (CLI-first triage, not a list). The natural working flow:
|
|
7
|
+
// linksee-memory-map where [<file>] # WHERE am I? (no arg = infer from recent edits) → node + AFFECTS
|
|
8
|
+
// linksee-memory-map affects <node> # what to change together if you touch this node
|
|
9
|
+
// linksee-memory-map explain <node> # WHY this status + ✓/✗ evidence + FIX (the diagnosis)
|
|
10
|
+
// linksee-memory-map status # whole-project health % + what needs attention
|
|
11
|
+
// ── also ──
|
|
12
|
+
// linksee-memory-map next # the prioritized next fix candidate(s)
|
|
13
|
+
// linksee-memory-map reconcile # check the hand-written Map against real code/files
|
|
14
|
+
// linksee-memory-map inspect --json # machine-readable dump (CI / tooling)
|
|
15
|
+
// linksee-memory-map blueprint # stage×node board (colors reflect the live verdict)
|
|
16
|
+
// linksee-memory-map [--file map.yaml] [--root <repo>]
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { openDb, runMigrations } from '../db/migrate.js';
|
|
19
|
+
import { parseMapFile, importMap } from '../lib/map-import.js';
|
|
20
|
+
import { blastRadius, getSuspects, getBlueprint, getNode, getProjectMeta, whereAmI } from '../lib/map-view.js';
|
|
21
|
+
import { reconcile } from '../lib/map-reconcile.js';
|
|
22
|
+
function flagValue(argv, name, dflt) {
|
|
23
|
+
const i = argv.indexOf(`--${name}`);
|
|
24
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
|
|
25
|
+
}
|
|
26
|
+
const argv = process.argv.slice(2);
|
|
27
|
+
// positional args, skipping flags and their values (so `where --file x` doesn't read "--file" as the target)
|
|
28
|
+
const VALUE_FLAGS = new Set(['--file', '--root', '--limit', '--lang']);
|
|
29
|
+
const positionals = [];
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
if (argv[i].startsWith('--')) {
|
|
32
|
+
if (VALUE_FLAGS.has(argv[i]))
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
positionals.push(argv[i]);
|
|
37
|
+
}
|
|
38
|
+
const sub = positionals[0] ?? 'import';
|
|
39
|
+
const arg1 = positionals[1]; // the node-id / file / topic for explain|blast|affects|where
|
|
40
|
+
const mapPath = flagValue(argv, 'file', join(process.cwd(), 'map.yaml'));
|
|
41
|
+
const db = openDb();
|
|
42
|
+
runMigrations(db);
|
|
43
|
+
const map = parseMapFile(mapPath);
|
|
44
|
+
// Always (re)import first so reads reflect the file — map.yaml is authoritative.
|
|
45
|
+
const res = importMap(db, map);
|
|
46
|
+
const COLOR_DOT = { green: '🟢', red: '🔴', gray: '⚪', amber: '🟡', blue: '🔵' };
|
|
47
|
+
const repoRoot = flagValue(argv, 'root', process.cwd());
|
|
48
|
+
// ── i18n: English by default, Japanese with --lang ja (map.yaml content stays the user's language) ──
|
|
49
|
+
const lang = flagValue(argv, 'lang', 'en') === 'ja' ? 'ja' : 'en';
|
|
50
|
+
const EN = {
|
|
51
|
+
status: { active: 'healthy (active)', commitment: 'commitment (has a due date)', suspect: 'needs review (possible drift)', planned: 'planned', paused: 'paused', future_thesis: 'future thesis', experiment: 'experiment' },
|
|
52
|
+
affHard: 'must fix together', affSoft: 'should align', affWatch: 'fyi (may ripple)',
|
|
53
|
+
declared: 'declared:', reality: 'reality: ', verdict: 'verdict: ',
|
|
54
|
+
rConv: 'implemented / matches', rDiv: 'drifted', rAbs: 'not realized',
|
|
55
|
+
cRefuted: 'declared suspect, refuted by reality (→ convergence)', cDrift: 'declared vs reality disagree (drift)', cVerified: 'declared and reality agree (verified)', cAbsence: 'declared but not realized (absence)',
|
|
56
|
+
realityExt: 'awaiting external check (no auto-verify)', realityNone: 'no auto-check configured',
|
|
57
|
+
whyFallback: 'declared color (no auto-check configured)',
|
|
58
|
+
evExt: 'external state — verified by a human, not the scanner', evNone: 'no auto-check configured (declared)',
|
|
59
|
+
affectsHdr: 'AFFECTS (change these together — by strength)', affectsSub: 'changes ripple to:',
|
|
60
|
+
affectsCmd: (id, n) => `change ${id} → fix these together (${n}) — by strength:`,
|
|
61
|
+
whereAutoHdr: 'Inferred "you are here" from your recent edits:', whereAutoNone: 'Could not locate you on the Map from recent edits.',
|
|
62
|
+
whereOwns: (t) => `"${t}" belongs to this Map node:`, whereTopic: 'No file match → closest nodes by topic:',
|
|
63
|
+
whereNone: (t) => `No file or topic on the Map matched "${t}".`,
|
|
64
|
+
needs: 'Needs attention:', actionable: 'Actionable now (local, fixable):', external: 'External checks (verify outside the repo):',
|
|
65
|
+
verified: 'Verified by reality:', noAction: 'No action:', refutedTag: 'refuted suspect → convergence', verifiedTag: 'verified',
|
|
66
|
+
graveyard: 'Deferrals with no expiry/condition (graveyard risk):', graveyardHint: 'add review_by: <date> or revival_condition: <text> in map.yaml',
|
|
67
|
+
overdue: (d) => `Overdue deferrals (promised by a date, now past ${d}):`,
|
|
68
|
+
nLocal: 'Next local fix:', nExternal: 'Next external checks:', nNothing: 'Nothing needs attention — the Map matches reality.',
|
|
69
|
+
evidence: 'evidence:', next: 'next:', empty: '(empty — absence)', impl: 'implementation', notFound: 'node not found',
|
|
70
|
+
};
|
|
71
|
+
const JA = {
|
|
72
|
+
status: { active: '健全(稼働中)', commitment: '約束(締切あり)', suspect: '要確認(ズレの疑い)', planned: '計画', paused: '保留', future_thesis: '将来構想', experiment: '実験中' },
|
|
73
|
+
affHard: '必ず一緒に直す', affSoft: 'できれば揃える', affWatch: '参考(連鎖の可能性)',
|
|
74
|
+
declared: '宣言状態:', reality: '現実判定:', verdict: '結論: ',
|
|
75
|
+
rConv: '実装あり / 一致', rDiv: 'ズレあり', rAbs: '未実現',
|
|
76
|
+
cRefuted: '要確認は現実により反証 (refuted suspect → convergence)', cDrift: '宣言と現実がズレている (drift)', cVerified: '宣言と現実が一致 (verified)', cAbsence: '宣言が現実に未実現 (absence)',
|
|
77
|
+
realityExt: '外部確認待ち(自動確認なし)', realityNone: '自動チェック未設定',
|
|
78
|
+
whyFallback: '宣言ベース(現実の自動チェックは未設定)',
|
|
79
|
+
evExt: '外部状態のため自動確認なし(人が確認)', evNone: '自動チェック未設定(宣言ベース)',
|
|
80
|
+
affectsHdr: 'AFFECTS(変えたら一緒に直す先・強度順)', affectsSub: '変えたら一緒に直す先:',
|
|
81
|
+
affectsCmd: (id, n) => `${id} を変えたら一緒に直す先 (${n}) — 強度順:`,
|
|
82
|
+
whereAutoHdr: '直近の編集から推定した「今いる場所」:', whereAutoNone: '直近の編集からMap上の位置を特定できませんでした。',
|
|
83
|
+
whereOwns: (t) => `"${t}" は Map 上のこのノードに属します:`, whereTopic: 'ファイル一致なし → トピックとして近いノード:',
|
|
84
|
+
whereNone: (t) => `Map上で "${t}" に該当するファイル/トピックは見つかりませんでした。`,
|
|
85
|
+
needs: '要対応:', actionable: '今すぐ直せる(ローカル):', external: '外部チェック(リポジトリ外で確認):',
|
|
86
|
+
verified: '現実で検証済み:', noAction: '対応不要:', refutedTag: '要確認→現実で反証 (convergence)', verifiedTag: '検証済み',
|
|
87
|
+
graveyard: '期限/条件のない保留(墓場リスク):', graveyardHint: 'map.yaml に review_by: <日付> か revival_condition: <条件> を追加',
|
|
88
|
+
overdue: (d) => `期限切れの保留(${d} を過ぎている):`,
|
|
89
|
+
nLocal: '次に直す(ローカル):', nExternal: '次の外部チェック:', nNothing: '対応事項なし — Mapと現実が一致。',
|
|
90
|
+
evidence: 'evidence:', next: 'next:', empty: '(empty — absence)', impl: 'implementation', notFound: 'node not found',
|
|
91
|
+
};
|
|
92
|
+
const T = lang === 'ja' ? JA : EN;
|
|
93
|
+
// Triage/diagnosis commands reflect REALITY, so reconcile first (scans repoRoot).
|
|
94
|
+
const DIAG_SUBS = new Set(['status', 'explain', 'next', 'inspect', 'where', 'blueprint']);
|
|
95
|
+
if (DIAG_SUBS.has(sub))
|
|
96
|
+
reconcile(db, map.project, repoRoot);
|
|
97
|
+
function diagOf(id) {
|
|
98
|
+
const node = getNode(db, map.project, id);
|
|
99
|
+
if (!node)
|
|
100
|
+
return null;
|
|
101
|
+
let ev = {};
|
|
102
|
+
try {
|
|
103
|
+
ev = JSON.parse(node.verdict_evidence || '{}');
|
|
104
|
+
}
|
|
105
|
+
catch { /* none */ }
|
|
106
|
+
return { node, ev };
|
|
107
|
+
}
|
|
108
|
+
// A node "needs attention" if reality flags it (divergence/absence) or it's a hand-declared
|
|
109
|
+
// suspect reality hasn't cleared. A suspect REFUTED by reality (→convergence) does NOT.
|
|
110
|
+
function needsAttention(n) {
|
|
111
|
+
if (n.live_verdict === 'divergence' || n.live_verdict === 'absence')
|
|
112
|
+
return true;
|
|
113
|
+
if (n.status === 'suspect' && n.live_verdict !== 'convergence')
|
|
114
|
+
return true;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const allNodes = () => db.prepare('SELECT * FROM map_nodes WHERE project = ?').all(map.project);
|
|
118
|
+
const parseJson = (s, dflt) => { try {
|
|
119
|
+
return JSON.parse(s || '');
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return dflt;
|
|
123
|
+
} };
|
|
124
|
+
const truncate = (s, n) => (s && s.length > n ? s.slice(0, n) + '…' : s ?? '');
|
|
125
|
+
const shortPath = (p) => (p ? p.split(/[\\/]/).slice(-2).join('/') : '');
|
|
126
|
+
// External = verified outside the repo (network/human); not fixable in code right now.
|
|
127
|
+
const isExternal = (n) => parseJson(n.reality, {}).kind === 'external';
|
|
128
|
+
const TODAY = new Date().toISOString().slice(0, 10);
|
|
129
|
+
// Accounted-for = parked with a reason (paused / external / has a revival plan).
|
|
130
|
+
const isAccountedFor = (n) => n.status === 'paused' || isExternal(n) || !!n.revival_condition || !!n.review_by;
|
|
131
|
+
const isOverdue = (n) => !!n.review_by && n.review_by < TODAY; // promised by a date, now past it
|
|
132
|
+
const noExpiry = (n) => isAccountedFor(n) && !n.review_by && !n.revival_condition; // graveyard risk
|
|
133
|
+
const facetsOf = (n) => parseJson(n.facets, []);
|
|
134
|
+
// A short tag when reality's verdict differs from / confirms the declared status.
|
|
135
|
+
const verdictMark = (n) => {
|
|
136
|
+
if (!n.live_verdict)
|
|
137
|
+
return '';
|
|
138
|
+
if (n.status === 'suspect' && n.live_verdict === 'convergence')
|
|
139
|
+
return ' ✓ declared suspect, reality verified';
|
|
140
|
+
if (n.live_verdict === 'divergence')
|
|
141
|
+
return ' ✗ reality drift';
|
|
142
|
+
if (n.live_verdict === 'absence')
|
|
143
|
+
return ' ⚪ declared but absent';
|
|
144
|
+
if (n.live_verdict === 'convergence')
|
|
145
|
+
return ' ✓ reality verified';
|
|
146
|
+
return '';
|
|
147
|
+
};
|
|
148
|
+
// AFFECTS grouped by edge strength so a big blast radius isn't flat noise.
|
|
149
|
+
function printAffects(id, indent = ' ') {
|
|
150
|
+
const blast = blastRadius(db, map.project, id);
|
|
151
|
+
if (!blast.length) {
|
|
152
|
+
console.log(`${indent}(none)`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const groups = [['hard', T.affHard], ['soft', T.affSoft], ['watch', T.affWatch]];
|
|
156
|
+
for (const [s, label] of groups) {
|
|
157
|
+
const hits = blast.filter((b) => b.strength === s);
|
|
158
|
+
if (!hits.length)
|
|
159
|
+
continue;
|
|
160
|
+
console.log(`${indent}${label} (${s}):`);
|
|
161
|
+
for (const b of hits)
|
|
162
|
+
console.log(`${indent} ${b.id} (${b.relation})`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const declaredJP = (s) => T.status[s] ?? s;
|
|
166
|
+
function printSuspectsWithBlast() {
|
|
167
|
+
const suspects = getSuspects(db, map.project);
|
|
168
|
+
if (suspects.length === 0) {
|
|
169
|
+
console.log('\nNo suspect nodes. (Map declares all surfaces converged.)');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log(`\n🔴 ${suspects.length} suspect node(s) — out-of-band drift candidates:`);
|
|
173
|
+
for (const s of suspects) {
|
|
174
|
+
console.log(`\n ${s.id} — ${s.statement}`);
|
|
175
|
+
if (s.note)
|
|
176
|
+
console.log(` note: ${s.note}`);
|
|
177
|
+
const blast = blastRadius(db, map.project, s.id);
|
|
178
|
+
if (blast.length === 0) {
|
|
179
|
+
console.log(' blast radius: (none — isolated node)');
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
console.log(` blast radius (${blast.length}): touching this implicates —`);
|
|
183
|
+
for (const b of blast)
|
|
184
|
+
console.log(` • ${b.id} [${b.status}] via ${b.relation}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (sub === 'import') {
|
|
188
|
+
console.log(`[map] imported ${map.project} from ${mapPath}`);
|
|
189
|
+
console.log(` ${res.nodes} nodes · ${res.edges} edges · ${res.linked_anchors} anchor link(s)`);
|
|
190
|
+
if (res.warnings.length) {
|
|
191
|
+
console.log(` ⚠ ${res.warnings.length} warning(s):`);
|
|
192
|
+
for (const w of res.warnings)
|
|
193
|
+
console.log(` - ${w}`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log(' ✓ no topology warnings');
|
|
197
|
+
}
|
|
198
|
+
printSuspectsWithBlast();
|
|
199
|
+
}
|
|
200
|
+
else if (sub === 'suspects') {
|
|
201
|
+
printSuspectsWithBlast();
|
|
202
|
+
}
|
|
203
|
+
else if (sub === 'reconcile') {
|
|
204
|
+
// Check declared intent against actual reality (local code/files). Scans process.cwd().
|
|
205
|
+
const flagRoot = flagValue(argv, 'root', process.cwd());
|
|
206
|
+
const res = reconcile(db, map.project, flagRoot);
|
|
207
|
+
console.log(`[reconcile] ${map.project} — ${res.checked} checked · ${res.external} external (human-confirmed)`);
|
|
208
|
+
if (res.refuted.length) {
|
|
209
|
+
console.log(`\n✓ ${res.refuted.length} suspect(s) REFUTED by reality (declared suspect → reality says convergence):`);
|
|
210
|
+
for (const v of res.refuted)
|
|
211
|
+
console.log(` 🟢 ${v.id} — ${v.reason}\n ${v.evidence.file ?? ''}${v.evidence.line_no ? ':' + v.evidence.line_no : ''}`);
|
|
212
|
+
}
|
|
213
|
+
if (res.confirmed.length) {
|
|
214
|
+
console.log(`\n🔴 ${res.confirmed.length} suspect(s) CONFIRMED by reality:`);
|
|
215
|
+
for (const v of res.confirmed)
|
|
216
|
+
console.log(` 🔴 ${v.id} — ${v.reason}`);
|
|
217
|
+
}
|
|
218
|
+
const newDiv = res.verdicts.filter((v) => v.flipped && v.verdict === 'divergence');
|
|
219
|
+
if (newDiv.length) {
|
|
220
|
+
console.log(`\n⚠ ${newDiv.length} node(s) reality flags as divergence (declared OK, reality disagrees):`);
|
|
221
|
+
for (const v of newDiv)
|
|
222
|
+
console.log(` 🔴 ${v.id} — ${v.reason}`);
|
|
223
|
+
}
|
|
224
|
+
console.log(`\nVerdicts: ${res.verdicts.filter(v => v.verdict === 'convergence').length} convergence · ${res.verdicts.filter(v => v.verdict === 'divergence').length} divergence · ${res.verdicts.filter(v => v.verdict === 'absence').length} absence · ${res.external} external`);
|
|
225
|
+
}
|
|
226
|
+
else if (sub === 'blast') {
|
|
227
|
+
const id = arg1;
|
|
228
|
+
if (!id) {
|
|
229
|
+
console.error('usage: linksee-memory-map blast <node-id>');
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
const blast = blastRadius(db, map.project, id);
|
|
233
|
+
console.log(`blast radius for ${id} (${blast.length}):`);
|
|
234
|
+
for (const b of blast)
|
|
235
|
+
console.log(` • ${b.id} [${b.status}] via ${b.relation}\n ${b.statement}`);
|
|
236
|
+
}
|
|
237
|
+
else if (sub === 'blueprint') {
|
|
238
|
+
// reconcile ran (DIAG_SUBS) → colors reflect the live verdict, not just the declared status.
|
|
239
|
+
const bp = getBlueprint(db, map.project, map.stages);
|
|
240
|
+
const all = allNodes();
|
|
241
|
+
const verified = all.filter((n) => n.live_verdict === 'convergence').length;
|
|
242
|
+
const attention = all.filter(needsAttention).length;
|
|
243
|
+
console.log(`Blueprint — ${bp.project}`);
|
|
244
|
+
console.log(`declared: ${Object.entries(bp.counts).map(([k, v]) => `${k}=${v}`).join(' ')}`);
|
|
245
|
+
console.log(`reality: verified=${verified} needs-attention=${attention}`);
|
|
246
|
+
const line = (n) => ` ${COLOR_DOT[n.color]} ${n.id} — ${n.statement}${verdictMark(n)}`;
|
|
247
|
+
for (const cell of bp.stages) {
|
|
248
|
+
console.log(`\n▌${cell.label} (${cell.stage})`);
|
|
249
|
+
if (cell.nodes.length === 0) {
|
|
250
|
+
console.log(` ${T.empty}`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
for (const n of cell.nodes)
|
|
254
|
+
console.log(line(n));
|
|
255
|
+
}
|
|
256
|
+
console.log(`\n▌${T.impl}`);
|
|
257
|
+
for (const n of bp.implementation)
|
|
258
|
+
console.log(line(n));
|
|
259
|
+
}
|
|
260
|
+
else if (sub === 'status') {
|
|
261
|
+
// Triage, not a list. Local-actionable first (fixable now, has evidence), then external.
|
|
262
|
+
const nodes = allNodes();
|
|
263
|
+
const attention = nodes.filter(needsAttention);
|
|
264
|
+
const local = attention.filter((n) => !isExternal(n));
|
|
265
|
+
const external = attention.filter(isExternal);
|
|
266
|
+
const verified = nodes.filter((n) => n.live_verdict === 'convergence');
|
|
267
|
+
const health = nodes.length ? Math.round(100 * (nodes.length - attention.length) / nodes.length) : 100;
|
|
268
|
+
console.log(`Product: ${map.project}`);
|
|
269
|
+
console.log(`Health: ${health}%`);
|
|
270
|
+
console.log(`\n${T.needs} ${attention.length}`);
|
|
271
|
+
if (local.length) {
|
|
272
|
+
console.log(`\n ${T.actionable}`);
|
|
273
|
+
local.forEach((n, i) => {
|
|
274
|
+
const ev = parseJson(n.verdict_evidence, {});
|
|
275
|
+
const failed = (ev.checks ?? []).find((c) => !c.ok);
|
|
276
|
+
const evidence = failed ? `${shortPath(failed.file)}${failed.line ? ':' + failed.line : ''} (${failed.detail})` : '—';
|
|
277
|
+
console.log(` ${i + 1}. ${n.id} ${n.live_verdict ?? n.status}`);
|
|
278
|
+
console.log(` ${truncate(ev.why || n.note || n.statement, 88)}`);
|
|
279
|
+
console.log(` ${T.evidence} ${evidence}`);
|
|
280
|
+
console.log(` ${T.next} linksee-memory-map explain ${n.id}`);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (external.length) {
|
|
284
|
+
console.log(`\n ${T.external}`);
|
|
285
|
+
external.forEach((n, i) => {
|
|
286
|
+
const ev = parseJson(n.verdict_evidence, {});
|
|
287
|
+
console.log(` ${local.length + i + 1}. ${n.id} ${n.status}`);
|
|
288
|
+
console.log(` ${truncate(ev.why || n.note || n.statement, 88)}`);
|
|
289
|
+
if (ev.expected)
|
|
290
|
+
console.log(` expected: ${ev.expected}`);
|
|
291
|
+
if (ev.check)
|
|
292
|
+
console.log(` check: ${ev.check}`);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// accounted-for can't become a drift graveyard: overdue deferrals re-escalate, missing expiries are flagged.
|
|
296
|
+
const overdue = nodes.filter(isOverdue);
|
|
297
|
+
if (overdue.length) {
|
|
298
|
+
console.log(`\n⏰ ${T.overdue(TODAY)}`);
|
|
299
|
+
for (const n of overdue)
|
|
300
|
+
console.log(` ${n.id} review_by ${n.review_by}${n.revival_condition ? ' — ' + n.revival_condition : ''}`);
|
|
301
|
+
}
|
|
302
|
+
const graveyard = nodes.filter(noExpiry);
|
|
303
|
+
if (graveyard.length) {
|
|
304
|
+
console.log(`\n⚠ ${T.graveyard} ${graveyard.map((n) => n.id).join(', ')}`);
|
|
305
|
+
console.log(` → ${T.graveyardHint}`);
|
|
306
|
+
}
|
|
307
|
+
console.log(`\n${T.verified} ${verified.length}`);
|
|
308
|
+
for (const n of verified)
|
|
309
|
+
console.log(` ${n.id.padEnd(20)} ${n.status === 'suspect' ? T.refutedTag : T.verifiedTag}`);
|
|
310
|
+
console.log(`\n${T.noAction} ${nodes.length - attention.length - verified.length}`);
|
|
311
|
+
}
|
|
312
|
+
else if (sub === 'explain') {
|
|
313
|
+
const id = arg1;
|
|
314
|
+
if (!id) {
|
|
315
|
+
console.error('usage: linksee-memory-map explain <node>');
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
const d = diagOf(id);
|
|
319
|
+
if (!d) {
|
|
320
|
+
console.error(`node not found: ${id}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
const { node, ev } = d;
|
|
324
|
+
const meta = getProjectMeta(db, map.project);
|
|
325
|
+
const stageLabel = node.stage ? (meta?.stages.find((s) => s.id === node.stage)?.label ?? node.stage) : null;
|
|
326
|
+
const v = node.live_verdict;
|
|
327
|
+
const isExt = parseJson(node.reality, {}).kind === 'external';
|
|
328
|
+
const facets = facetsOf(node);
|
|
329
|
+
console.log(`${node.id}${stageLabel ? ` [${stageLabel}]` : ''}${facets.length ? ` {${facets.join(', ')}}` : ''}`);
|
|
330
|
+
console.log(node.statement);
|
|
331
|
+
// declared state and reality verdict are DIFFERENT things — show them separately.
|
|
332
|
+
console.log('\nSTATUS');
|
|
333
|
+
console.log(` ${T.declared} ${declaredJP(node.status)}`);
|
|
334
|
+
if (v) {
|
|
335
|
+
const realityV = v === 'convergence' ? T.rConv : v === 'divergence' ? T.rDiv : T.rAbs;
|
|
336
|
+
const concl = node.status === 'suspect' && v === 'convergence' ? T.cRefuted
|
|
337
|
+
: v === 'divergence' ? T.cDrift
|
|
338
|
+
: v === 'convergence' ? T.cVerified
|
|
339
|
+
: T.cAbsence;
|
|
340
|
+
console.log(` ${T.reality} ${realityV}`);
|
|
341
|
+
console.log(` ${T.verdict} ${concl}`);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
console.log(` ${T.reality} ${isExt ? T.realityExt : T.realityNone}`);
|
|
345
|
+
}
|
|
346
|
+
console.log(`\nWHY\n ${ev.why || node.note || T.whyFallback}`);
|
|
347
|
+
console.log('\nEVIDENCE');
|
|
348
|
+
if (ev.checks && ev.checks.length) {
|
|
349
|
+
for (const c of ev.checks)
|
|
350
|
+
console.log(` ${c.ok ? '✓' : '✗'} ${c.claim}\n ${shortPath(c.file)}${c.line ? ':' + c.line : ''} — ${c.detail}`);
|
|
351
|
+
}
|
|
352
|
+
else if (isExt) {
|
|
353
|
+
console.log(` ${T.evExt}`);
|
|
354
|
+
if (ev.expected)
|
|
355
|
+
console.log(` expected: ${ev.expected}`);
|
|
356
|
+
if (ev.check)
|
|
357
|
+
console.log(` check: ${ev.check}`);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
console.log(` ${T.evNone}`);
|
|
361
|
+
}
|
|
362
|
+
if (ev.fix && ev.fix.length) {
|
|
363
|
+
console.log('\nFIX');
|
|
364
|
+
ev.fix.forEach((f, i) => console.log(` ${i + 1}. ${f}`));
|
|
365
|
+
}
|
|
366
|
+
if (node.review_by || node.revival_condition) {
|
|
367
|
+
console.log('\nDEFERRED UNTIL');
|
|
368
|
+
if (node.review_by)
|
|
369
|
+
console.log(` review_by: ${node.review_by}${isOverdue(node) ? ' ⏰ OVERDUE' : ''}`);
|
|
370
|
+
if (node.revival_condition)
|
|
371
|
+
console.log(` condition: ${node.revival_condition}`);
|
|
372
|
+
}
|
|
373
|
+
console.log(`\n${T.affectsHdr}`);
|
|
374
|
+
printAffects(id);
|
|
375
|
+
console.log(`\nNEXT\n linksee-memory-map reconcile # re-check after a fix\n linksee-memory-map affects ${id}`);
|
|
376
|
+
}
|
|
377
|
+
else if (sub === 'affects') {
|
|
378
|
+
const id = arg1;
|
|
379
|
+
if (!id) {
|
|
380
|
+
console.error('usage: linksee-memory-map affects <node>');
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
const blast = blastRadius(db, map.project, id);
|
|
384
|
+
console.log(T.affectsCmd(id, blast.length));
|
|
385
|
+
printAffects(id);
|
|
386
|
+
}
|
|
387
|
+
else if (sub === 'where') {
|
|
388
|
+
// "I'm about to touch this file — where is it on the Map, and what does it touch?"
|
|
389
|
+
const target = arg1;
|
|
390
|
+
if (!target) {
|
|
391
|
+
// no arg → auto-locate from what you just edited this session
|
|
392
|
+
const res = whereAmI(db, { project: map.project });
|
|
393
|
+
if (!res.matched.length) {
|
|
394
|
+
console.log(T.whereAutoNone);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
console.log(T.whereAutoHdr);
|
|
398
|
+
for (const m of res.matched) {
|
|
399
|
+
console.log(`\n ${m.node.id}${m.stage_label ? ` [${m.stage_label}]` : ''} ${m.node.live_verdict ?? m.node.status} (${m.match_reason})`);
|
|
400
|
+
console.log(` ${T.affectsSub}`);
|
|
401
|
+
printAffects(m.node.id, ' ');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
db.close();
|
|
405
|
+
process.exit(0);
|
|
406
|
+
}
|
|
407
|
+
const norm = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
408
|
+
const t = norm(target);
|
|
409
|
+
const meta = getProjectMeta(db, map.project);
|
|
410
|
+
const stageLabel = (s) => (s ? meta?.stages.find((x) => x.id === s)?.label ?? s : null);
|
|
411
|
+
// Ownership = a node whose reality names this FILE explicitly (check.path / reality.path).
|
|
412
|
+
// `dir:` is a scan SCOPE (e.g. all of src), not ownership — so it does NOT match here.
|
|
413
|
+
const matched = allNodes().filter((n) => {
|
|
414
|
+
const r = parseJson(n.reality, {});
|
|
415
|
+
const paths = [];
|
|
416
|
+
if (r.path)
|
|
417
|
+
paths.push(norm(r.path));
|
|
418
|
+
for (const c of (r.checks ?? []))
|
|
419
|
+
if (c.path)
|
|
420
|
+
paths.push(norm(c.path));
|
|
421
|
+
return paths.some((p) => t === p);
|
|
422
|
+
});
|
|
423
|
+
if (matched.length) {
|
|
424
|
+
console.log(T.whereOwns(target));
|
|
425
|
+
for (const n of matched) {
|
|
426
|
+
console.log(`\n ${n.id}${n.stage ? ` [${stageLabel(n.stage)}]` : ''} ${n.live_verdict ?? n.status}`);
|
|
427
|
+
console.log(` ${n.statement}`);
|
|
428
|
+
console.log(` ${T.affectsSub}`);
|
|
429
|
+
printAffects(n.id, ' ');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// no file match → treat the arg as a topic (lexical locate, like where_am_i)
|
|
434
|
+
const res = whereAmI(db, { project: map.project, query: target });
|
|
435
|
+
if (!res.matched.length) {
|
|
436
|
+
console.log(T.whereNone(target));
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
console.log(T.whereTopic);
|
|
440
|
+
for (const m of res.matched)
|
|
441
|
+
console.log(` ${m.node.id}${m.stage_label ? ` [${m.stage_label}]` : ''} (${m.match_reason})`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else if (sub === 'next') {
|
|
446
|
+
// local-first: what you can fix in code now, then what to verify externally.
|
|
447
|
+
const attention = allNodes().filter(needsAttention);
|
|
448
|
+
const local = attention.filter((n) => !isExternal(n))
|
|
449
|
+
.sort((a, b) => blastRadius(db, map.project, b.id).length - blastRadius(db, map.project, a.id).length);
|
|
450
|
+
const external = attention.filter(isExternal);
|
|
451
|
+
if (!attention.length) {
|
|
452
|
+
console.log(`✓ ${T.nNothing}`);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
if (local.length) {
|
|
456
|
+
console.log(T.nLocal);
|
|
457
|
+
local.slice(0, 3).forEach((n, i) => {
|
|
458
|
+
const ev = parseJson(n.verdict_evidence, {});
|
|
459
|
+
console.log(` ${i + 1}. ${n.id} — ${truncate(ev.why || n.note || n.statement, 88)}`);
|
|
460
|
+
console.log(` → linksee-memory-map explain ${n.id}`);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (external.length) {
|
|
464
|
+
console.log(`${local.length ? '\n' : ''}${T.nExternal}`);
|
|
465
|
+
external.forEach((n) => {
|
|
466
|
+
const ev = parseJson(n.verdict_evidence, {});
|
|
467
|
+
console.log(` • ${n.id}${ev.check ? ` — ${ev.check}` : ''}`);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else if (sub === 'inspect') {
|
|
473
|
+
const nodes = allNodes();
|
|
474
|
+
const out = {
|
|
475
|
+
project: map.project,
|
|
476
|
+
health: nodes.length ? Math.round(100 * (nodes.length - nodes.filter(needsAttention).length) / nodes.length) : 100,
|
|
477
|
+
nodes: nodes.map((n) => {
|
|
478
|
+
const ev = parseJson(n.verdict_evidence, {});
|
|
479
|
+
return {
|
|
480
|
+
id: n.id, stage: n.stage, layer: n.layer, status: n.status, live_verdict: n.live_verdict,
|
|
481
|
+
why: ev.why ?? null, checks: ev.checks ?? [], fix: ev.fix ?? [],
|
|
482
|
+
affects: blastRadius(db, map.project, n.id).map((b) => b.id), needs_attention: needsAttention(n),
|
|
483
|
+
};
|
|
484
|
+
}),
|
|
485
|
+
};
|
|
486
|
+
console.log(JSON.stringify(out, null, 2));
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
console.error(`unknown subcommand: ${sub}`);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
db.close();
|
|
493
|
+
//# sourceMappingURL=map-import.js.map
|
package/dist/bin/setup.js
CHANGED
|
@@ -6,14 +6,16 @@
|
|
|
6
6
|
// npx linksee-memory-setup --yes (accept all defaults, no prompts)
|
|
7
7
|
// npx linksee-memory-setup --dry-run
|
|
8
8
|
//
|
|
9
|
-
// Does
|
|
9
|
+
// Does four things:
|
|
10
10
|
// 1. Registers the MCP server with Claude Code
|
|
11
11
|
// 2. Installs the SKILL.md (agent trigger phrases)
|
|
12
|
-
// 3. Configures the Stop hook (auto-capture sessions)
|
|
12
|
+
// 3. Configures the Stop hook (auto-capture sessions) — user-global
|
|
13
|
+
// 4. Offers to wire the re-injection guard into THIS project's .claude/settings.json
|
|
13
14
|
//
|
|
14
15
|
// After setup, every Claude Code session:
|
|
15
16
|
// - Auto-captures decisions, learnings, caveats to local memory
|
|
16
17
|
// - Agent auto-recalls past context at task start (via SKILL.md triggers)
|
|
18
|
+
// - (if guard enabled) accepted decisions are re-injected before edits + on session start
|
|
17
19
|
// - "Use Linksee" in any prompt forces a recall
|
|
18
20
|
//
|
|
19
21
|
// Why: Competing memory tools (claude-mem, etc.) are one-install-and-done.
|
|
@@ -24,6 +26,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from
|
|
|
24
26
|
import { join, dirname } from 'node:path';
|
|
25
27
|
import { homedir } from 'node:os';
|
|
26
28
|
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { createInterface } from 'node:readline';
|
|
27
30
|
const args = process.argv.slice(2);
|
|
28
31
|
const dryRun = args.includes('--dry-run');
|
|
29
32
|
const autoYes = args.includes('--yes') || args.includes('-y');
|
|
@@ -40,6 +43,7 @@ What it does:
|
|
|
40
43
|
1. Registers linksee-memory MCP server with Claude Code
|
|
41
44
|
2. Installs SKILL.md (teaches the agent when to recall/remember)
|
|
42
45
|
3. Configures Stop hook (auto-captures every session)
|
|
46
|
+
4. Offers to wire the re-injection guard into THIS project's .claude/settings.json
|
|
43
47
|
|
|
44
48
|
After setup, just chat with Claude Code normally.
|
|
45
49
|
Add "Use Linksee" to any prompt to trigger memory recall.`);
|
|
@@ -56,6 +60,14 @@ const SKILL_SRC = join(dirname(__filename), '..', 'skill', 'SKILL.md');
|
|
|
56
60
|
const SERVER_NAME = 'linksee';
|
|
57
61
|
const MCP_COMMAND = `claude mcp add -s user ${SERVER_NAME} -- npx -y linksee-memory`;
|
|
58
62
|
const HOOK_COMMAND = 'npx -y linksee-memory-sync';
|
|
63
|
+
// Re-injection guard — wired into the PROJECT (not user-global) settings, because it enforces THIS
|
|
64
|
+
// project's accepted decisions. Mirrors the dogfood wiring's ${CLAUDE_PROJECT_DIR}/dist/bin path, but
|
|
65
|
+
// points at the globally-installed `linksee-memory-guard` bin so it ships without a build step. Shell
|
|
66
|
+
// form (resolved at run time) survives npx-cache eviction; a baked dist path would not.
|
|
67
|
+
const GUARD_COMMAND = 'npx -y linksee-memory-guard';
|
|
68
|
+
const PROJECT_DIR = process.cwd();
|
|
69
|
+
const PROJECT_CLAUDE_DIR = join(PROJECT_DIR, '.claude');
|
|
70
|
+
const PROJECT_SETTINGS_PATH = join(PROJECT_CLAUDE_DIR, 'settings.json');
|
|
59
71
|
const CHECK = '\x1b[32m✓\x1b[0m';
|
|
60
72
|
const SKIP = '\x1b[33m○\x1b[0m';
|
|
61
73
|
const FAIL = '\x1b[31m✗\x1b[0m';
|
|
@@ -67,7 +79,7 @@ console.log(`${BOLD}Linksee Memory Setup${RESET}`);
|
|
|
67
79
|
console.log(`${DIM}Local-first cross-LLM memory · precision recall${RESET}`);
|
|
68
80
|
console.log('');
|
|
69
81
|
// ── Step 1: Register MCP server ──────────────────────────
|
|
70
|
-
console.log(`${BOLD}[1/
|
|
82
|
+
console.log(`${BOLD}[1/4]${RESET} Registering MCP server...`);
|
|
71
83
|
let mcpAlreadyRegistered = false;
|
|
72
84
|
try {
|
|
73
85
|
// Check if already registered by looking at settings.json or .claude.json
|
|
@@ -132,7 +144,7 @@ else {
|
|
|
132
144
|
}
|
|
133
145
|
console.log('');
|
|
134
146
|
// ── Step 2: Install SKILL.md ─────────────────────────────
|
|
135
|
-
console.log(`${BOLD}[2/
|
|
147
|
+
console.log(`${BOLD}[2/4]${RESET} Installing agent skill...`);
|
|
136
148
|
if (!existsSync(SKILL_SRC)) {
|
|
137
149
|
console.log(` ${FAIL} Bundled SKILL.md not found (packaging bug)`);
|
|
138
150
|
console.log(` ${DIM}Expected at: ${SKILL_SRC}${RESET}`);
|
|
@@ -165,7 +177,7 @@ else {
|
|
|
165
177
|
}
|
|
166
178
|
console.log('');
|
|
167
179
|
// ── Step 3: Configure Stop hook ──────────────────────────
|
|
168
|
-
console.log(`${BOLD}[3/
|
|
180
|
+
console.log(`${BOLD}[3/4]${RESET} Configuring auto-capture hook...`);
|
|
169
181
|
let settings = {};
|
|
170
182
|
if (existsSync(SETTINGS_PATH)) {
|
|
171
183
|
try {
|
|
@@ -200,6 +212,80 @@ else {
|
|
|
200
212
|
console.log(` ${CHECK} Stop hook added → ${SETTINGS_PATH}`);
|
|
201
213
|
}
|
|
202
214
|
console.log('');
|
|
215
|
+
const GUARD_EVENTS = ['SessionStart', 'PreToolUse'];
|
|
216
|
+
const GUARD_HOOKS = {
|
|
217
|
+
// matchers + timeouts mirror the dogfood .claude/settings.json
|
|
218
|
+
SessionStart: { matcher: 'startup|resume|compact', hooks: [{ type: 'command', command: GUARD_COMMAND, timeout: 15 }] },
|
|
219
|
+
PreToolUse: { matcher: 'Edit|Write|Bash', hooks: [{ type: 'command', command: GUARD_COMMAND, timeout: 8 }] },
|
|
220
|
+
};
|
|
221
|
+
// Idempotency probe: is OUR guard already wired for this event? (Match by bin name so a manually-added
|
|
222
|
+
// or previously-installed entry isn't duplicated, and other people's hooks are never touched.)
|
|
223
|
+
function guardWiredFor(s, ev) {
|
|
224
|
+
return (s.hooks?.[ev] ?? []).some((entry) => entry?.hooks?.some((h) => typeof h?.command === 'string' && h.command.includes('linksee-memory-guard')));
|
|
225
|
+
}
|
|
226
|
+
function askYesNo(question) {
|
|
227
|
+
return new Promise((resolve) => {
|
|
228
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
229
|
+
rl.question(`${question} [Y/n] `, (ans) => {
|
|
230
|
+
rl.close();
|
|
231
|
+
const a = ans.trim().toLowerCase();
|
|
232
|
+
resolve(a === '' || a === 'y' || a === 'yes'); // default = yes
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async function configureGuard() {
|
|
237
|
+
console.log(`${BOLD}[4/4]${RESET} Configuring re-injection guard (this project)...`);
|
|
238
|
+
let project = {};
|
|
239
|
+
if (existsSync(PROJECT_SETTINGS_PATH)) {
|
|
240
|
+
try {
|
|
241
|
+
// Strip a leading BOM (U+FEFF) — Windows editors (Notepad) emit UTF-8+BOM, which JSON.parse rejects.
|
|
242
|
+
const raw = readFileSync(PROJECT_SETTINGS_PATH, 'utf8');
|
|
243
|
+
project = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Never clobber a file we can't parse — the user may have hand-authored it.
|
|
247
|
+
console.log(` ${FAIL} Could not parse ${PROJECT_SETTINGS_PATH} — left untouched`);
|
|
248
|
+
console.log(` ${DIM}Add the guard block by hand (see README → Re-injection Guard).${RESET}`);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (GUARD_EVENTS.every((ev) => guardWiredFor(project, ev))) {
|
|
253
|
+
console.log(` ${SKIP} Guard already wired in ${PROJECT_SETTINGS_PATH}`);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (dryRun) {
|
|
257
|
+
console.log(` ${DIM}[dry-run] Would merge SessionStart + PreToolUse guard hooks into ${PROJECT_SETTINGS_PATH}${RESET}`);
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
// "Offer" — opt-in, because the guard can deny tool calls on a 'hard' contradiction.
|
|
261
|
+
if (!autoYes) {
|
|
262
|
+
if (!process.stdin.isTTY) {
|
|
263
|
+
console.log(` ${SKIP} Skipped (non-interactive shell). Re-run with --yes, or paste the README block.`);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
console.log(` ${DIM}Re-injects your accepted decisions before Edit/Write/Bash and on session start.`);
|
|
267
|
+
console.log(` Fail-open — only an action that contradicts a 'hard' anchor is ever blocked.${RESET}`);
|
|
268
|
+
const ok = await askYesNo(` Wire it into ${PROJECT_SETTINGS_PATH}?`);
|
|
269
|
+
if (!ok) {
|
|
270
|
+
console.log(` ${SKIP} Skipped. Enable later via the README → Re-injection Guard.`);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Merge, don't replace: append only the events we don't already own; leave foreign hooks intact.
|
|
275
|
+
const hooks = project.hooks ?? (project.hooks = {});
|
|
276
|
+
for (const ev of GUARD_EVENTS) {
|
|
277
|
+
if (!Array.isArray(hooks[ev]))
|
|
278
|
+
hooks[ev] = [];
|
|
279
|
+
if (!guardWiredFor(project, ev))
|
|
280
|
+
hooks[ev].push(GUARD_HOOKS[ev]);
|
|
281
|
+
}
|
|
282
|
+
mkdirSync(PROJECT_CLAUDE_DIR, { recursive: true });
|
|
283
|
+
writeFileSync(PROJECT_SETTINGS_PATH, JSON.stringify(project, null, 2), 'utf8');
|
|
284
|
+
console.log(` ${CHECK} Guard wired → ${PROJECT_SETTINGS_PATH}`);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
const guardConfigured = await configureGuard();
|
|
288
|
+
console.log('');
|
|
203
289
|
// ── Summary ──────────────────────────────────────────────
|
|
204
290
|
console.log(`${BOLD}Setup complete!${RESET}`);
|
|
205
291
|
console.log('');
|
|
@@ -208,6 +294,9 @@ console.log(` ${DIM}• Every session is auto-captured (decisions, caveats, lea
|
|
|
208
294
|
console.log(` ${DIM}• Agent auto-recalls past context when starting a task${RESET}`);
|
|
209
295
|
console.log(` ${DIM}• Memory is local-first (nothing leaves your machine)${RESET}`);
|
|
210
296
|
console.log(` ${DIM}• Works across Claude Code, Cursor, ChatGPT (cross-LLM)${RESET}`);
|
|
297
|
+
if (guardConfigured) {
|
|
298
|
+
console.log(` ${DIM}• Re-injection guard re-surfaces this project's accepted decisions before edits${RESET}`);
|
|
299
|
+
}
|
|
211
300
|
console.log('');
|
|
212
301
|
console.log('Test by asking:');
|
|
213
302
|
console.log(` ${BOLD}"How did we solve this before?"${RESET}`);
|