barry-cache 0.2.2 → 0.3.2

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.
Files changed (3) hide show
  1. package/README.md +57 -9
  2. package/dist/cli.js +490 -93
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Barry Cache
2
2
 
3
3
  <p align="center">
4
- <img src="assets/barry-cache.png" alt="Barry Cache" width="420">
4
+ <img src="https://raw.githubusercontent.com/AlexanderIstomin/barry-cache/main/assets/barry-cache.png" alt="Barry Cache" width="420">
5
5
  </p>
6
6
 
7
7
  Barry Cache remembers your repo.
@@ -72,7 +72,7 @@ Checks whether the repo context is structurally valid.
72
72
  barry-cache validate
73
73
  ```
74
74
 
75
- It verifies required context files exist and validates every fact row in `docs/context/features/*/FACTS.jsonl`.
75
+ It verifies required context files, ADR frontmatter, and every fact row in `docs/context/features/*/FACTS.jsonl`.
76
76
 
77
77
  Use this after editing context files, importing memory, or before committing Barry Cache changes.
78
78
 
@@ -100,13 +100,13 @@ Use this when deciding which context pack an agent should load before doing work
100
100
 
101
101
  ### `barry-cache search`
102
102
 
103
- Searches feature packs and facts for a query.
103
+ Searches feature packs, facts, and ADRs for a query.
104
104
 
105
105
  ```bash
106
106
  barry-cache search --query "transport clock"
107
107
  ```
108
108
 
109
- It returns matching feature packs and fact records with route, score, source, and text.
109
+ It returns matching feature packs, fact records, and ADR records with route, score, source, and text.
110
110
 
111
111
  Use this when you know a term, file, component, or concept and want to find the relevant memory.
112
112
 
@@ -118,7 +118,7 @@ Loads one feature context pack.
118
118
  barry-cache load --route renderer-runtime
119
119
  ```
120
120
 
121
- It returns the feature README, facts, and source file list for `docs/context/features/<route>/`.
121
+ It returns the feature README, facts, linked ADRs, and source file list for `docs/context/features/<route>/`.
122
122
 
123
123
  Use this after `route` or `search` selects a specific feature.
124
124
 
@@ -150,12 +150,41 @@ It appends a JSONL handoff record to `.context-state/handoffs/handoffs.jsonl`.
150
150
 
151
151
  Use this before ending a meaningful work session so the next agent can recover what happened.
152
152
 
153
+ `finalize` writes operational memory only. It does not update canonical project context in `docs/context/`. If a task introduced durable implementation behavior, add or update source-backed facts in `docs/context/features/*/FACTS.jsonl` and run `barry-cache validate`.
154
+
153
155
  Statuses:
154
156
  - `success`: the task was completed.
155
157
  - `partial`: some useful progress was made.
156
158
  - `blocked`: the task cannot proceed without external input.
157
159
  - `failed`: the attempted approach did not work.
158
160
 
161
+ ### `barry-cache adr`
162
+
163
+ Creates and lists architecture decision records.
164
+
165
+ ```bash
166
+ barry-cache adr new --title "Use repo-native context"
167
+ barry-cache adr new --title "Keep generated indexes disposable" --tags context,cache
168
+ barry-cache adr list
169
+ ```
170
+
171
+ ADRs live in `docs/context/adrs/` as Markdown files with frontmatter. Use them for durable architectural decisions, then reference the ADR file from decision facts with `src`.
172
+
173
+ ```json
174
+ {
175
+ "id": "CTX001",
176
+ "subject": "Barry",
177
+ "predicate": "stores canonical context in",
178
+ "object": "docs/context/",
179
+ "src": ["docs/context/adrs/ADR-0001-use-repo-native-context.md"],
180
+ "status": "active",
181
+ "kind": "decision",
182
+ "updated_at": "2026-05-19"
183
+ }
184
+ ```
185
+
186
+ Barry can then search the ADR, route tasks through feature facts linked to it, load it with the relevant feature pack, and show it in review data.
187
+
159
188
  ### `barry-cache review`
160
189
 
161
190
  Opens a local browser tool for inspecting memory.
@@ -228,6 +257,12 @@ npx barry-cache init
228
257
 
229
258
  After this, Barry Cache writes short instructions into agent-facing files such as `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.cursor/rules/barry-cache.mdc`, and `.github/copilot-instructions.md`.
230
259
 
260
+ When a package manager is detected, those instructions use the repo package script instead of assuming `barry-cache` is on `PATH`. For a Bun repo, agents are told to run commands like:
261
+
262
+ ```bash
263
+ bun run barry -- resume --task "fix playback drift in the editor"
264
+ ```
265
+
231
266
  For a Codex-only repo, use:
232
267
 
233
268
  ```bash
@@ -238,7 +273,7 @@ Those generated files tell coding agents how to use Barry before they edit the r
238
273
 
239
274
  ### Ask An Agent To Work On A Task
240
275
 
241
- Usually, you do not need to run `barry-cache resume` yourself.
276
+ Usually, you do not need to run Barry yourself.
242
277
 
243
278
  Ask your coding agent normally:
244
279
 
@@ -249,13 +284,15 @@ Fix playback drift in the editor.
249
284
  Because `barry-cache init` added instructions to the repo, the agent should run this before non-trivial work:
250
285
 
251
286
  ```bash
252
- barry-cache resume --task "fix playback drift in the editor"
287
+ bun run barry -- resume --task "fix playback drift in the editor"
253
288
  ```
254
289
 
290
+ The generated instruction file uses the package manager Barry detected for that repo, for example `bun`, `npm`, `pnpm`, or `yarn`.
291
+
255
292
  The agent then uses the returned routes to load focused context:
256
293
 
257
294
  ```bash
258
- barry-cache load --route editor-media-runtime
295
+ bun run barry -- load --route editor-media-runtime
259
296
  ```
260
297
 
261
298
  This keeps the agent from reading every context file in the repo.
@@ -264,7 +301,7 @@ If an agent ignores the repo instructions, prompt it explicitly:
264
301
 
265
302
  ```text
266
303
  Before editing, follow Barry Cache protocol:
267
- 1. Run barry-cache resume --task "<my task>".
304
+ 1. Run the repo's Barry package script, for example bun run barry -- resume --task "<my task>".
268
305
  2. Load the returned route context.
269
306
  3. Do the work.
270
307
  4. Run validation.
@@ -287,6 +324,16 @@ Use the browser review tool for a broader overview:
287
324
  barry-cache review
288
325
  ```
289
326
 
327
+ ### Record An Architectural Decision
328
+
329
+ Create an ADR when a decision explains why future agents should preserve or intentionally change architecture:
330
+
331
+ ```bash
332
+ barry-cache adr new --title "Use repo-native context" --tags context,agents
333
+ ```
334
+
335
+ Then add or update a `kind: "decision"` fact in the relevant feature pack and point `src` to the ADR file. That keeps the human-readable reasoning and the machine-routable fact connected.
336
+
290
337
  ### Save Agent Sessions
291
338
 
292
339
  At the end of a meaningful session, ask the agent:
@@ -300,6 +347,7 @@ Rules:
300
347
  3. Put uncertain notes, blockers, and next steps in operational memory, not canonical facts.
301
348
  4. Update IDMAP.md or KG.adj only when new source IDs or relationships are needed.
302
349
  5. Run barry-cache validate before finishing.
350
+ 6. Do not claim Barry canonical memory is updated unless docs/context/ changed.
303
351
  ```
304
352
 
305
353
  The minimum useful save is:
package/dist/cli.js CHANGED
@@ -2,9 +2,9 @@
2
2
  import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
- // src/core/context.ts
6
- import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
7
- import { basename, join as join3 } from "node:path";
5
+ // src/core/adr.ts
6
+ import { readdir as readdir2 } from "node:fs/promises";
7
+ import { basename, join as join2 } from "node:path";
8
8
 
9
9
  // src/core/fs.ts
10
10
  import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
@@ -50,8 +50,223 @@ function repoPath(repo, ...parts) {
50
50
  return join(repo, ...parts);
51
51
  }
52
52
 
53
+ // src/core/adr.ts
54
+ var adrStatuses = ["active", "superseded", "deprecated"];
55
+ async function createAdr(options) {
56
+ const title = options.title.trim();
57
+ if (title.length === 0)
58
+ throw new Error("ADR title is required");
59
+ const status = options.status ?? "active";
60
+ if (!isAdrStatus(status))
61
+ throw new Error(`invalid ADR status: ${status}`);
62
+ const existing = await listAdrFiles(options.repo);
63
+ const id = nextAdrId(existing);
64
+ const path = `docs/context/adrs/${id}-${slug(title)}.md`;
65
+ const date = options.date ?? new Date().toISOString().slice(0, 10);
66
+ const content = formatAdr({
67
+ id,
68
+ title,
69
+ status,
70
+ date,
71
+ supersedes: options.supersedes ?? [],
72
+ tags: options.tags ?? []
73
+ });
74
+ await writeText(repoPath(options.repo, path), content);
75
+ return {
76
+ id,
77
+ title,
78
+ status,
79
+ date,
80
+ supersedes: options.supersedes ?? [],
81
+ tags: options.tags ?? [],
82
+ path,
83
+ content
84
+ };
85
+ }
86
+ async function listAdrs({ repo }) {
87
+ const catalog = await readAdrCatalog(repo);
88
+ return catalog.adrs;
89
+ }
90
+ async function readAdrCatalog(repo) {
91
+ const files = await listAdrFiles(repo);
92
+ const adrs = [];
93
+ const errors = [];
94
+ for (const path of files) {
95
+ const absolute = repoPath(repo, path);
96
+ const content = await readText(absolute);
97
+ const parsed = parseAdr(repo, path, content);
98
+ if (parsed.record)
99
+ adrs.push(parsed.record);
100
+ errors.push(...parsed.errors);
101
+ }
102
+ adrs.sort((a, b) => a.id.localeCompare(b.id));
103
+ errors.sort((a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0));
104
+ return { adrs, errors };
105
+ }
106
+ function linkedAdrsForSources(sources, adrs) {
107
+ const linked = new Map;
108
+ for (const source of sources) {
109
+ for (const adr of adrs) {
110
+ if (adrMatchesSource(adr, source))
111
+ linked.set(adr.id, adr);
112
+ }
113
+ }
114
+ return [...linked.values()].sort((a, b) => a.id.localeCompare(b.id));
115
+ }
116
+ function adrMatchesSource(adr, source) {
117
+ const normalized = source.split("#")[0] ?? source;
118
+ const filename = basename(adr.path);
119
+ return normalized === adr.id || normalized === adr.path || normalized === filename || normalized.endsWith(`/${filename}`);
120
+ }
121
+ function looksLikeAdrSource(source) {
122
+ const normalized = source.split("#")[0] ?? source;
123
+ return /^ADR-\d{4}\b/.test(normalized) || /(?:^|\/)ADR-\d{4}-.+\.md$/.test(normalized) || normalized.startsWith("docs/context/adrs/");
124
+ }
125
+ function adrToText(adr) {
126
+ return [
127
+ adr.id,
128
+ adr.title,
129
+ adr.status,
130
+ adr.date,
131
+ ...adr.tags,
132
+ ...adr.supersedes,
133
+ adr.content
134
+ ].join(" ");
135
+ }
136
+ async function listAdrFiles(repo) {
137
+ const dir = repoPath(repo, "docs/context/adrs");
138
+ if (!await exists(dir))
139
+ return [];
140
+ const entries = await readdir2(dir, { withFileTypes: true });
141
+ return entries.filter((entry) => entry.isFile() && /^ADR-\d{4}-.+\.md$/.test(entry.name)).map((entry) => rel(repo, join2(dir, entry.name))).sort();
142
+ }
143
+ function parseAdr(repo, path, content) {
144
+ const errors = [];
145
+ const frontmatter = parseFrontmatter(path, content);
146
+ if (!frontmatter.data)
147
+ return { errors: [frontmatter.error] };
148
+ const id = stringField(frontmatter.data, "id");
149
+ const title = stringField(frontmatter.data, "title");
150
+ const status = stringField(frontmatter.data, "status");
151
+ const date = stringField(frontmatter.data, "date");
152
+ const supersedes = arrayField(frontmatter.data, "supersedes");
153
+ const tags = arrayField(frontmatter.data, "tags");
154
+ if (!id)
155
+ errors.push({ file: path, message: "missing ADR field: id" });
156
+ if (!title)
157
+ errors.push({ file: path, message: "missing ADR field: title" });
158
+ if (!status)
159
+ errors.push({ file: path, message: "missing ADR field: status" });
160
+ if (!date)
161
+ errors.push({ file: path, message: "missing ADR field: date" });
162
+ if (id && !/^ADR-\d{4}$/.test(id))
163
+ errors.push({ file: path, message: `invalid ADR id: ${id}` });
164
+ if (id && !basename(path).startsWith(`${id}-`))
165
+ errors.push({ file: path, message: "ADR id does not match filename" });
166
+ if (status && !isAdrStatus(status))
167
+ errors.push({ file: path, message: `invalid ADR status: ${status}` });
168
+ if (date && !/^\d{4}-\d{2}-\d{2}$/.test(date))
169
+ errors.push({ file: path, message: `invalid ADR date: ${date}` });
170
+ if (errors.length > 0 || !id || !title || !status || !date || !isAdrStatus(status))
171
+ return { errors };
172
+ return {
173
+ record: {
174
+ id,
175
+ title,
176
+ status,
177
+ date,
178
+ supersedes,
179
+ tags,
180
+ path: rel(repo, repoPath(repo, path)),
181
+ content
182
+ },
183
+ errors
184
+ };
185
+ }
186
+ function parseFrontmatter(path, content) {
187
+ const lines = content.split(/\r?\n/);
188
+ if (lines[0] !== "---")
189
+ return { error: { file: path, line: 1, message: "ADR frontmatter is missing" } };
190
+ const end = lines.findIndex((line, index) => index > 0 && line === "---");
191
+ if (end < 0)
192
+ return { error: { file: path, line: 1, message: "ADR frontmatter is not closed" } };
193
+ const data = new Map;
194
+ for (let index = 1;index < end; index++) {
195
+ const line = lines[index]?.trim() ?? "";
196
+ if (line.length === 0)
197
+ continue;
198
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
199
+ if (!match || !match[1]) {
200
+ return { error: { file: path, line: index + 1, message: "invalid ADR frontmatter line" } };
201
+ }
202
+ data.set(match[1], match[2] ?? "");
203
+ }
204
+ return { data, error: { file: path, message: "" } };
205
+ }
206
+ function stringField(data, key) {
207
+ const value = data.get(key)?.trim();
208
+ if (!value)
209
+ return;
210
+ return unquote(value);
211
+ }
212
+ function arrayField(data, key) {
213
+ const value = data.get(key)?.trim();
214
+ if (!value || value === "[]")
215
+ return [];
216
+ const bracketed = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
217
+ return bracketed.split(",").map((item) => unquote(item.trim())).filter(Boolean);
218
+ }
219
+ function nextAdrId(files) {
220
+ const next = files.reduce((max, path) => {
221
+ const match = basename(path).match(/^ADR-(\d{4})-/);
222
+ return match?.[1] ? Math.max(max, Number(match[1])) : max;
223
+ }, 0) + 1;
224
+ return `ADR-${String(next).padStart(4, "0")}`;
225
+ }
226
+ function formatAdr(options) {
227
+ return `---
228
+ id: ${options.id}
229
+ title: ${options.title}
230
+ status: ${options.status}
231
+ date: ${options.date}
232
+ supersedes: ${formatArray(options.supersedes)}
233
+ tags: ${formatArray(options.tags)}
234
+ ---
235
+
236
+ # ${options.id}: ${options.title}
237
+
238
+ ## Context
239
+
240
+ Describe the forces, constraints, and history that make this decision necessary.
241
+
242
+ ## Decision
243
+
244
+ Describe the chosen direction.
245
+
246
+ ## Consequences
247
+
248
+ Describe the tradeoffs, follow-up work, and facts that should reference this ADR.
249
+ `;
250
+ }
251
+ function formatArray(values) {
252
+ return values.length === 0 ? "[]" : `[${values.join(", ")}]`;
253
+ }
254
+ function isAdrStatus(value) {
255
+ return adrStatuses.includes(value);
256
+ }
257
+ function slug(input) {
258
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "decision";
259
+ }
260
+ function unquote(input) {
261
+ return input.replace(/^['"]|['"]$/g, "");
262
+ }
263
+
264
+ // src/core/context.ts
265
+ import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
266
+ import { basename as basename2, join as join4 } from "node:path";
267
+
53
268
  // src/core/validate.ts
54
- import { join as join2 } from "node:path";
269
+ import { join as join3 } from "node:path";
55
270
  var requiredFiles = [
56
271
  "docs/context/INDEX.md",
57
272
  "docs/context/LOG.md",
@@ -61,6 +276,8 @@ var requiredFiles = [
61
276
  async function validateProject({ repo }) {
62
277
  const errors = [];
63
278
  const warnings = [];
279
+ const adrCatalog = await readAdrCatalog(repo);
280
+ errors.push(...adrCatalog.errors);
64
281
  for (const file of requiredFiles) {
65
282
  if (!await exists(repoPath(repo, file))) {
66
283
  errors.push({ file, message: "required context file is missing" });
@@ -68,8 +285,8 @@ async function validateProject({ repo }) {
68
285
  }
69
286
  const featureRoot = repoPath(repo, "docs/context/features");
70
287
  const features = await listDirs(featureRoot);
71
- for (const slug of features) {
72
- const factsPath = join2(featureRoot, slug, "FACTS.jsonl");
288
+ for (const slug2 of features) {
289
+ const factsPath = join3(featureRoot, slug2, "FACTS.jsonl");
73
290
  if (!await exists(factsPath)) {
74
291
  warnings.push({ file: rel(repo, factsPath), message: "feature pack has no FACTS.jsonl" });
75
292
  continue;
@@ -84,6 +301,14 @@ async function validateProject({ repo }) {
84
301
  const message = validateFact(value);
85
302
  if (message)
86
303
  errors.push({ file: rel(repo, factsPath), line, message });
304
+ if (!message) {
305
+ const fact = value;
306
+ for (const source of fact.src) {
307
+ if (looksLikeAdrSource(source) && !adrCatalog.adrs.some((adr) => adrMatchesSource(adr, source))) {
308
+ warnings.push({ file: rel(repo, factsPath), line, message: `fact references missing ADR source: ${source}` });
309
+ }
310
+ }
311
+ }
87
312
  } catch {
88
313
  errors.push({ file: rel(repo, factsPath), line, message: "invalid JSON" });
89
314
  }
@@ -116,12 +341,14 @@ function validateFact(value) {
116
341
  // src/core/context.ts
117
342
  async function routeTask({ repo, task }) {
118
343
  const features = await readFeaturePacks(repo);
344
+ const adrs = await listAdrs({ repo });
119
345
  const taskTokens = tokens(task);
120
- const routes = features.map((feature) => scoreFeature(feature, taskTokens)).filter((route) => route.score > 0).sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
346
+ const routes = features.map((feature) => scoreFeature(feature, taskTokens, adrs)).filter((route) => route.score > 0).sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
121
347
  return { task, routes };
122
348
  }
123
349
  async function searchContext({ repo, query }) {
124
350
  const features = await readFeaturePacks(repo);
351
+ const adrs = await listAdrs({ repo });
125
352
  const queryTokens = tokens(query);
126
353
  const results = [];
127
354
  for (const feature of features) {
@@ -147,28 +374,43 @@ async function searchContext({ repo, query }) {
147
374
  route: feature.slug,
148
375
  score,
149
376
  text: factText,
150
- source: `${rel(repo, join3(feature.dir, "FACTS.jsonl"))}#${fact.id}`
377
+ source: `${rel(repo, join4(feature.dir, "FACTS.jsonl"))}#${fact.id}`
151
378
  });
152
379
  }
153
380
  }
154
381
  }
382
+ for (const adr of adrs) {
383
+ const score = scoreText(adrToText(adr), queryTokens);
384
+ if (score > 0) {
385
+ results.push({
386
+ type: "adr",
387
+ id: adr.id,
388
+ route: "adrs",
389
+ score,
390
+ text: adr.title,
391
+ source: adr.path
392
+ });
393
+ }
394
+ }
155
395
  results.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
156
396
  return { query, results };
157
397
  }
158
398
  async function loadContext({ repo, route }) {
159
399
  const features = await readFeaturePacks(repo);
400
+ const adrs = await listAdrs({ repo });
160
401
  const feature = features.find((item) => item.slug === route) ?? null;
161
402
  if (!feature)
162
- return { feature: null, facts: [], sources: [] };
403
+ return { feature: null, facts: [], sources: [], adrs: [] };
163
404
  return {
164
405
  feature,
165
406
  facts: feature.facts,
166
407
  sources: [
167
- rel(repo, join3(feature.dir, "README.md")),
168
- rel(repo, join3(feature.dir, "IDMAP.md")),
169
- rel(repo, join3(feature.dir, "KG.adj")),
170
- rel(repo, join3(feature.dir, "FACTS.jsonl"))
171
- ]
408
+ rel(repo, join4(feature.dir, "README.md")),
409
+ rel(repo, join4(feature.dir, "IDMAP.md")),
410
+ rel(repo, join4(feature.dir, "KG.adj")),
411
+ rel(repo, join4(feature.dir, "FACTS.jsonl"))
412
+ ],
413
+ adrs: linkedAdrsForSources(feature.facts.flatMap((fact) => fact.src), adrs)
172
414
  };
173
415
  }
174
416
  async function resumeProject({ repo, task }) {
@@ -181,7 +423,7 @@ async function resumeProject({ repo, task }) {
181
423
  execution_contract: {
182
424
  task_goal: task,
183
425
  first_action: firstAction,
184
- edit_scope: selected.map((slug) => `docs/context/features/${slug}/**`),
426
+ edit_scope: selected.map((slug2) => `docs/context/features/${slug2}/**`),
185
427
  validation_commands: ["barry-cache validate"],
186
428
  contract_strength: "soft"
187
429
  }
@@ -190,7 +432,7 @@ async function resumeProject({ repo, task }) {
190
432
  async function finalizeProject(options) {
191
433
  const dir = repoPath(options.repo, ".context-state/handoffs");
192
434
  await mkdir2(dir, { recursive: true });
193
- const path = join3(dir, "handoffs.jsonl");
435
+ const path = join4(dir, "handoffs.jsonl");
194
436
  const record = {
195
437
  id: `handoff-${new Date().toISOString()}`,
196
438
  updated_at: new Date().toISOString(),
@@ -207,15 +449,15 @@ async function readFeaturePacks(repo) {
207
449
  const root = repoPath(repo, "docs/context/features");
208
450
  const slugs = await listDirs(root);
209
451
  const features = [];
210
- for (const slug of slugs) {
211
- const dir = join3(root, slug);
452
+ for (const slug2 of slugs) {
453
+ const dir = join4(root, slug2);
212
454
  features.push({
213
- slug,
455
+ slug: slug2,
214
456
  dir,
215
- readme: await readTextIfExists(join3(dir, "README.md")),
216
- idmap: await readTextIfExists(join3(dir, "IDMAP.md")),
217
- graph: await readTextIfExists(join3(dir, "KG.adj")),
218
- facts: await readFacts(join3(dir, "FACTS.jsonl"))
457
+ readme: await readTextIfExists(join4(dir, "README.md")),
458
+ idmap: await readTextIfExists(join4(dir, "IDMAP.md")),
459
+ graph: await readTextIfExists(join4(dir, "KG.adj")),
460
+ facts: await readFacts(join4(dir, "FACTS.jsonl"))
219
461
  });
220
462
  }
221
463
  return features;
@@ -232,14 +474,16 @@ async function readFacts(path) {
232
474
  }
233
475
  return facts;
234
476
  }
235
- function scoreFeature(feature, taskTokens) {
477
+ function scoreFeature(feature, taskTokens, adrs) {
478
+ const linkedAdrs = linkedAdrsForSources(feature.facts.flatMap((fact) => fact.src), adrs);
236
479
  const text = [
237
480
  feature.slug,
238
- basename(feature.dir),
481
+ basename2(feature.dir),
239
482
  feature.readme,
240
483
  feature.idmap,
241
484
  feature.graph,
242
- ...feature.facts.map(factToText)
485
+ ...feature.facts.map(factToText),
486
+ ...linkedAdrs.map(adrToText)
243
487
  ].join(" ");
244
488
  const score = scoreText(text, taskTokens);
245
489
  return {
@@ -263,6 +507,7 @@ function factToText(fact) {
263
507
  fact.object,
264
508
  fact.status,
265
509
  fact.kind,
510
+ ...fact.src,
266
511
  ...fact.tags ?? []
267
512
  ].join(" ");
268
513
  }
@@ -271,7 +516,7 @@ function firstLine(value) {
271
516
  }
272
517
 
273
518
  // src/core/import-pulpcut.ts
274
- import { join as join4 } from "node:path";
519
+ import { join as join5 } from "node:path";
275
520
  async function importPulpcutKb(options) {
276
521
  const dryRun = options.dryRun ?? false;
277
522
  const result = {
@@ -286,34 +531,34 @@ async function importPulpcutKb(options) {
286
531
  warnings: []
287
532
  };
288
533
  const sourceDocs = repoPath(options.from, "docs");
289
- const kbIndex = await readTextIfExists(join4(sourceDocs, "KB_INDEX.md"));
534
+ const kbIndex = await readTextIfExists(join5(sourceDocs, "KB_INDEX.md"));
290
535
  if (kbIndex.trim().length === 0) {
291
- throw new Error(`PulpCut KB index not found at ${join4(sourceDocs, "KB_INDEX.md")}`);
536
+ throw new Error(`PulpCut KB index not found at ${join5(sourceDocs, "KB_INDEX.md")}`);
292
537
  }
293
538
  const routes = parseKbIndex(kbIndex);
294
539
  const slugs = await listDirs(sourceDocs);
295
- for (const slug of slugs) {
296
- const sourceDir = join4(sourceDocs, slug);
297
- const rawFacts = await readTextIfExists(join4(sourceDir, "FACTS.jsonl"));
540
+ for (const slug2 of slugs) {
541
+ const sourceDir = join5(sourceDocs, slug2);
542
+ const rawFacts = await readTextIfExists(join5(sourceDir, "FACTS.jsonl"));
298
543
  if (rawFacts.trim().length === 0)
299
544
  continue;
300
- const idmap = await readTextIfExists(join4(sourceDir, "IDMAP.md"));
301
- const graph = await readTextIfExists(join4(sourceDir, "KG.adj"));
545
+ const idmap = await readTextIfExists(join5(sourceDir, "IDMAP.md"));
546
+ const graph = await readTextIfExists(join5(sourceDir, "KG.adj"));
302
547
  if (idmap.trim().length === 0)
303
- result.warnings.push(`${slug}: missing IDMAP.md`);
548
+ result.warnings.push(`${slug2}: missing IDMAP.md`);
304
549
  if (graph.trim().length === 0)
305
- result.warnings.push(`${slug}: missing KG.adj`);
306
- const facts = parsePulpcutFacts(rawFacts, slug, result.warnings);
307
- const route = routes.get(slug);
308
- const targetPrefix = `docs/context/features/${slug}`;
309
- await writePlanned(options.repo, result, `${targetPrefix}/README.md`, featureReadme(slug, route, idmap), dryRun);
550
+ result.warnings.push(`${slug2}: missing KG.adj`);
551
+ const facts = parsePulpcutFacts(rawFacts, slug2, result.warnings);
552
+ const route = routes.get(slug2);
553
+ const targetPrefix = `docs/context/features/${slug2}`;
554
+ await writePlanned(options.repo, result, `${targetPrefix}/README.md`, featureReadme(slug2, route, idmap), dryRun);
310
555
  await writePlanned(options.repo, result, `${targetPrefix}/IDMAP.md`, normalizeText(idmap), dryRun);
311
556
  await writePlanned(options.repo, result, `${targetPrefix}/KG.adj`, normalizeText(graph), dryRun);
312
557
  await writePlanned(options.repo, result, `${targetPrefix}/FACTS.jsonl`, facts.map((fact) => JSON.stringify(fact)).join(`
313
558
  `) + `
314
559
  `, dryRun);
315
560
  result.imported++;
316
- result.features.push(slug);
561
+ result.features.push(slug2);
317
562
  }
318
563
  if (result.imported === 0) {
319
564
  result.warnings.push("No PulpCut KB feature folders with FACTS.jsonl were found.");
@@ -321,7 +566,7 @@ async function importPulpcutKb(options) {
321
566
  result.features.sort();
322
567
  return result;
323
568
  }
324
- function parsePulpcutFacts(input, slug, warnings) {
569
+ function parsePulpcutFacts(input, slug2, warnings) {
325
570
  const facts = [];
326
571
  const seen = new Set;
327
572
  input.split(/\r?\n/).forEach((line, index) => {
@@ -332,7 +577,7 @@ function parsePulpcutFacts(input, slug, warnings) {
332
577
  parsed = JSON.parse(line);
333
578
  } catch (error) {
334
579
  const message = error instanceof Error ? error.message : String(error);
335
- warnings.push(`${slug}/FACTS.jsonl:${index + 1} invalid JSON: ${message}`);
580
+ warnings.push(`${slug2}/FACTS.jsonl:${index + 1} invalid JSON: ${message}`);
336
581
  return;
337
582
  }
338
583
  const id = stringValue(parsed.id);
@@ -341,11 +586,11 @@ function parsePulpcutFacts(input, slug, warnings) {
341
586
  const object = objectValue(parsed.o);
342
587
  const src = arrayOfStrings(parsed.src);
343
588
  if (!id || !subject || !predicate || !object || src.length === 0) {
344
- warnings.push(`${slug}/FACTS.jsonl:${index + 1} skipped row with missing id/s/p/o/src`);
589
+ warnings.push(`${slug2}/FACTS.jsonl:${index + 1} skipped row with missing id/s/p/o/src`);
345
590
  return;
346
591
  }
347
592
  if (seen.has(id))
348
- warnings.push(`${slug}/FACTS.jsonl:${index + 1} duplicate fact id ${id}`);
593
+ warnings.push(`${slug2}/FACTS.jsonl:${index + 1} duplicate fact id ${id}`);
349
594
  seen.add(id);
350
595
  const fact = {
351
596
  id,
@@ -405,10 +650,10 @@ function parseKbIndex(input) {
405
650
  }
406
651
  return routes;
407
652
  }
408
- function featureReadme(slug, route, idmap) {
653
+ function featureReadme(slug2, route, idmap) {
409
654
  const scope = extractScope(idmap);
410
655
  const sections = [
411
- `# ${titleFromSlug(slug)}`,
656
+ `# ${titleFromSlug(slug2)}`,
412
657
  "",
413
658
  "Imported from a PulpCut KB feature folder.",
414
659
  ""
@@ -456,8 +701,8 @@ function normalizeText(input) {
456
701
  return input.trimEnd().length === 0 ? "" : `${input.trimEnd()}
457
702
  `;
458
703
  }
459
- function titleFromSlug(slug) {
460
- return slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
704
+ function titleFromSlug(slug2) {
705
+ return slug2.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
461
706
  }
462
707
  function stringValue(value) {
463
708
  return typeof value === "string" && value.length > 0 ? value : undefined;
@@ -483,7 +728,7 @@ function stringOrStringArray(value) {
483
728
 
484
729
  // src/core/init.ts
485
730
  import { mkdir as mkdir3 } from "node:fs/promises";
486
- import { join as join5 } from "node:path";
731
+ import { join as join6 } from "node:path";
487
732
 
488
733
  // src/core/templates.ts
489
734
  var managedStart = "<!-- barry-cache:start -->";
@@ -507,37 +752,48 @@ function applyManagedBlock(existing, body) {
507
752
  ` : "";
508
753
  return `${prefix}${nextBlock}`;
509
754
  }
510
- var agentInstructions = `
755
+ function agentInstructions(commandPrefix = "barry-cache", installCommand) {
756
+ const commandNote = installCommand ? `Use the repo package script so Barry Cache runs from the local npm dependency without relying on shell PATH. If the dependency is missing, run \`${installCommand}\` first. Use \`npx barry-cache <command>\` only when package scripts are unavailable.` : "Use `barry-cache` directly. If it is unavailable, run it through your package runner, for example `npx barry-cache <command>`.";
757
+ return `
511
758
  ## Barry Cache
512
759
 
513
760
  Barry Cache remembers this repo through source-backed context files.
514
761
 
762
+ ${commandNote}
763
+
515
764
  Start task context with:
516
765
 
517
766
  \`\`\`bash
518
- barry-cache resume --task "<task>"
767
+ ${commandPrefix} resume --task "<task>"
519
768
  \`\`\`
520
769
 
521
770
  Use focused retrieval during work:
522
771
 
523
772
  \`\`\`bash
524
- barry-cache route --task "<task>"
525
- barry-cache search --query "<query>"
526
- barry-cache load --route "<route>"
773
+ ${commandPrefix} route --task "<task>"
774
+ ${commandPrefix} search --query "<query>"
775
+ ${commandPrefix} load --route "<route>"
527
776
  \`\`\`
528
777
 
529
778
  When context files change, run:
530
779
 
531
780
  \`\`\`bash
532
- barry-cache validate
781
+ ${commandPrefix} validate
533
782
  \`\`\`
534
783
 
535
784
  Before handing off substantial work, record factual evidence:
536
785
 
537
786
  \`\`\`bash
538
- barry-cache finalize --status success --summary "<summary>"
787
+ ${commandPrefix} finalize --status success --summary "<summary>"
539
788
  \`\`\`
789
+
790
+ Memory policy:
791
+
792
+ - Finalize writes operational memory only.
793
+ - Do not claim Barry canonical memory is updated unless \`docs/context/\` changed.
794
+ - If a task adds durable implementation behavior, add or update source-backed facts in \`docs/context/features/*/FACTS.jsonl\` and run \`${commandPrefix} validate\`.
540
795
  `;
796
+ }
541
797
  var indexMd = `# Context Index
542
798
 
543
799
  This directory stores source-backed context for coding agents and humans.
@@ -552,6 +808,10 @@ barry-cache validate
552
808
  ## Routes
553
809
 
554
810
  Feature context packs live in \`docs/context/features/*\`.
811
+
812
+ ## Decisions
813
+
814
+ Architecture decision records live in \`docs/context/adrs/*\`.
555
815
  `;
556
816
  var logMd = `# Context Log
557
817
 
@@ -561,7 +821,8 @@ var maintenanceMd = `# Context Maintenance
561
821
 
562
822
  - Keep project truth in Git.
563
823
  - Add source-backed facts to feature \`FACTS.jsonl\` files.
564
- - Use ADRs for decisions that change architecture.
824
+ - Use \`barry-cache adr new --title "<decision>"\` for decisions that change architecture.
825
+ - Reference ADR files from decision facts through the fact \`src\` array.
565
826
  - Treat \`.context-state/\` as operational memory, not canonical truth.
566
827
  - Run \`barry-cache validate\` after context changes.
567
828
 
@@ -596,6 +857,19 @@ This directory is the canonical project memory for Barry Cache. It keeps durable
596
857
 
597
858
  Barry separates three concerns: \`docs/context/\` is reviewed truth, \`.context-state/\` is operational session continuity, and \`.context-cache/\` is disposable retrieval data. Use this structure to explain existing behavior, route tasks, validate facts, and resume agent work without loading the whole repo.
598
859
  `;
860
+ var adrReadmeMd = `# Architecture Decision Records
861
+
862
+ ADRs explain why durable architectural decisions exist. Keep them short, source-backed, and linked from feature facts when a decision affects implementation behavior.
863
+
864
+ Use:
865
+
866
+ \`\`\`bash
867
+ barry-cache adr new --title "<decision>"
868
+ barry-cache adr list
869
+ \`\`\`
870
+
871
+ Facts can reference ADRs with \`src: ["docs/context/adrs/ADR-0001-example.md"]\`. Barry can then route, search, load, and review the decision together with the facts it supports.
872
+ `;
599
873
  var conceptOverviewMd = `# Project Context Model
600
874
 
601
875
  Barry Cache separates canonical context, operational state, and generated caches.
@@ -622,6 +896,20 @@ var factSchema = {
622
896
  tags: { type: "array", items: { type: "string" } }
623
897
  }
624
898
  };
899
+ var adrSchema = {
900
+ $schema: "https://json-schema.org/draft/2020-12/schema",
901
+ title: "Barry Cache ADR frontmatter",
902
+ type: "object",
903
+ required: ["id", "title", "status", "date"],
904
+ properties: {
905
+ id: { type: "string", pattern: "^ADR-[0-9]{4}$" },
906
+ title: { type: "string", minLength: 1 },
907
+ status: { enum: ["active", "superseded", "deprecated"] },
908
+ date: { type: "string", pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
909
+ supersedes: { type: "array", items: { type: "string" } },
910
+ tags: { type: "array", items: { type: "string" } }
911
+ }
912
+ };
625
913
  var routeSchema = {
626
914
  $schema: "https://json-schema.org/draft/2020-12/schema",
627
915
  title: "Barry Cache route",
@@ -646,11 +934,11 @@ var failureSchema = {
646
934
  // src/core/init.ts
647
935
  var allAgentTargets = ["codex", "cursor", "copilot", "claude", "gemini", "llms"];
648
936
  var adapterFiles = [
649
- { target: "cursor", path: ".cursor/rules/barry-cache.mdc", content: adapterFile("Cursor") },
650
- { target: "copilot", path: ".github/copilot-instructions.md", content: adapterFile("GitHub Copilot") },
651
- { target: "claude", path: "CLAUDE.md", content: adapterFile("Claude Code") },
652
- { target: "gemini", path: "GEMINI.md", content: adapterFile("Gemini") },
653
- { target: "llms", path: "llms.txt", content: llmsTxt() }
937
+ { target: "cursor", path: ".cursor/rules/barry-cache.mdc", agent: "Cursor" },
938
+ { target: "copilot", path: ".github/copilot-instructions.md", agent: "GitHub Copilot" },
939
+ { target: "claude", path: "CLAUDE.md", agent: "Claude Code" },
940
+ { target: "gemini", path: "GEMINI.md", agent: "Gemini" },
941
+ { target: "llms", path: "llms.txt", agent: "LLM" }
654
942
  ];
655
943
  async function initProject(options) {
656
944
  const repo = options.repo;
@@ -661,7 +949,10 @@ async function initProject(options) {
661
949
  { path: "docs/context/INDEX.md", content: indexMd },
662
950
  { path: "docs/context/LOG.md", content: logMd },
663
951
  { path: "docs/context/MAINTENANCE.md", content: maintenanceMd },
952
+ { path: "docs/context/adrs/README.md", content: adrReadmeMd },
664
953
  { path: "docs/context/concepts/project-context-model.md", content: conceptOverviewMd },
954
+ { path: "docs/context/schema/adr.schema.json", content: `${JSON.stringify(adrSchema, null, 2)}
955
+ ` },
665
956
  { path: "docs/context/schema/fact.schema.json", content: `${JSON.stringify(factSchema, null, 2)}
666
957
  ` },
667
958
  { path: "docs/context/schema/route.schema.json", content: `${JSON.stringify(routeSchema, null, 2)}
@@ -677,17 +968,18 @@ async function initProject(options) {
677
968
  const status = await writeIfChanged(repoPath(repo, file.path), file.content, dryRun);
678
969
  record(result, status, file.path);
679
970
  }
680
- await patchAgentInstructions(repo, dryRun, result, options.agents);
681
- await patchGitignore(repo, dryRun, result);
682
971
  const packageManager = await patchPackageJson(repo, dryRun, result);
683
972
  if (packageManager)
684
973
  result.packageManager = packageManager;
974
+ const commandPrefix = barryCommandPrefix(packageManager);
975
+ await patchAgentInstructions(repo, dryRun, result, options.agents, commandPrefix, packageManager);
976
+ await patchGitignore(repo, dryRun, result);
685
977
  if (!dryRun) {
686
- await mkdir3(join5(repo, ".context-state/work/threads"), { recursive: true });
687
- await mkdir3(join5(repo, ".context-state/handoffs"), { recursive: true });
688
- await mkdir3(join5(repo, ".context-state/failures"), { recursive: true });
689
- await mkdir3(join5(repo, ".context-state/strategies"), { recursive: true });
690
- await mkdir3(join5(repo, ".context-cache"), { recursive: true });
978
+ await mkdir3(join6(repo, ".context-state/work/threads"), { recursive: true });
979
+ await mkdir3(join6(repo, ".context-state/handoffs"), { recursive: true });
980
+ await mkdir3(join6(repo, ".context-state/failures"), { recursive: true });
981
+ await mkdir3(join6(repo, ".context-state/strategies"), { recursive: true });
982
+ await mkdir3(join6(repo, ".context-cache"), { recursive: true });
691
983
  }
692
984
  result.changed = result.written.length > 0 || result.updated.length > 0;
693
985
  return result;
@@ -700,20 +992,21 @@ function record(result, status, path) {
700
992
  if (status === "skipped")
701
993
  result.skipped.push(path);
702
994
  }
703
- async function patchAgentInstructions(repo, dryRun, result, agents) {
995
+ async function patchAgentInstructions(repo, dryRun, result, agents, commandPrefix, packageManager) {
704
996
  const selected = new Set(agents ?? allAgentTargets);
705
997
  if (selected.has("codex"))
706
- await patchCodexAgents(repo, dryRun, result);
998
+ await patchCodexAgents(repo, dryRun, result, commandPrefix, packageManager);
707
999
  for (const file of adapterFiles) {
708
1000
  if (!selected.has(file.target))
709
1001
  continue;
710
- record(result, await writeIfChanged(repoPath(repo, file.path), file.content, dryRun), file.path);
1002
+ const content = file.target === "llms" ? llmsTxt() : adapterFile(file.agent, commandPrefix);
1003
+ record(result, await writeIfChanged(repoPath(repo, file.path), content, dryRun), file.path);
711
1004
  }
712
1005
  }
713
- async function patchCodexAgents(repo, dryRun, result) {
1006
+ async function patchCodexAgents(repo, dryRun, result, commandPrefix, packageManager) {
714
1007
  const path = repoPath(repo, "AGENTS.md");
715
1008
  const existing = await exists(path) ? await readText(path) : "";
716
- const content = applyManagedBlock(existing, agentInstructions);
1009
+ const content = applyManagedBlock(existing, agentInstructions(commandPrefix, packageManager?.installCommand));
717
1010
  record(result, await writeIfChanged(path, content, dryRun), "AGENTS.md");
718
1011
  }
719
1012
  async function patchGitignore(repo, dryRun, result) {
@@ -764,7 +1057,10 @@ function packageManagerHint(name) {
764
1057
  installCommand: `${name} install`
765
1058
  };
766
1059
  }
767
- function adapterFile(agent) {
1060
+ function barryCommandPrefix(packageManager) {
1061
+ return packageManager ? `${packageManager.name} run barry --` : "barry-cache";
1062
+ }
1063
+ function adapterFile(agent, commandPrefix) {
768
1064
  return `# Barry Cache for ${agent}
769
1065
 
770
1066
  Canonical context lives in \`docs/context/\`.
@@ -772,14 +1068,26 @@ Canonical context lives in \`docs/context/\`.
772
1068
  Start by running:
773
1069
 
774
1070
  \`\`\`bash
775
- barry-cache resume --task "<task>"
1071
+ ${commandPrefix} resume --task "<task>"
776
1072
  \`\`\`
777
1073
 
778
1074
  Validate context changes with:
779
1075
 
780
1076
  \`\`\`bash
781
- barry-cache validate
1077
+ ${commandPrefix} validate
1078
+ \`\`\`
1079
+
1080
+ Before handing off substantial work, record factual evidence:
1081
+
1082
+ \`\`\`bash
1083
+ ${commandPrefix} finalize --status success --summary "<summary>"
782
1084
  \`\`\`
1085
+
1086
+ Memory policy:
1087
+
1088
+ - Finalize writes operational memory only.
1089
+ - Do not claim Barry canonical memory is updated unless \`docs/context/\` changed.
1090
+ - If a task adds durable implementation behavior, add or update source-backed facts in \`docs/context/features/*/FACTS.jsonl\` and run \`${commandPrefix} validate\`.
783
1091
  `;
784
1092
  }
785
1093
  function llmsTxt() {
@@ -794,7 +1102,7 @@ function llmsTxt() {
794
1102
  }
795
1103
 
796
1104
  // src/core/review-model.ts
797
- import { join as join6 } from "node:path";
1105
+ import { join as join7 } from "node:path";
798
1106
 
799
1107
  // src/core/review-tree.ts
800
1108
  function buildReviewTree(facts) {
@@ -852,7 +1160,7 @@ function buildReviewTree(facts) {
852
1160
  const grouped = groupFacts(routeFacts, groupBy);
853
1161
  for (const [value, groupFacts] of grouped) {
854
1162
  groups.push({
855
- id: `tree:group:${route}:${groupBy}:${slug(value)}`,
1163
+ id: `tree:group:${route}:${groupBy}:${slug2(value)}`,
856
1164
  route,
857
1165
  groupBy,
858
1166
  value,
@@ -933,10 +1241,10 @@ function sortedRecord(record2) {
933
1241
  function mapSetToRecord(map) {
934
1242
  return Object.fromEntries([...map.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, values]) => [key, [...values].sort()]));
935
1243
  }
936
- function titleFromSlug2(slug) {
937
- return slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1244
+ function titleFromSlug2(slug2) {
1245
+ return slug2.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
938
1246
  }
939
- function slug(input) {
1247
+ function slug2(input) {
940
1248
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "value";
941
1249
  }
942
1250
 
@@ -947,6 +1255,9 @@ async function buildReviewModel({ repo }) {
947
1255
  const facts = [];
948
1256
  const timeline = [];
949
1257
  const features = await readFeaturePacks(repo);
1258
+ const adrs = await listAdrs({ repo });
1259
+ for (const adr of adrs)
1260
+ addAdr(graph, adr);
950
1261
  for (const feature of features) {
951
1262
  addFeature(graph, repo, feature);
952
1263
  const sourceMap = parseIdMap(feature.idmap);
@@ -954,10 +1265,10 @@ async function buildReviewModel({ repo }) {
954
1265
  for (const fact of feature.facts) {
955
1266
  facts.push({
956
1267
  route: feature.slug,
957
- source: `${rel(repo, join6(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
1268
+ source: `${rel(repo, join7(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
958
1269
  fact
959
1270
  });
960
- addFact(graph, repo, feature, fact, sourceMap);
1271
+ addFact(graph, repo, feature, fact, sourceMap, adrs);
961
1272
  }
962
1273
  }
963
1274
  timeline.push(...await addOperationalRecords({
@@ -999,6 +1310,7 @@ async function buildReviewModel({ repo }) {
999
1310
  summary: {
1000
1311
  features: features.length,
1001
1312
  facts: facts.length,
1313
+ adrs: adrs.length,
1002
1314
  entities: nodes.filter((node) => node.kind === "entity").length,
1003
1315
  sources: nodes.filter((node) => node.kind === "source").length,
1004
1316
  handoffs: timeline.filter((item) => item.kind === "handoff").length,
@@ -1028,14 +1340,14 @@ function addFeature(graph, repo, feature) {
1028
1340
  }
1029
1341
  });
1030
1342
  }
1031
- function addFact(graph, repo, feature, fact, sourceMap) {
1343
+ function addFact(graph, repo, feature, fact, sourceMap, adrs) {
1032
1344
  const factId = `fact:${fact.id}`;
1033
1345
  addNode(graph, {
1034
1346
  id: factId,
1035
1347
  kind: "fact",
1036
1348
  label: fact.id,
1037
1349
  subtitle: `${fact.subject} ${fact.predicate} ${fact.object}`,
1038
- source: `${rel(repo, join6(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
1350
+ source: `${rel(repo, join7(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
1039
1351
  meta: {
1040
1352
  route: feature.slug,
1041
1353
  status: fact.status,
@@ -1069,7 +1381,8 @@ function addFact(graph, repo, feature, fact, sourceMap) {
1069
1381
  });
1070
1382
  for (const source of fact.src) {
1071
1383
  const resolved = sourceMap.get(source) ?? source;
1072
- const sourceId = addSource(graph, resolved, { alias: resolved === source ? undefined : source });
1384
+ const adr = adrs.find((item) => adrMatchesSource(item, resolved) || adrMatchesSource(item, source));
1385
+ const sourceId = adr ? `adr:${adr.id}` : addSource(graph, resolved, { alias: resolved === source ? undefined : source });
1073
1386
  addEdge(graph, {
1074
1387
  source: factId,
1075
1388
  target: sourceId,
@@ -1087,6 +1400,30 @@ function addFact(graph, repo, feature, fact, sourceMap) {
1087
1400
  });
1088
1401
  }
1089
1402
  }
1403
+ function addAdr(graph, adr) {
1404
+ const nodeId = `adr:${adr.id}`;
1405
+ addNode(graph, {
1406
+ id: nodeId,
1407
+ kind: "adr",
1408
+ label: adr.id,
1409
+ subtitle: adr.title,
1410
+ source: adr.path,
1411
+ meta: {
1412
+ status: adr.status,
1413
+ date: adr.date,
1414
+ supersedes: adr.supersedes,
1415
+ tags: adr.tags
1416
+ }
1417
+ });
1418
+ for (const superseded of adr.supersedes) {
1419
+ addEdge(graph, {
1420
+ source: nodeId,
1421
+ target: `adr:${superseded}`,
1422
+ kind: "supersedes",
1423
+ meta: {}
1424
+ });
1425
+ }
1426
+ }
1090
1427
  function addKgEdges(graph, feature) {
1091
1428
  for (const line of feature.graph.split(/\r?\n/)) {
1092
1429
  const trimmed = line.trim();
@@ -1175,7 +1512,7 @@ async function readJsonl(path, displayPath, warnings) {
1175
1512
  return records;
1176
1513
  }
1177
1514
  function addEntity(graph, label) {
1178
- const id = `entity:${slug2(label)}`;
1515
+ const id = `entity:${slug3(label)}`;
1179
1516
  addNode(graph, {
1180
1517
  id,
1181
1518
  kind: "entity",
@@ -1250,7 +1587,7 @@ function firstMarkdownHeading(input) {
1250
1587
  const heading = input.split(/\r?\n/).find((line) => line.startsWith("# "));
1251
1588
  return heading?.replace(/^#\s*/, "").trim() ?? "";
1252
1589
  }
1253
- function slug2(input) {
1590
+ function slug3(input) {
1254
1591
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
1255
1592
  }
1256
1593
  function stripBackticks(input) {
@@ -3596,7 +3933,12 @@ async function main(argv = process.argv.slice(2)) {
3596
3933
  case "finalize": {
3597
3934
  const status = optionalChoice(parsed, "status", finalizeStatuses, "success", commandUsage("finalize"));
3598
3935
  const summary = requiredString(parsed, "summary", commandUsage("finalize"), { status: [...finalizeStatuses] });
3599
- print(await finalizeProject({ repo, status, summary }), json);
3936
+ const result = await finalizeProject({ repo, status, summary });
3937
+ print(result, json, formatFinalizeMessage(result));
3938
+ break;
3939
+ }
3940
+ case "adr": {
3941
+ await handleAdrCommand(parsed, repo, json);
3600
3942
  break;
3601
3943
  }
3602
3944
  case "import": {
@@ -3671,10 +4013,14 @@ async function main(argv = process.argv.slice(2)) {
3671
4013
  function parseArgs(argv) {
3672
4014
  const [command = "help", ...rest] = argv;
3673
4015
  const flags = new Map;
4016
+ const positionals = [];
3674
4017
  for (let index = 0;index < rest.length; index++) {
3675
4018
  const arg = rest[index];
3676
- if (!arg?.startsWith("--"))
4019
+ if (!arg?.startsWith("--")) {
4020
+ if (arg)
4021
+ positionals.push(arg);
3677
4022
  continue;
4023
+ }
3678
4024
  const key = arg.slice(2);
3679
4025
  const next = rest[index + 1];
3680
4026
  if (next && !next.startsWith("--")) {
@@ -3684,7 +4030,7 @@ function parseArgs(argv) {
3684
4030
  flags.set(key, true);
3685
4031
  }
3686
4032
  }
3687
- return { command, flags };
4033
+ return { command, positionals, flags };
3688
4034
  }
3689
4035
  function requiredString(parsed, key, usageValue, options) {
3690
4036
  const value = parsed.flags.get(key);
@@ -3719,6 +4065,14 @@ function optionalChoice(parsed, key, choices, fallback, usageValue) {
3719
4065
  }
3720
4066
  return value;
3721
4067
  }
4068
+ function optionalList(parsed, key, usageValue) {
4069
+ const value = parsed.flags.get(key);
4070
+ if (value === undefined)
4071
+ return [];
4072
+ if (typeof value !== "string")
4073
+ throw new CliArgumentError(`--${key} requires a comma-separated value`, { usage: usageValue });
4074
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
4075
+ }
3722
4076
  function parseAgentTargets(value) {
3723
4077
  if (value === undefined)
3724
4078
  return;
@@ -3759,6 +4113,44 @@ function print(value, json, message) {
3759
4113
  }
3760
4114
  console.log(JSON.stringify(value, null, 2));
3761
4115
  }
4116
+ async function handleAdrCommand(parsed, repo, json) {
4117
+ const action = parsed.positionals[0];
4118
+ if (action === "new") {
4119
+ const title = requiredString(parsed, "title", commandUsage("adr new"));
4120
+ const status = optionalChoice(parsed, "status", adrStatuses, "active", commandUsage("adr new"));
4121
+ const adr = await createAdr({
4122
+ repo,
4123
+ title,
4124
+ status,
4125
+ supersedes: optionalList(parsed, "supersedes", commandUsage("adr new")),
4126
+ tags: optionalList(parsed, "tags", commandUsage("adr new"))
4127
+ });
4128
+ print(adr, json, `Created ${adr.id} at ${adr.path}.`);
4129
+ return;
4130
+ }
4131
+ if (action === "list") {
4132
+ const adrs = await listAdrs({ repo });
4133
+ print({ adrs }, json, formatAdrList(adrs));
4134
+ return;
4135
+ }
4136
+ throw new CliArgumentError(action ? `Unknown ADR action: ${action}` : "Missing ADR action", {
4137
+ usage: commandUsage("adr")
4138
+ });
4139
+ }
4140
+ function formatAdrList(adrs) {
4141
+ if (adrs.length === 0)
4142
+ return "No ADRs found.";
4143
+ return adrs.map((adr) => `${adr.id} ${adr.status} ${adr.title} (${adr.path})`).join(`
4144
+ `);
4145
+ }
4146
+ function formatFinalizeMessage(result) {
4147
+ return [
4148
+ `Saved operational handoff to ${result.path}.`,
4149
+ "Finalize writes operational memory only; it does not update canonical context in docs/context/.",
4150
+ "If this task introduced durable implementation behavior, add or update docs/context/features/*/FACTS.jsonl and run barry-cache validate."
4151
+ ].join(`
4152
+ `);
4153
+ }
3762
4154
  function formatInitMessage(result) {
3763
4155
  if (!result.dryRun) {
3764
4156
  const lines2 = [`Barry Cache init ${result.changed ? "changed files" : "already up to date"}.`];
@@ -3827,6 +4219,9 @@ function errorMessage(error) {
3827
4219
  function commandUsage(command) {
3828
4220
  const usages = {
3829
4221
  init: "barry-cache init [--yes] [--dry-run] [--agents all|none|codex,cursor,copilot,claude,gemini,llms]",
4222
+ adr: "barry-cache adr <new|list> [--json]",
4223
+ "adr new": 'barry-cache adr new --title "..." [--status active|superseded|deprecated] [--supersedes ADR-0001] [--tags context,agents] [--json]',
4224
+ "adr list": "barry-cache adr list [--json]",
3830
4225
  route: 'barry-cache route --task "..." [--json]',
3831
4226
  search: 'barry-cache search --query "..." [--json]',
3832
4227
  load: 'barry-cache load --route "..." [--json]',
@@ -3853,6 +4248,8 @@ Usage:
3853
4248
  barry-cache load --route "..." [--json]
3854
4249
  barry-cache resume --task "..." [--json]
3855
4250
  barry-cache finalize --summary "..." [--status success] [--json]
4251
+ barry-cache adr new --title "..." [--status active] [--tags context,agents]
4252
+ barry-cache adr list [--json]
3856
4253
  barry-cache import --source pulpcut-kb --from /path/to/repo [--dry-run] [--json]
3857
4254
  barry-cache review [--port 8787] [--open]
3858
4255
  barry-cache review --json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "barry-cache",
3
- "version": "0.2.2",
3
+ "version": "0.3.2",
4
4
  "description": "Barry Cache remembers your repo.",
5
5
  "type": "module",
6
6
  "bin": {