barry-cache 0.3.2 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +175 -62
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,7 +12,7 @@ It creates source-backed context files for coding agents, validates them, and gi
12
12
 
13
13
  Barry Cache exists because coding agents need durable project context that is shared, reviewable, and smaller than the whole repository. Private assistant memory, ad hoc chat history, and vendor-specific instruction files drift apart; Barry keeps the source of truth in the repo and lets every agent load the same facts.
14
14
 
15
- The structure is intentionally layered: canonical context lives in `docs/context/`, operational continuity lives in `.context-state/`, and generated retrieval data lives in `.context-cache/`. Canonical facts stay source-backed and validated, while `route`, `search`, `load`, and `resume` project only the relevant feature pack into an agent session.
15
+ The structure is intentionally layered: canonical context lives in `docs/context/`, operational continuity lives in `.context-state/`, and generated retrieval data lives in `.context-cache/`. Canonical facts stay source-backed and validated, while `route`, `search`, `load`, and `resume` project only the relevant feature pack into an agent session. Repeated retrieval commands reuse `.context-cache/context-index.json` when the source context file manifest is unchanged.
16
16
 
17
17
  That gives Barry three core advantages: it is agent-agnostic, because adapters point to one canonical context; auditable, because facts carry stable IDs and source references; and context-efficient, because agents start from a small routed slice instead of rereading the whole codebase.
18
18
 
@@ -297,6 +297,8 @@ bun run barry -- load --route editor-media-runtime
297
297
 
298
298
  This keeps the agent from reading every context file in the repo.
299
299
 
300
+ Barry also keeps a disposable parsed context index in `.context-cache/context-index.json`. It is keyed by the source context files and directories, so repeated `resume`, `load`, `route`, `search`, and `review --json` commands reuse parsed context until a context source file changes.
301
+
300
302
  If an agent ignores the repo instructions, prompt it explicitly:
301
303
 
302
304
  ```text
package/dist/cli.js CHANGED
@@ -263,7 +263,11 @@ function unquote(input) {
263
263
 
264
264
  // src/core/context.ts
265
265
  import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
266
- import { basename as basename2, join as join4 } from "node:path";
266
+ import { basename as basename2, join as join5 } from "node:path";
267
+
268
+ // src/core/context-cache.ts
269
+ import { stat as stat2 } from "node:fs/promises";
270
+ import { join as join4 } from "node:path";
267
271
 
268
272
  // src/core/validate.ts
269
273
  import { join as join3 } from "node:path";
@@ -338,17 +342,155 @@ function validateFact(value) {
338
342
  return null;
339
343
  }
340
344
 
345
+ // src/core/context-cache.ts
346
+ var cacheVersion = 1;
347
+ var contextCachePath = ".context-cache/context-index.json";
348
+ async function readContextSnapshot(repo) {
349
+ const cached = await readFreshContextCache(repo);
350
+ if (cached)
351
+ return hydrateSnapshot(repo, cached);
352
+ const snapshot = await readContextSnapshotFromDisk(repo);
353
+ await writeContextCache(repo, snapshot);
354
+ return snapshot;
355
+ }
356
+ async function readFreshContextCache(repo) {
357
+ try {
358
+ const parsed = JSON.parse(await readTextIfExists(repoPath(repo, contextCachePath)));
359
+ if (!isContextCacheFile(parsed))
360
+ return null;
361
+ return await manifestIsFresh(repo, parsed.manifest) ? parsed : null;
362
+ } catch {
363
+ return null;
364
+ }
365
+ }
366
+ async function writeContextCache(repo, snapshot) {
367
+ const cache = {
368
+ version: cacheVersion,
369
+ generated_at: new Date().toISOString(),
370
+ manifest: await buildManifest(repo, snapshot),
371
+ features: snapshot.features.map((feature) => ({
372
+ ...feature,
373
+ dir: rel(repo, feature.dir)
374
+ })),
375
+ adrs: snapshot.adrs
376
+ };
377
+ try {
378
+ await writeText(repoPath(repo, contextCachePath), `${JSON.stringify(cache, null, 2)}
379
+ `);
380
+ } catch {}
381
+ }
382
+ async function readContextSnapshotFromDisk(repo) {
383
+ return {
384
+ features: await readFeaturePacksFromDisk(repo),
385
+ adrs: (await readAdrCatalog(repo)).adrs
386
+ };
387
+ }
388
+ async function readFeaturePacksFromDisk(repo) {
389
+ const root = repoPath(repo, "docs/context/features");
390
+ const slugs = await listDirs(root);
391
+ const features = [];
392
+ for (const slug2 of slugs) {
393
+ const dir = join4(root, slug2);
394
+ features.push({
395
+ slug: slug2,
396
+ dir,
397
+ readme: await readTextIfExists(join4(dir, "README.md")),
398
+ idmap: await readTextIfExists(join4(dir, "IDMAP.md")),
399
+ graph: await readTextIfExists(join4(dir, "KG.adj")),
400
+ facts: await readFacts(join4(dir, "FACTS.jsonl"))
401
+ });
402
+ }
403
+ return features;
404
+ }
405
+ async function readFacts(path) {
406
+ const rows = (await readTextIfExists(path)).split(/\r?\n/);
407
+ const facts = [];
408
+ for (const row of rows) {
409
+ if (row.trim().length === 0)
410
+ continue;
411
+ const parsed = JSON.parse(row);
412
+ if (validateFact(parsed) === null)
413
+ facts.push(parsed);
414
+ }
415
+ return facts;
416
+ }
417
+ async function buildManifest(repo, snapshot) {
418
+ const paths = new Map;
419
+ paths.set("docs/context/features", "dir");
420
+ paths.set("docs/context/adrs", "dir");
421
+ for (const feature of snapshot.features) {
422
+ const dir = rel(repo, feature.dir);
423
+ paths.set(dir, "dir");
424
+ paths.set(`${dir}/README.md`, "file");
425
+ paths.set(`${dir}/IDMAP.md`, "file");
426
+ paths.set(`${dir}/KG.adj`, "file");
427
+ paths.set(`${dir}/FACTS.jsonl`, "file");
428
+ }
429
+ for (const adr of snapshot.adrs) {
430
+ paths.set(adr.path, "file");
431
+ }
432
+ const entries = [...paths.entries()].sort((a, b) => a[0].localeCompare(b[0]));
433
+ return await Promise.all(entries.map(([path, kind]) => statManifestEntry(repo, path, kind)));
434
+ }
435
+ async function manifestIsFresh(repo, manifest) {
436
+ for (const entry of manifest) {
437
+ const current = await statManifestEntry(repo, entry.path, entry.kind);
438
+ if (!sameManifestEntry(entry, current))
439
+ return false;
440
+ }
441
+ return true;
442
+ }
443
+ async function statManifestEntry(repo, path, kind) {
444
+ try {
445
+ const value = await stat2(repoPath(repo, path));
446
+ const matchesKind = kind === "dir" ? value.isDirectory() : value.isFile();
447
+ if (!matchesKind)
448
+ return { path, kind, exists: false };
449
+ return {
450
+ path,
451
+ kind,
452
+ exists: true,
453
+ mtimeMs: value.mtimeMs,
454
+ size: value.size
455
+ };
456
+ } catch (error) {
457
+ if (error.code === "ENOENT")
458
+ return { path, kind, exists: false };
459
+ throw error;
460
+ }
461
+ }
462
+ function sameManifestEntry(left, right) {
463
+ if (left.path !== right.path || left.kind !== right.kind || left.exists !== right.exists)
464
+ return false;
465
+ if (!left.exists || !right.exists)
466
+ return true;
467
+ return left.mtimeMs === right.mtimeMs && left.size === right.size;
468
+ }
469
+ function hydrateSnapshot(repo, cache) {
470
+ return {
471
+ features: cache.features.map((feature) => ({
472
+ ...feature,
473
+ dir: repoPath(repo, feature.dir)
474
+ })),
475
+ adrs: cache.adrs
476
+ };
477
+ }
478
+ function isContextCacheFile(value) {
479
+ if (typeof value !== "object" || value === null)
480
+ return false;
481
+ const candidate = value;
482
+ return candidate.version === cacheVersion && Array.isArray(candidate.manifest) && Array.isArray(candidate.features) && Array.isArray(candidate.adrs);
483
+ }
484
+
341
485
  // src/core/context.ts
342
486
  async function routeTask({ repo, task }) {
343
- const features = await readFeaturePacks(repo);
344
- const adrs = await listAdrs({ repo });
487
+ const { features, adrs } = await readContextSnapshot(repo);
345
488
  const taskTokens = tokens(task);
346
489
  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));
347
490
  return { task, routes };
348
491
  }
349
492
  async function searchContext({ repo, query }) {
350
- const features = await readFeaturePacks(repo);
351
- const adrs = await listAdrs({ repo });
493
+ const { features, adrs } = await readContextSnapshot(repo);
352
494
  const queryTokens = tokens(query);
353
495
  const results = [];
354
496
  for (const feature of features) {
@@ -374,7 +516,7 @@ async function searchContext({ repo, query }) {
374
516
  route: feature.slug,
375
517
  score,
376
518
  text: factText,
377
- source: `${rel(repo, join4(feature.dir, "FACTS.jsonl"))}#${fact.id}`
519
+ source: `${rel(repo, join5(feature.dir, "FACTS.jsonl"))}#${fact.id}`
378
520
  });
379
521
  }
380
522
  }
@@ -396,8 +538,7 @@ async function searchContext({ repo, query }) {
396
538
  return { query, results };
397
539
  }
398
540
  async function loadContext({ repo, route }) {
399
- const features = await readFeaturePacks(repo);
400
- const adrs = await listAdrs({ repo });
541
+ const { features, adrs } = await readContextSnapshot(repo);
401
542
  const feature = features.find((item) => item.slug === route) ?? null;
402
543
  if (!feature)
403
544
  return { feature: null, facts: [], sources: [], adrs: [] };
@@ -405,10 +546,10 @@ async function loadContext({ repo, route }) {
405
546
  feature,
406
547
  facts: feature.facts,
407
548
  sources: [
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"))
549
+ rel(repo, join5(feature.dir, "README.md")),
550
+ rel(repo, join5(feature.dir, "IDMAP.md")),
551
+ rel(repo, join5(feature.dir, "KG.adj")),
552
+ rel(repo, join5(feature.dir, "FACTS.jsonl"))
412
553
  ],
413
554
  adrs: linkedAdrsForSources(feature.facts.flatMap((fact) => fact.src), adrs)
414
555
  };
@@ -432,7 +573,7 @@ async function resumeProject({ repo, task }) {
432
573
  async function finalizeProject(options) {
433
574
  const dir = repoPath(options.repo, ".context-state/handoffs");
434
575
  await mkdir2(dir, { recursive: true });
435
- const path = join4(dir, "handoffs.jsonl");
576
+ const path = join5(dir, "handoffs.jsonl");
436
577
  const record = {
437
578
  id: `handoff-${new Date().toISOString()}`,
438
579
  updated_at: new Date().toISOString(),
@@ -445,35 +586,6 @@ async function finalizeProject(options) {
445
586
  `);
446
587
  return { saved: true, path: rel(options.repo, path), summary: options.summary };
447
588
  }
448
- async function readFeaturePacks(repo) {
449
- const root = repoPath(repo, "docs/context/features");
450
- const slugs = await listDirs(root);
451
- const features = [];
452
- for (const slug2 of slugs) {
453
- const dir = join4(root, slug2);
454
- features.push({
455
- slug: slug2,
456
- dir,
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"))
461
- });
462
- }
463
- return features;
464
- }
465
- async function readFacts(path) {
466
- const rows = (await readTextIfExists(path)).split(/\r?\n/);
467
- const facts = [];
468
- for (const row of rows) {
469
- if (row.trim().length === 0)
470
- continue;
471
- const parsed = JSON.parse(row);
472
- if (validateFact(parsed) === null)
473
- facts.push(parsed);
474
- }
475
- return facts;
476
- }
477
589
  function scoreFeature(feature, taskTokens, adrs) {
478
590
  const linkedAdrs = linkedAdrsForSources(feature.facts.flatMap((fact) => fact.src), adrs);
479
591
  const text = [
@@ -516,7 +628,7 @@ function firstLine(value) {
516
628
  }
517
629
 
518
630
  // src/core/import-pulpcut.ts
519
- import { join as join5 } from "node:path";
631
+ import { join as join6 } from "node:path";
520
632
  async function importPulpcutKb(options) {
521
633
  const dryRun = options.dryRun ?? false;
522
634
  const result = {
@@ -531,19 +643,19 @@ async function importPulpcutKb(options) {
531
643
  warnings: []
532
644
  };
533
645
  const sourceDocs = repoPath(options.from, "docs");
534
- const kbIndex = await readTextIfExists(join5(sourceDocs, "KB_INDEX.md"));
646
+ const kbIndex = await readTextIfExists(join6(sourceDocs, "KB_INDEX.md"));
535
647
  if (kbIndex.trim().length === 0) {
536
- throw new Error(`PulpCut KB index not found at ${join5(sourceDocs, "KB_INDEX.md")}`);
648
+ throw new Error(`PulpCut KB index not found at ${join6(sourceDocs, "KB_INDEX.md")}`);
537
649
  }
538
650
  const routes = parseKbIndex(kbIndex);
539
651
  const slugs = await listDirs(sourceDocs);
540
652
  for (const slug2 of slugs) {
541
- const sourceDir = join5(sourceDocs, slug2);
542
- const rawFacts = await readTextIfExists(join5(sourceDir, "FACTS.jsonl"));
653
+ const sourceDir = join6(sourceDocs, slug2);
654
+ const rawFacts = await readTextIfExists(join6(sourceDir, "FACTS.jsonl"));
543
655
  if (rawFacts.trim().length === 0)
544
656
  continue;
545
- const idmap = await readTextIfExists(join5(sourceDir, "IDMAP.md"));
546
- const graph = await readTextIfExists(join5(sourceDir, "KG.adj"));
657
+ const idmap = await readTextIfExists(join6(sourceDir, "IDMAP.md"));
658
+ const graph = await readTextIfExists(join6(sourceDir, "KG.adj"));
547
659
  if (idmap.trim().length === 0)
548
660
  result.warnings.push(`${slug2}: missing IDMAP.md`);
549
661
  if (graph.trim().length === 0)
@@ -728,7 +840,7 @@ function stringOrStringArray(value) {
728
840
 
729
841
  // src/core/init.ts
730
842
  import { mkdir as mkdir3 } from "node:fs/promises";
731
- import { join as join6 } from "node:path";
843
+ import { join as join7 } from "node:path";
732
844
 
733
845
  // src/core/templates.ts
734
846
  var managedStart = "<!-- barry-cache:start -->";
@@ -761,6 +873,8 @@ Barry Cache remembers this repo through source-backed context files.
761
873
 
762
874
  ${commandNote}
763
875
 
876
+ Repeated retrieval commands use the disposable parsed index in \`.context-cache/context-index.json\` when source context files have not changed.
877
+
764
878
  Start task context with:
765
879
 
766
880
  \`\`\`bash
@@ -855,7 +969,7 @@ Barry Cache keeps repo context source-backed, validated, and easy for agents to
855
969
 
856
970
  This directory is the canonical project memory for Barry Cache. It keeps durable implementation context in Git so humans and agents can review the same source-backed facts instead of relying on private assistant memory or stale chat history.
857
971
 
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.
972
+ Barry separates three concerns: \`docs/context/\` is reviewed truth, \`.context-state/\` is operational session continuity, and \`.context-cache/\` is disposable retrieval data. Barry stores a parsed context index in \`.context-cache/context-index.json\` and reuses it while the source context manifest is unchanged. Use this structure to explain existing behavior, route tasks, validate facts, and resume agent work without loading the whole repo.
859
973
  `;
860
974
  var adrReadmeMd = `# Architecture Decision Records
861
975
 
@@ -876,7 +990,7 @@ Barry Cache separates canonical context, operational state, and generated caches
876
990
 
877
991
  - Canonical context lives in \`docs/context/\`.
878
992
  - Operational continuity lives in \`.context-state/\`.
879
- - Generated retrieval data lives in \`.context-cache/\`.
993
+ - Generated retrieval data lives in \`.context-cache/\`, including the disposable parsed context index.
880
994
  `;
881
995
  var factSchema = {
882
996
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -975,11 +1089,11 @@ async function initProject(options) {
975
1089
  await patchAgentInstructions(repo, dryRun, result, options.agents, commandPrefix, packageManager);
976
1090
  await patchGitignore(repo, dryRun, result);
977
1091
  if (!dryRun) {
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 });
1092
+ await mkdir3(join7(repo, ".context-state/work/threads"), { recursive: true });
1093
+ await mkdir3(join7(repo, ".context-state/handoffs"), { recursive: true });
1094
+ await mkdir3(join7(repo, ".context-state/failures"), { recursive: true });
1095
+ await mkdir3(join7(repo, ".context-state/strategies"), { recursive: true });
1096
+ await mkdir3(join7(repo, ".context-cache"), { recursive: true });
983
1097
  }
984
1098
  result.changed = result.written.length > 0 || result.updated.length > 0;
985
1099
  return result;
@@ -1102,7 +1216,7 @@ function llmsTxt() {
1102
1216
  }
1103
1217
 
1104
1218
  // src/core/review-model.ts
1105
- import { join as join7 } from "node:path";
1219
+ import { join as join8 } from "node:path";
1106
1220
 
1107
1221
  // src/core/review-tree.ts
1108
1222
  function buildReviewTree(facts) {
@@ -1254,8 +1368,7 @@ async function buildReviewModel({ repo }) {
1254
1368
  const warnings = [];
1255
1369
  const facts = [];
1256
1370
  const timeline = [];
1257
- const features = await readFeaturePacks(repo);
1258
- const adrs = await listAdrs({ repo });
1371
+ const { features, adrs } = await readContextSnapshot(repo);
1259
1372
  for (const adr of adrs)
1260
1373
  addAdr(graph, adr);
1261
1374
  for (const feature of features) {
@@ -1265,7 +1378,7 @@ async function buildReviewModel({ repo }) {
1265
1378
  for (const fact of feature.facts) {
1266
1379
  facts.push({
1267
1380
  route: feature.slug,
1268
- source: `${rel(repo, join7(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
1381
+ source: `${rel(repo, join8(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
1269
1382
  fact
1270
1383
  });
1271
1384
  addFact(graph, repo, feature, fact, sourceMap, adrs);
@@ -1347,7 +1460,7 @@ function addFact(graph, repo, feature, fact, sourceMap, adrs) {
1347
1460
  kind: "fact",
1348
1461
  label: fact.id,
1349
1462
  subtitle: `${fact.subject} ${fact.predicate} ${fact.object}`,
1350
- source: `${rel(repo, join7(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
1463
+ source: `${rel(repo, join8(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
1351
1464
  meta: {
1352
1465
  route: feature.slug,
1353
1466
  status: fact.status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "barry-cache",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Barry Cache remembers your repo.",
5
5
  "type": "module",
6
6
  "bin": {