@sabaiway/agent-workflow-memory 3.0.0 → 3.0.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to the memory substrate. Versions are this **package's** npm
4
4
  they are distinct from the **deployment-lineage** stamp written into a project's
5
5
  `docs/ai/.memory-version` (which tracks the shared `agent-workflow` lineage, head `3.0.0`).
6
6
 
7
+ ## 3.0.1 — bundled reference scripts refreshed (no behavior change)
8
+
9
+ PATCH rider on the kit 3.1.0 release — the publish workflow's changed-but-unbumped tooth caught
10
+ that two bundled reference scripts had moved since 3.0.0 without a version bump. No CLI surface,
11
+ output, or exit-code change:
12
+
13
+ - `references/scripts/check-docs-size.mjs` — reworked to an in-process CLI shape (pure argv
14
+ parser, injectable log, exported `runCli`) so suites drive it without a spawn; same flags, same
15
+ behavior; a colocated CLI test pins it.
16
+ - `references/scripts/archive-decisions.mjs` — comment-only cleanup (reviewer-round identity
17
+ references removed as part of the family-wide neutral-review-ID sweep).
18
+
7
19
  ## 3.0.0 — strip-the-kit: the substrate follows the hardened computed core (AD-059)
8
20
 
9
21
  > ### ⚠ BREAKING — retired loop surfaces + Node >= 22
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-memory
3
3
  description: Deploy or upgrade a portable AI-agent memory substrate in any project — an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) and a structured `docs/ai/` context store with cap/archive/index enforcement. Use when the user wants to bootstrap `docs/ai/`, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-memory` / `/agent-workflow-memory upgrade`. Triggers on "set up the memory system", "deploy the AI memory here", "bootstrap docs/ai", "upgrade the memory substrate". This is the substrate only — the workflow methodology (plan→execute→review, queue, Cleanup) is owned elsewhere and injected into AGENTS.md by the family composition root.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '3.0.0'
6
+ version: '3.0.1'
7
7
  ---
8
8
 
9
9
  # agent-workflow-memory
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-memory",
5
5
  "kind": "memory-substrate",
6
- "version": "3.0.0",
6
+ "version": "3.0.1",
7
7
  "provides": ["context"],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-memory",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Portable, cross-agent memory substrate for AI coding agents — an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement, deployable standalone or as part of the agent-workflow family. The memory layer of the agent-workflow family.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -649,7 +649,7 @@ const runMigrate = (root, flags, today, deps, log, logError) => {
649
649
  verifyConservation(oldItems, newItems);
650
650
  // Integrity over the FULL final store — existing records ∪ freshly-exploded — not just the new
651
651
  // writes: a pre-existing adr/ record whose id also stays RETAINED in HOT would otherwise leave the
652
- // ADR in two places (codex R4). Same full-store pattern as runRotate.
652
+ // ADR in two places. Same full-store pattern as runRotate.
653
653
  const finalStoreById = new Map(existingStore.map((e) => [e.id, { id: e.id, idNum: e.idNum, fileName: e.fileName }]));
654
654
  for (const r of records) finalStoreById.set(r.id, { id: r.id, idNum: r.idNum, fileName: r.fileName });
655
655
  assertStoreIntegrity(retained, [...finalStoreById.values()]);
@@ -783,7 +783,7 @@ const runRotate = (root, flags, today, deps, log, logError) => {
783
783
  }
784
784
 
785
785
  const records = explode(toExplode, today);
786
- // Crash-resume (fold-induced, codex R2): a record from a prior crashed rotate may already be on
786
+ // Crash-resume (fold-induced): a record from a prior crashed rotate may already be on
787
787
  // disk. A byte-identical one is done (deduped, not a duplicate-id error); a divergent-body one is
788
788
  // corrupt → FAIL. The FINAL store = existing ∪ freshly-exploded, deduped by id.
789
789
  const existingById = new Map(existingStore.map((e) => [e.id, e]));
@@ -341,7 +341,7 @@ describe('1.3 --migrate --apply — records + snapshot + retire monoliths + HOT
341
341
  assert.equal(run(['--check', '--today=2026-07-09'], root).code, 0, 'the resumed tree passes --check');
342
342
  });
343
343
 
344
- it('refuses when a pre-existing adr/ record duplicates an ADR that stays in HOT (never two places — codex R4)', () => {
344
+ it('refuses when a pre-existing adr/ record duplicates an ADR that stays in HOT (never two places — review-adr-archive-r04-major-01)', () => {
345
345
  const root = makeRoot();
346
346
  seedLegacy(root, { hot: ['005', '006'], warm: ['003'], cold: ['001'] });
347
347
  mkdirSync(join(root, ADR_DIR_REL), { recursive: true });
@@ -452,7 +452,7 @@ describe('1.4 --check', () => {
452
452
  assert.match(errText, /duplicate ADR id AD-002/);
453
453
  });
454
454
 
455
- it('a decisions.md with NO maxLines cap fails loud (never operates against an unknown budget — codex R4)', () => {
455
+ it('a decisions.md with NO maxLines cap fails loud (never operates against an unknown budget — review-adr-archive-r04-major-02)', () => {
456
456
  const root = makeRoot();
457
457
  mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
458
458
  const noCapFm = '---\ntype: reference\nlastUpdated: 2026-01-01\nscope: permanent\nstaleAfter: never\nowner: none\n---\n';
@@ -514,7 +514,7 @@ describe('1.4 --check', () => {
514
514
  assert.match(errText, /two records for AD-001/);
515
515
  });
516
516
 
517
- it('a NESTED subdirectory in adr/ fails loud (the store is a flat directory — codex R2)', () => {
517
+ it('a NESTED subdirectory in adr/ fails loud (the store is a flat directory — review-adr-archive-r02-major-01)', () => {
518
518
  const root = makeRoot();
519
519
  seedMigrated(root, { hotIds: ['005'], storeIds: ['001'] });
520
520
  mkdirSync(join(root, ADR_DIR_REL, 'nested'), { recursive: true });
@@ -561,7 +561,7 @@ describe('1.4 default rotate — explode the oldest beyond cap + regenerate the
561
561
  assert.equal(regenCalls.length, 0, 'a no-op never regenerates the index');
562
562
  });
563
563
 
564
- it('is crash-resumable: a byte-identical record from a crashed prior rotate is deduped, not a fatal duplicate (codex R2)', () => {
564
+ it('is crash-resumable: a byte-identical record from a crashed prior rotate is deduped, not a fatal duplicate (review-adr-archive-r02-major-02)', () => {
565
565
  const root = makeRoot();
566
566
  mkdirSync(join(root, ADR_DIR_REL), { recursive: true });
567
567
  const blocks = ['005', '006', '007', '008'].map((id) => adrBlock(id));
@@ -579,7 +579,7 @@ describe('1.4 default rotate — explode the oldest beyond cap + regenerate the
579
579
  assert.equal(run(['--check', '--today=2026-07-09'], root).code, 0);
580
580
  });
581
581
 
582
- it('a no-op rotate still REFUSES a corrupt store (partition) — never a silent green no-op (codex R2)', () => {
582
+ it('a no-op rotate still REFUSES a corrupt store (partition) — never a silent green no-op (review-adr-archive-r02-major-03)', () => {
583
583
  const root = makeRoot();
584
584
  seedMigrated(root, { hotIds: ['005'], storeIds: ['001'] }); // under cap
585
585
  const rec = explode(parseDecisionsText(tierText(999, '# T', [adrBlock('010')]), 'x').entries, '2026-07-09')[0];
@@ -0,0 +1,41 @@
1
+ // check-docs-size-cli.test.mjs — runCli branch pins the subprocess smokes cannot reach
2
+ // in-process (Phase-5 coverage fill; the main spec file is parity-frozen, so these ride a
3
+ // colocated file): the unknown-argument refusal and the written-empty-index guard.
4
+ import { describe, it } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { runCli } from './check-docs-size.mjs';
10
+
11
+ const cli = async (argv) => {
12
+ const { code, stdout, stderr } = await runCli(argv);
13
+ return { code, stdout, stderr };
14
+ };
15
+
16
+ describe('check-docs-size runCli — refusal branches', () => {
17
+ it('an unknown argument exits 2 naming it', async () => {
18
+ const { code, stderr } = await cli(['--bogus']);
19
+ assert.equal(code, 2);
20
+ assert.match(stderr, /Unknown argument: --bogus/);
21
+ });
22
+
23
+ it('--write-index landing on a sink path (index stat size 0) is the loud written-empty refusal', async () => {
24
+ const root = mkdtempSync(join(tmpdir(), 'cds-cli-'));
25
+ try {
26
+ mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
27
+ writeFileSync(
28
+ join(root, 'docs', 'ai', 'a.md'),
29
+ '---\ntype: state\nlastUpdated: 2026-07-18\nscope: session\nstaleAfter: never\nowner: none\nmaxLines: 10\n---\n\n# a\n',
30
+ );
31
+ // The index path is a symlink into /dev/null: the write lands, the stat reads size 0 —
32
+ // the guard must refuse loudly instead of reporting a written index.
33
+ symlinkSync('/dev/null', join(root, 'docs', 'ai', 'index.md'));
34
+ const { code, stderr } = await cli(['--write-index', `--root=${root}`]);
35
+ assert.equal(code, 2);
36
+ assert.match(stderr, /index\.md was written empty/);
37
+ } finally {
38
+ rmSync(root, { recursive: true, force: true });
39
+ }
40
+ });
41
+ });
@@ -103,25 +103,19 @@ export const discoverMeta = async (root = ROOT) => {
103
103
  return { projectName, hierarchicalLinks, onDemandLinks };
104
104
  };
105
105
 
106
+ // Pure argv parser (no I/O, no exit): `help` / `error` ride out as data for runCli to render.
106
107
  const parseArgs = (argv) => {
107
108
  const flags = { report: false, writeIndex: false, checkIndex: false, quiet: false };
108
109
  const opts = { today: null, root: null };
109
- for (const arg of argv.slice(2)) {
110
+ for (const arg of argv) {
110
111
  if (arg === '--report') flags.report = true;
111
112
  else if (arg === '--write-index') flags.writeIndex = true;
112
113
  else if (arg === '--check-index') flags.checkIndex = true;
113
114
  else if (arg === '--quiet') flags.quiet = true;
114
115
  else if (arg.startsWith('--today=')) opts.today = arg.slice('--today='.length);
115
116
  else if (arg.startsWith('--root=')) opts.root = arg.slice('--root='.length);
116
- else if (arg === '--help' || arg === '-h') {
117
- console.log(
118
- 'Usage: check-docs-size.mjs [--report|--write-index|--check-index] [--today=YYYY-MM-DD] [--root=<dir>] [--quiet]',
119
- );
120
- process.exit(0);
121
- } else {
122
- console.error(`Unknown argument: ${arg}`);
123
- process.exit(2);
124
- }
117
+ else if (arg === '--help' || arg === '-h') return { flags, opts, help: true };
118
+ else return { flags, opts, error: `Unknown argument: ${arg}` };
125
119
  }
126
120
  return { flags, opts };
127
121
  };
@@ -218,7 +212,7 @@ const formatRow = (row) => {
218
212
  return { status, sizeCell, ...row };
219
213
  };
220
214
 
221
- const printReport = (rows, quiet) => {
215
+ const printReport = (rows, quiet, log = console.log) => {
222
216
  const widths = {
223
217
  status: 2,
224
218
  path: Math.max(4, ...rows.map((r) => r.path.length)),
@@ -228,15 +222,15 @@ const printReport = (rows, quiet) => {
228
222
  };
229
223
  const printable = quiet ? rows.filter((r) => r.errors.length || r.warnings.length) : rows;
230
224
  if (printable.length > 0) {
231
- console.log(
225
+ log(
232
226
  `${'S'.padEnd(widths.status)} ${'PATH'.padEnd(widths.path)} ${'SIZE/MAX'.padEnd(widths.size)} ${'TYPE'.padEnd(widths.type)} ${'UPDATED'.padEnd(widths.updated)}`,
233
227
  );
234
228
  for (const row of printable) {
235
- console.log(
229
+ log(
236
230
  `${row.status.padEnd(widths.status)} ${row.path.padEnd(widths.path)} ${row.sizeCell.padEnd(widths.size)} ${(row.frontmatter?.type ?? '').padEnd(widths.type)} ${(row.frontmatter?.lastUpdated ?? '').padEnd(widths.updated)}`,
237
231
  );
238
- for (const err of row.errors) console.log(` - ERROR ${err}`);
239
- for (const warn of row.warnings) console.log(` - WARN ${warn}`);
232
+ for (const err of row.errors) log(` - ERROR ${err}`);
233
+ for (const warn of row.warnings) log(` - WARN ${warn}`);
240
234
  }
241
235
  }
242
236
  };
@@ -361,9 +355,29 @@ export const regenerateIndex = async (root, todayStr = null) => {
361
355
  return { indexPath, files: rows.length };
362
356
  };
363
357
 
364
- const main = async () => {
365
- const { flags, opts } = parseArgs(process.argv);
366
- const { root, docsDir, indexPath } = pathsFor(opts.root ? resolve(opts.root) : ROOT);
358
+ // The return-code entry point (no process.argv / process.exit / console inside): argv[]
359
+ // { code, stdout, stderr }. The thin shell at the bottom is the only process-coupled code.
360
+ export const runCli = async (argv, deps = {}) => {
361
+ const stdoutLines = [];
362
+ const stderrLines = [];
363
+ const log = (line) => stdoutLines.push(line);
364
+ const logError = (line) => stderrLines.push(line);
365
+ const result = (code) => ({
366
+ code,
367
+ stdout: stdoutLines.length > 0 ? `${stdoutLines.join('\n')}\n` : '',
368
+ stderr: stderrLines.length > 0 ? `${stderrLines.join('\n')}\n` : '',
369
+ });
370
+
371
+ const { flags, opts, help, error } = parseArgs(argv);
372
+ if (help) {
373
+ log('Usage: check-docs-size.mjs [--report|--write-index|--check-index] [--today=YYYY-MM-DD] [--root=<dir>] [--quiet]');
374
+ return result(0);
375
+ }
376
+ if (error) {
377
+ logError(error);
378
+ return result(2);
379
+ }
380
+ const { root, docsDir, indexPath } = pathsFor(opts.root ? resolve(opts.root) : (deps.root ?? ROOT));
367
381
  const today = computeToday(opts.today);
368
382
  const files = (await walkMarkdownFiles(docsDir)).sort();
369
383
  const inspected = await Promise.all(files.map((f) => inspectFile(f, today, root)));
@@ -373,11 +387,11 @@ const main = async () => {
373
387
 
374
388
  if (flags.writeIndex) {
375
389
  await writeIndex(rows, today, meta, indexPath);
376
- console.log(`Wrote ${relative(root, indexPath)}`);
390
+ log(`Wrote ${relative(root, indexPath)}`);
377
391
  const after = await stat(indexPath);
378
392
  if (after.size === 0) {
379
- console.error('index.md was written empty');
380
- process.exit(2);
393
+ logError('index.md was written empty');
394
+ return result(2);
381
395
  }
382
396
  }
383
397
 
@@ -385,28 +399,31 @@ const main = async () => {
385
399
  const onDisk = existsSync(indexPath) ? await readFile(indexPath, 'utf8') : null;
386
400
  const { fresh } = checkIndexFreshness(rows, onDisk, meta);
387
401
  if (!fresh) {
388
- console.error(
402
+ logError(
389
403
  `[check-docs-size] FAIL: ${relative(root, indexPath)} is stale (out of sync with source frontmatter). Regenerate the index (--write-index) and commit the regenerated file.`,
390
404
  );
391
- process.exit(1);
405
+ return result(1);
392
406
  }
393
- console.log(
407
+ log(
394
408
  `[check-docs-size] OK — ${relative(root, indexPath)} is in sync with source frontmatter.`,
395
409
  );
396
- return;
410
+ return result(0);
397
411
  }
398
412
 
399
- printReport(rows, flags.quiet);
413
+ printReport(rows, flags.quiet, log);
400
414
  const errorCount = rows.reduce((n, r) => n + r.errors.length, 0);
401
415
  const warnCount = rows.reduce((n, r) => n + r.warnings.length, 0);
402
- console.log(
416
+ log(
403
417
  `\n${rows.length} files inspected — ${errorCount} error(s), ${warnCount} warning(s)`,
404
418
  );
405
419
 
406
- if (errorCount > 0 && !flags.report) process.exit(1);
420
+ return result(errorCount > 0 && !flags.report ? 1 : 0);
407
421
  };
408
422
 
409
423
  const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
410
424
  if (isDirectRun) {
411
- await main();
425
+ const { code, stdout, stderr } = await runCli(process.argv.slice(2));
426
+ if (stdout) process.stdout.write(stdout);
427
+ if (stderr) process.stderr.write(stderr);
428
+ process.exitCode = code;
412
429
  }