barry-cache 0.2.2 → 0.3.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 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://unpkg.com/barry-cache@latest/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
 
@@ -156,6 +156,33 @@ Statuses:
156
156
  - `blocked`: the task cannot proceed without external input.
157
157
  - `failed`: the attempted approach did not work.
158
158
 
159
+ ### `barry-cache adr`
160
+
161
+ Creates and lists architecture decision records.
162
+
163
+ ```bash
164
+ barry-cache adr new --title "Use repo-native context"
165
+ barry-cache adr new --title "Keep generated indexes disposable" --tags context,cache
166
+ barry-cache adr list
167
+ ```
168
+
169
+ 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`.
170
+
171
+ ```json
172
+ {
173
+ "id": "CTX001",
174
+ "subject": "Barry",
175
+ "predicate": "stores canonical context in",
176
+ "object": "docs/context/",
177
+ "src": ["docs/context/adrs/ADR-0001-use-repo-native-context.md"],
178
+ "status": "active",
179
+ "kind": "decision",
180
+ "updated_at": "2026-05-19"
181
+ }
182
+ ```
183
+
184
+ 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.
185
+
159
186
  ### `barry-cache review`
160
187
 
161
188
  Opens a local browser tool for inspecting memory.
@@ -228,6 +255,12 @@ npx barry-cache init
228
255
 
229
256
  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
257
 
258
+ 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:
259
+
260
+ ```bash
261
+ bun run barry -- resume --task "fix playback drift in the editor"
262
+ ```
263
+
231
264
  For a Codex-only repo, use:
232
265
 
233
266
  ```bash
@@ -238,7 +271,7 @@ Those generated files tell coding agents how to use Barry before they edit the r
238
271
 
239
272
  ### Ask An Agent To Work On A Task
240
273
 
241
- Usually, you do not need to run `barry-cache resume` yourself.
274
+ Usually, you do not need to run Barry yourself.
242
275
 
243
276
  Ask your coding agent normally:
244
277
 
@@ -249,13 +282,15 @@ Fix playback drift in the editor.
249
282
  Because `barry-cache init` added instructions to the repo, the agent should run this before non-trivial work:
250
283
 
251
284
  ```bash
252
- barry-cache resume --task "fix playback drift in the editor"
285
+ bun run barry -- resume --task "fix playback drift in the editor"
253
286
  ```
254
287
 
288
+ The generated instruction file uses the package manager Barry detected for that repo, for example `bun`, `npm`, `pnpm`, or `yarn`.
289
+
255
290
  The agent then uses the returned routes to load focused context:
256
291
 
257
292
  ```bash
258
- barry-cache load --route editor-media-runtime
293
+ bun run barry -- load --route editor-media-runtime
259
294
  ```
260
295
 
261
296
  This keeps the agent from reading every context file in the repo.
@@ -264,7 +299,7 @@ If an agent ignores the repo instructions, prompt it explicitly:
264
299
 
265
300
  ```text
266
301
  Before editing, follow Barry Cache protocol:
267
- 1. Run barry-cache resume --task "<my task>".
302
+ 1. Run the repo's Barry package script, for example bun run barry -- resume --task "<my task>".
268
303
  2. Load the returned route context.
269
304
  3. Do the work.
270
305
  4. Run validation.
@@ -287,6 +322,16 @@ Use the browser review tool for a broader overview:
287
322
  barry-cache review
288
323
  ```
289
324
 
325
+ ### Record An Architectural Decision
326
+
327
+ Create an ADR when a decision explains why future agents should preserve or intentionally change architecture:
328
+
329
+ ```bash
330
+ barry-cache adr new --title "Use repo-native context" --tags context,agents
331
+ ```
332
+
333
+ 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.
334
+
290
335
  ### Save Agent Sessions
291
336
 
292
337
  At the end of a meaningful session, ask the agent:
Binary file
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,42 @@ 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
  \`\`\`
540
789
  `;
790
+ }
541
791
  var indexMd = `# Context Index
542
792
 
543
793
  This directory stores source-backed context for coding agents and humans.
@@ -552,6 +802,10 @@ barry-cache validate
552
802
  ## Routes
553
803
 
554
804
  Feature context packs live in \`docs/context/features/*\`.
805
+
806
+ ## Decisions
807
+
808
+ Architecture decision records live in \`docs/context/adrs/*\`.
555
809
  `;
556
810
  var logMd = `# Context Log
557
811
 
@@ -561,7 +815,8 @@ var maintenanceMd = `# Context Maintenance
561
815
 
562
816
  - Keep project truth in Git.
563
817
  - Add source-backed facts to feature \`FACTS.jsonl\` files.
564
- - Use ADRs for decisions that change architecture.
818
+ - Use \`barry-cache adr new --title "<decision>"\` for decisions that change architecture.
819
+ - Reference ADR files from decision facts through the fact \`src\` array.
565
820
  - Treat \`.context-state/\` as operational memory, not canonical truth.
566
821
  - Run \`barry-cache validate\` after context changes.
567
822
 
@@ -596,6 +851,19 @@ This directory is the canonical project memory for Barry Cache. It keeps durable
596
851
 
597
852
  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
853
  `;
854
+ var adrReadmeMd = `# Architecture Decision Records
855
+
856
+ ADRs explain why durable architectural decisions exist. Keep them short, source-backed, and linked from feature facts when a decision affects implementation behavior.
857
+
858
+ Use:
859
+
860
+ \`\`\`bash
861
+ barry-cache adr new --title "<decision>"
862
+ barry-cache adr list
863
+ \`\`\`
864
+
865
+ 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.
866
+ `;
599
867
  var conceptOverviewMd = `# Project Context Model
600
868
 
601
869
  Barry Cache separates canonical context, operational state, and generated caches.
@@ -622,6 +890,20 @@ var factSchema = {
622
890
  tags: { type: "array", items: { type: "string" } }
623
891
  }
624
892
  };
893
+ var adrSchema = {
894
+ $schema: "https://json-schema.org/draft/2020-12/schema",
895
+ title: "Barry Cache ADR frontmatter",
896
+ type: "object",
897
+ required: ["id", "title", "status", "date"],
898
+ properties: {
899
+ id: { type: "string", pattern: "^ADR-[0-9]{4}$" },
900
+ title: { type: "string", minLength: 1 },
901
+ status: { enum: ["active", "superseded", "deprecated"] },
902
+ date: { type: "string", pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
903
+ supersedes: { type: "array", items: { type: "string" } },
904
+ tags: { type: "array", items: { type: "string" } }
905
+ }
906
+ };
625
907
  var routeSchema = {
626
908
  $schema: "https://json-schema.org/draft/2020-12/schema",
627
909
  title: "Barry Cache route",
@@ -646,11 +928,11 @@ var failureSchema = {
646
928
  // src/core/init.ts
647
929
  var allAgentTargets = ["codex", "cursor", "copilot", "claude", "gemini", "llms"];
648
930
  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() }
931
+ { target: "cursor", path: ".cursor/rules/barry-cache.mdc", agent: "Cursor" },
932
+ { target: "copilot", path: ".github/copilot-instructions.md", agent: "GitHub Copilot" },
933
+ { target: "claude", path: "CLAUDE.md", agent: "Claude Code" },
934
+ { target: "gemini", path: "GEMINI.md", agent: "Gemini" },
935
+ { target: "llms", path: "llms.txt", agent: "LLM" }
654
936
  ];
655
937
  async function initProject(options) {
656
938
  const repo = options.repo;
@@ -661,7 +943,10 @@ async function initProject(options) {
661
943
  { path: "docs/context/INDEX.md", content: indexMd },
662
944
  { path: "docs/context/LOG.md", content: logMd },
663
945
  { path: "docs/context/MAINTENANCE.md", content: maintenanceMd },
946
+ { path: "docs/context/adrs/README.md", content: adrReadmeMd },
664
947
  { path: "docs/context/concepts/project-context-model.md", content: conceptOverviewMd },
948
+ { path: "docs/context/schema/adr.schema.json", content: `${JSON.stringify(adrSchema, null, 2)}
949
+ ` },
665
950
  { path: "docs/context/schema/fact.schema.json", content: `${JSON.stringify(factSchema, null, 2)}
666
951
  ` },
667
952
  { path: "docs/context/schema/route.schema.json", content: `${JSON.stringify(routeSchema, null, 2)}
@@ -677,17 +962,18 @@ async function initProject(options) {
677
962
  const status = await writeIfChanged(repoPath(repo, file.path), file.content, dryRun);
678
963
  record(result, status, file.path);
679
964
  }
680
- await patchAgentInstructions(repo, dryRun, result, options.agents);
681
- await patchGitignore(repo, dryRun, result);
682
965
  const packageManager = await patchPackageJson(repo, dryRun, result);
683
966
  if (packageManager)
684
967
  result.packageManager = packageManager;
968
+ const commandPrefix = barryCommandPrefix(packageManager);
969
+ await patchAgentInstructions(repo, dryRun, result, options.agents, commandPrefix, packageManager);
970
+ await patchGitignore(repo, dryRun, result);
685
971
  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 });
972
+ await mkdir3(join6(repo, ".context-state/work/threads"), { recursive: true });
973
+ await mkdir3(join6(repo, ".context-state/handoffs"), { recursive: true });
974
+ await mkdir3(join6(repo, ".context-state/failures"), { recursive: true });
975
+ await mkdir3(join6(repo, ".context-state/strategies"), { recursive: true });
976
+ await mkdir3(join6(repo, ".context-cache"), { recursive: true });
691
977
  }
692
978
  result.changed = result.written.length > 0 || result.updated.length > 0;
693
979
  return result;
@@ -700,20 +986,21 @@ function record(result, status, path) {
700
986
  if (status === "skipped")
701
987
  result.skipped.push(path);
702
988
  }
703
- async function patchAgentInstructions(repo, dryRun, result, agents) {
989
+ async function patchAgentInstructions(repo, dryRun, result, agents, commandPrefix, packageManager) {
704
990
  const selected = new Set(agents ?? allAgentTargets);
705
991
  if (selected.has("codex"))
706
- await patchCodexAgents(repo, dryRun, result);
992
+ await patchCodexAgents(repo, dryRun, result, commandPrefix, packageManager);
707
993
  for (const file of adapterFiles) {
708
994
  if (!selected.has(file.target))
709
995
  continue;
710
- record(result, await writeIfChanged(repoPath(repo, file.path), file.content, dryRun), file.path);
996
+ const content = file.target === "llms" ? llmsTxt() : adapterFile(file.agent, commandPrefix);
997
+ record(result, await writeIfChanged(repoPath(repo, file.path), content, dryRun), file.path);
711
998
  }
712
999
  }
713
- async function patchCodexAgents(repo, dryRun, result) {
1000
+ async function patchCodexAgents(repo, dryRun, result, commandPrefix, packageManager) {
714
1001
  const path = repoPath(repo, "AGENTS.md");
715
1002
  const existing = await exists(path) ? await readText(path) : "";
716
- const content = applyManagedBlock(existing, agentInstructions);
1003
+ const content = applyManagedBlock(existing, agentInstructions(commandPrefix, packageManager?.installCommand));
717
1004
  record(result, await writeIfChanged(path, content, dryRun), "AGENTS.md");
718
1005
  }
719
1006
  async function patchGitignore(repo, dryRun, result) {
@@ -764,7 +1051,10 @@ function packageManagerHint(name) {
764
1051
  installCommand: `${name} install`
765
1052
  };
766
1053
  }
767
- function adapterFile(agent) {
1054
+ function barryCommandPrefix(packageManager) {
1055
+ return packageManager ? `${packageManager.name} run barry --` : "barry-cache";
1056
+ }
1057
+ function adapterFile(agent, commandPrefix) {
768
1058
  return `# Barry Cache for ${agent}
769
1059
 
770
1060
  Canonical context lives in \`docs/context/\`.
@@ -772,13 +1062,13 @@ Canonical context lives in \`docs/context/\`.
772
1062
  Start by running:
773
1063
 
774
1064
  \`\`\`bash
775
- barry-cache resume --task "<task>"
1065
+ ${commandPrefix} resume --task "<task>"
776
1066
  \`\`\`
777
1067
 
778
1068
  Validate context changes with:
779
1069
 
780
1070
  \`\`\`bash
781
- barry-cache validate
1071
+ ${commandPrefix} validate
782
1072
  \`\`\`
783
1073
  `;
784
1074
  }
@@ -794,7 +1084,7 @@ function llmsTxt() {
794
1084
  }
795
1085
 
796
1086
  // src/core/review-model.ts
797
- import { join as join6 } from "node:path";
1087
+ import { join as join7 } from "node:path";
798
1088
 
799
1089
  // src/core/review-tree.ts
800
1090
  function buildReviewTree(facts) {
@@ -852,7 +1142,7 @@ function buildReviewTree(facts) {
852
1142
  const grouped = groupFacts(routeFacts, groupBy);
853
1143
  for (const [value, groupFacts] of grouped) {
854
1144
  groups.push({
855
- id: `tree:group:${route}:${groupBy}:${slug(value)}`,
1145
+ id: `tree:group:${route}:${groupBy}:${slug2(value)}`,
856
1146
  route,
857
1147
  groupBy,
858
1148
  value,
@@ -933,10 +1223,10 @@ function sortedRecord(record2) {
933
1223
  function mapSetToRecord(map) {
934
1224
  return Object.fromEntries([...map.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, values]) => [key, [...values].sort()]));
935
1225
  }
936
- function titleFromSlug2(slug) {
937
- return slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1226
+ function titleFromSlug2(slug2) {
1227
+ return slug2.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
938
1228
  }
939
- function slug(input) {
1229
+ function slug2(input) {
940
1230
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "value";
941
1231
  }
942
1232
 
@@ -947,6 +1237,9 @@ async function buildReviewModel({ repo }) {
947
1237
  const facts = [];
948
1238
  const timeline = [];
949
1239
  const features = await readFeaturePacks(repo);
1240
+ const adrs = await listAdrs({ repo });
1241
+ for (const adr of adrs)
1242
+ addAdr(graph, adr);
950
1243
  for (const feature of features) {
951
1244
  addFeature(graph, repo, feature);
952
1245
  const sourceMap = parseIdMap(feature.idmap);
@@ -954,10 +1247,10 @@ async function buildReviewModel({ repo }) {
954
1247
  for (const fact of feature.facts) {
955
1248
  facts.push({
956
1249
  route: feature.slug,
957
- source: `${rel(repo, join6(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
1250
+ source: `${rel(repo, join7(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
958
1251
  fact
959
1252
  });
960
- addFact(graph, repo, feature, fact, sourceMap);
1253
+ addFact(graph, repo, feature, fact, sourceMap, adrs);
961
1254
  }
962
1255
  }
963
1256
  timeline.push(...await addOperationalRecords({
@@ -999,6 +1292,7 @@ async function buildReviewModel({ repo }) {
999
1292
  summary: {
1000
1293
  features: features.length,
1001
1294
  facts: facts.length,
1295
+ adrs: adrs.length,
1002
1296
  entities: nodes.filter((node) => node.kind === "entity").length,
1003
1297
  sources: nodes.filter((node) => node.kind === "source").length,
1004
1298
  handoffs: timeline.filter((item) => item.kind === "handoff").length,
@@ -1028,14 +1322,14 @@ function addFeature(graph, repo, feature) {
1028
1322
  }
1029
1323
  });
1030
1324
  }
1031
- function addFact(graph, repo, feature, fact, sourceMap) {
1325
+ function addFact(graph, repo, feature, fact, sourceMap, adrs) {
1032
1326
  const factId = `fact:${fact.id}`;
1033
1327
  addNode(graph, {
1034
1328
  id: factId,
1035
1329
  kind: "fact",
1036
1330
  label: fact.id,
1037
1331
  subtitle: `${fact.subject} ${fact.predicate} ${fact.object}`,
1038
- source: `${rel(repo, join6(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
1332
+ source: `${rel(repo, join7(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
1039
1333
  meta: {
1040
1334
  route: feature.slug,
1041
1335
  status: fact.status,
@@ -1069,7 +1363,8 @@ function addFact(graph, repo, feature, fact, sourceMap) {
1069
1363
  });
1070
1364
  for (const source of fact.src) {
1071
1365
  const resolved = sourceMap.get(source) ?? source;
1072
- const sourceId = addSource(graph, resolved, { alias: resolved === source ? undefined : source });
1366
+ const adr = adrs.find((item) => adrMatchesSource(item, resolved) || adrMatchesSource(item, source));
1367
+ const sourceId = adr ? `adr:${adr.id}` : addSource(graph, resolved, { alias: resolved === source ? undefined : source });
1073
1368
  addEdge(graph, {
1074
1369
  source: factId,
1075
1370
  target: sourceId,
@@ -1087,6 +1382,30 @@ function addFact(graph, repo, feature, fact, sourceMap) {
1087
1382
  });
1088
1383
  }
1089
1384
  }
1385
+ function addAdr(graph, adr) {
1386
+ const nodeId = `adr:${adr.id}`;
1387
+ addNode(graph, {
1388
+ id: nodeId,
1389
+ kind: "adr",
1390
+ label: adr.id,
1391
+ subtitle: adr.title,
1392
+ source: adr.path,
1393
+ meta: {
1394
+ status: adr.status,
1395
+ date: adr.date,
1396
+ supersedes: adr.supersedes,
1397
+ tags: adr.tags
1398
+ }
1399
+ });
1400
+ for (const superseded of adr.supersedes) {
1401
+ addEdge(graph, {
1402
+ source: nodeId,
1403
+ target: `adr:${superseded}`,
1404
+ kind: "supersedes",
1405
+ meta: {}
1406
+ });
1407
+ }
1408
+ }
1090
1409
  function addKgEdges(graph, feature) {
1091
1410
  for (const line of feature.graph.split(/\r?\n/)) {
1092
1411
  const trimmed = line.trim();
@@ -1175,7 +1494,7 @@ async function readJsonl(path, displayPath, warnings) {
1175
1494
  return records;
1176
1495
  }
1177
1496
  function addEntity(graph, label) {
1178
- const id = `entity:${slug2(label)}`;
1497
+ const id = `entity:${slug3(label)}`;
1179
1498
  addNode(graph, {
1180
1499
  id,
1181
1500
  kind: "entity",
@@ -1250,7 +1569,7 @@ function firstMarkdownHeading(input) {
1250
1569
  const heading = input.split(/\r?\n/).find((line) => line.startsWith("# "));
1251
1570
  return heading?.replace(/^#\s*/, "").trim() ?? "";
1252
1571
  }
1253
- function slug2(input) {
1572
+ function slug3(input) {
1254
1573
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
1255
1574
  }
1256
1575
  function stripBackticks(input) {
@@ -3599,6 +3918,10 @@ async function main(argv = process.argv.slice(2)) {
3599
3918
  print(await finalizeProject({ repo, status, summary }), json);
3600
3919
  break;
3601
3920
  }
3921
+ case "adr": {
3922
+ await handleAdrCommand(parsed, repo, json);
3923
+ break;
3924
+ }
3602
3925
  case "import": {
3603
3926
  const source = requiredString(parsed, "source", commandUsage("import"), { source: [...importSources] });
3604
3927
  const from = requiredString(parsed, "from", commandUsage("import"), { source: [...importSources] });
@@ -3671,10 +3994,14 @@ async function main(argv = process.argv.slice(2)) {
3671
3994
  function parseArgs(argv) {
3672
3995
  const [command = "help", ...rest] = argv;
3673
3996
  const flags = new Map;
3997
+ const positionals = [];
3674
3998
  for (let index = 0;index < rest.length; index++) {
3675
3999
  const arg = rest[index];
3676
- if (!arg?.startsWith("--"))
4000
+ if (!arg?.startsWith("--")) {
4001
+ if (arg)
4002
+ positionals.push(arg);
3677
4003
  continue;
4004
+ }
3678
4005
  const key = arg.slice(2);
3679
4006
  const next = rest[index + 1];
3680
4007
  if (next && !next.startsWith("--")) {
@@ -3684,7 +4011,7 @@ function parseArgs(argv) {
3684
4011
  flags.set(key, true);
3685
4012
  }
3686
4013
  }
3687
- return { command, flags };
4014
+ return { command, positionals, flags };
3688
4015
  }
3689
4016
  function requiredString(parsed, key, usageValue, options) {
3690
4017
  const value = parsed.flags.get(key);
@@ -3719,6 +4046,14 @@ function optionalChoice(parsed, key, choices, fallback, usageValue) {
3719
4046
  }
3720
4047
  return value;
3721
4048
  }
4049
+ function optionalList(parsed, key, usageValue) {
4050
+ const value = parsed.flags.get(key);
4051
+ if (value === undefined)
4052
+ return [];
4053
+ if (typeof value !== "string")
4054
+ throw new CliArgumentError(`--${key} requires a comma-separated value`, { usage: usageValue });
4055
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
4056
+ }
3722
4057
  function parseAgentTargets(value) {
3723
4058
  if (value === undefined)
3724
4059
  return;
@@ -3759,6 +4094,36 @@ function print(value, json, message) {
3759
4094
  }
3760
4095
  console.log(JSON.stringify(value, null, 2));
3761
4096
  }
4097
+ async function handleAdrCommand(parsed, repo, json) {
4098
+ const action = parsed.positionals[0];
4099
+ if (action === "new") {
4100
+ const title = requiredString(parsed, "title", commandUsage("adr new"));
4101
+ const status = optionalChoice(parsed, "status", adrStatuses, "active", commandUsage("adr new"));
4102
+ const adr = await createAdr({
4103
+ repo,
4104
+ title,
4105
+ status,
4106
+ supersedes: optionalList(parsed, "supersedes", commandUsage("adr new")),
4107
+ tags: optionalList(parsed, "tags", commandUsage("adr new"))
4108
+ });
4109
+ print(adr, json, `Created ${adr.id} at ${adr.path}.`);
4110
+ return;
4111
+ }
4112
+ if (action === "list") {
4113
+ const adrs = await listAdrs({ repo });
4114
+ print({ adrs }, json, formatAdrList(adrs));
4115
+ return;
4116
+ }
4117
+ throw new CliArgumentError(action ? `Unknown ADR action: ${action}` : "Missing ADR action", {
4118
+ usage: commandUsage("adr")
4119
+ });
4120
+ }
4121
+ function formatAdrList(adrs) {
4122
+ if (adrs.length === 0)
4123
+ return "No ADRs found.";
4124
+ return adrs.map((adr) => `${adr.id} ${adr.status} ${adr.title} (${adr.path})`).join(`
4125
+ `);
4126
+ }
3762
4127
  function formatInitMessage(result) {
3763
4128
  if (!result.dryRun) {
3764
4129
  const lines2 = [`Barry Cache init ${result.changed ? "changed files" : "already up to date"}.`];
@@ -3827,6 +4192,9 @@ function errorMessage(error) {
3827
4192
  function commandUsage(command) {
3828
4193
  const usages = {
3829
4194
  init: "barry-cache init [--yes] [--dry-run] [--agents all|none|codex,cursor,copilot,claude,gemini,llms]",
4195
+ adr: "barry-cache adr <new|list> [--json]",
4196
+ "adr new": 'barry-cache adr new --title "..." [--status active|superseded|deprecated] [--supersedes ADR-0001] [--tags context,agents] [--json]',
4197
+ "adr list": "barry-cache adr list [--json]",
3830
4198
  route: 'barry-cache route --task "..." [--json]',
3831
4199
  search: 'barry-cache search --query "..." [--json]',
3832
4200
  load: 'barry-cache load --route "..." [--json]',
@@ -3853,6 +4221,8 @@ Usage:
3853
4221
  barry-cache load --route "..." [--json]
3854
4222
  barry-cache resume --task "..." [--json]
3855
4223
  barry-cache finalize --summary "..." [--status success] [--json]
4224
+ barry-cache adr new --title "..." [--status active] [--tags context,agents]
4225
+ barry-cache adr list [--json]
3856
4226
  barry-cache import --source pulpcut-kb --from /path/to/repo [--dry-run] [--json]
3857
4227
  barry-cache review [--port 8787] [--open]
3858
4228
  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.0",
4
4
  "description": "Barry Cache remembers your repo.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
+ "assets/barry-cache.png",
18
19
  "README.md"
19
20
  ],
20
21
  "engines": {
@@ -25,4 +26,4 @@
25
26
  "@types/node": "^25.8.0",
26
27
  "typescript": "^6.0.3"
27
28
  }
28
- }
29
+ }