@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 +12 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/scripts/archive-decisions.mjs +2 -2
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
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.
|
|
6
|
+
version: '3.0.1'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-memory
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-memory",
|
|
3
|
-
"version": "3.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
|
|
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
|
|
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 —
|
|
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 —
|
|
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 —
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
239
|
-
for (const warn of row.warnings)
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
390
|
+
log(`Wrote ${relative(root, indexPath)}`);
|
|
377
391
|
const after = await stat(indexPath);
|
|
378
392
|
if (after.size === 0) {
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
+
return result(1);
|
|
392
406
|
}
|
|
393
|
-
|
|
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
|
-
|
|
416
|
+
log(
|
|
403
417
|
`\n${rows.length} files inspected — ${errorCount} error(s), ${warnCount} warning(s)`,
|
|
404
418
|
);
|
|
405
419
|
|
|
406
|
-
|
|
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
|
|
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
|
}
|