@sabaiway/agent-workflow-memory 8.1.5 → 8.2.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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ 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 `4.0.0`).
|
|
6
6
|
|
|
7
|
+
## 8.2.0 — the docs navigator stamps a cap it honours past 80 lines (AD-182)
|
|
8
|
+
|
|
9
|
+
**`references/scripts/check-docs-size.mjs` now writes a `docs/ai/index.md` that its own default run and `--check-index`
|
|
10
|
+
accept, however many docs the project has.** Before, the navigator always stamped `maxLines: 80` while it renders one
|
|
11
|
+
row per document outside the collapsed ADR and spec stores, so a project with enough docs (measured: 67 docs, an 87-line navigator) got a navigator its own
|
|
12
|
+
pre-commit hook refused.
|
|
13
|
+
|
|
14
|
+
- The stamp is `maxLines: 80` while the rendered navigator has 80 lines or fewer, and its own line count past that,
|
|
15
|
+
counted as the gate counts (a trailing newline closes the last line and opens none). A navigator that fits is
|
|
16
|
+
byte-identical to before. There is no ceiling, as for the ADR navigator (AD-167).
|
|
17
|
+
- A hand-edited cap line still reads as stale under `--check-index`, as before.
|
|
18
|
+
- `SKILL.md`: a deployed spec-layer script with an edited body is named by the composition root's upgrade and replaced
|
|
19
|
+
only on the user's yes; this substrate still never overwrites a deployed script.
|
|
20
|
+
- MINOR: the navigator's stamped cap changes past 80 lines. The deployed `check-docs-size.test.mjs` body is unchanged.
|
|
21
|
+
|
|
7
22
|
## 8.1.5 — the gate migration names the `lcovProducer` marker (AD-175)
|
|
8
23
|
|
|
9
24
|
`references/scripts/migrate-gates.mjs`: the four hints that print the paste-ready suite cmd (the
|
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: '8.
|
|
6
|
+
version: '8.2.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-memory
|
|
@@ -251,7 +251,8 @@ Fill strategy:
|
|
|
251
251
|
is what the pre-commit hook runs, and an older or edited one renders the store row by row and
|
|
252
252
|
reds its own index check. Otherwise seed NO store root and report which file differs: a
|
|
253
253
|
file of either pair still on a body a release shipped is refreshed by the composition root's own upgrade
|
|
254
|
-
(it carries the catalog of shipped bodies), an edited one is
|
|
254
|
+
(it carries the catalog of shipped bodies), and an edited one is named by that upgrade and replaced
|
|
255
|
+
only on the user's yes — this
|
|
255
256
|
substrate carries no such catalog and never overwrites a deployed script. **Same gate, also
|
|
256
257
|
stamp-independent — ensure the NAVIGATOR:** `docs/ai/index.md` is a GENERATED artifact the entry
|
|
257
258
|
point declares always-loaded, so a deployment that never had one must gain it even at head — run
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-memory",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.0",
|
|
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",
|
|
@@ -58,6 +58,51 @@ maxLines: 80
|
|
|
58
58
|
| [\`a.md\`](./a.md) | state | 10/10 | 2026-07-18 | never |
|
|
59
59
|
`;
|
|
60
60
|
|
|
61
|
+
describe('spec:docs-navigator/S1 the navigator stamps a cap it honours', () => {
|
|
62
|
+
const createNames = (count) => Array.from({ length: count }, (_, index) => `n${String(index + 1).padStart(2, '0')}`);
|
|
63
|
+
const buildDocs = (names) => ({ docs }) => {
|
|
64
|
+
for (const name of names) writeFileSync(join(docs, `${name}.md`), DOC(name));
|
|
65
|
+
};
|
|
66
|
+
const countLines = (text) => text.split('\n').length - (text.endsWith('\n') ? 1 : 0);
|
|
67
|
+
|
|
68
|
+
it("a tree whose navigator renders exactly 80 lines stamps maxLines 80, byte-identical to today's render", async () => {
|
|
69
|
+
const names = createNames(57);
|
|
70
|
+
await withRoot(buildDocs(names), async ({ root }) => {
|
|
71
|
+
assert.equal((await cli(['--write-index', `--root=${root}`, '--today=2026-07-18'])).code, 0);
|
|
72
|
+
const index = indexOf(root);
|
|
73
|
+
const rows = names.map((name) => `| [\`${name}.md\`](./${name}.md) | state | 10/10 | 2026-07-18 | never |\n`);
|
|
74
|
+
assert.equal(countLines(index), 80);
|
|
75
|
+
assert.equal(index, EXPECTED_INDEX + rows.join(''));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('a tree of 81 lines stamps maxLines 81 and both checker runs exit 0', async () => {
|
|
80
|
+
await withRoot(buildDocs(createNames(58)), async ({ root }) => {
|
|
81
|
+
assert.equal((await cli(['--write-index', `--root=${root}`, '--today=2026-07-18'])).code, 0);
|
|
82
|
+
const index = indexOf(root);
|
|
83
|
+
const checked = await runCli([`--root=${root}`]);
|
|
84
|
+
const fresh = await runCli(['--check-index', `--root=${root}`]);
|
|
85
|
+
assert.equal(countLines(index), 81);
|
|
86
|
+
assert.equal(index.match(/^maxLines: \d+$/m)?.[0], 'maxLines: 81', `default: ${checked.code}, --check-index: ${fresh.code}`);
|
|
87
|
+
assert.equal(checked.code, 0, checked.stdout + checked.stderr);
|
|
88
|
+
assert.equal(fresh.code, 0, fresh.stdout + fresh.stderr);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('a hand-edited cap line is stale under --check-index', async () => {
|
|
93
|
+
await withRoot(buildDocs(createNames(58)), async ({ root, docs }) => {
|
|
94
|
+
assert.equal((await cli(['--write-index', `--root=${root}`, '--today=2026-07-18'])).code, 0);
|
|
95
|
+
const index = indexOf(root);
|
|
96
|
+
const edited = index.replace(/^maxLines: \d+$/m, 'maxLines: 90');
|
|
97
|
+
assert.notEqual(edited, index);
|
|
98
|
+
writeFileSync(join(docs, 'index.md'), edited);
|
|
99
|
+
const { code, stdout, stderr } = await runCli(['--check-index', `--root=${root}`]);
|
|
100
|
+
assert.equal(code, 1);
|
|
101
|
+
assert.match(stdout + stderr, /is stale/);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
61
106
|
describe('check-docs-size runCli — refusal branches', () => {
|
|
62
107
|
it('an unknown argument exits 2 naming it', async () => {
|
|
63
108
|
const { code, stderr } = await cli(['--bogus']);
|
|
@@ -11,6 +11,9 @@ import { runCli } from './check-docs-size.mjs';
|
|
|
11
11
|
const DOC = (title) =>
|
|
12
12
|
`---\ntype: state\nlastUpdated: 2026-08-15\nscope: session\nstaleAfter: never\nowner: none\nmaxLines: 10\n---\n\n# ${title}\n`;
|
|
13
13
|
|
|
14
|
+
const NAVIGATOR_LINE_FLOOR = 80;
|
|
15
|
+
const EXTRA_DOC_COUNT = 85;
|
|
16
|
+
|
|
14
17
|
const makeTree = (prefix) => {
|
|
15
18
|
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
16
19
|
mkdirSync(join(dir, 'docs', 'ai'), { recursive: true });
|
|
@@ -330,3 +333,28 @@ describe('check-docs-size --ensure-index', () => {
|
|
|
330
333
|
}
|
|
331
334
|
});
|
|
332
335
|
});
|
|
336
|
+
|
|
337
|
+
describe('spec:docs-navigator/S2 --ensure-index past the 80-line floor', () => {
|
|
338
|
+
it('writes a navigator past 80 lines that the default run and --check-index accept', async () => {
|
|
339
|
+
const root = makeTree('cds-ensure-past-floor-');
|
|
340
|
+
try {
|
|
341
|
+
for (const number of Array.from({ length: EXTRA_DOC_COUNT }, (_, index) => index)) {
|
|
342
|
+
writeFileSync(join(root, 'docs', 'ai', `z-extra-${number}.md`), DOC(`extra ${number}`));
|
|
343
|
+
}
|
|
344
|
+
const { code, stdout } = await runCli(['--ensure-index', `--root=${root}`]);
|
|
345
|
+
assert.equal(code, 0);
|
|
346
|
+
assert.match(stdout, /ensure-index: regenerated/);
|
|
347
|
+
const navigator = readFileSync(join(root, 'docs', 'ai', 'index.md'), 'utf8');
|
|
348
|
+
const lineCount = navigator.split('\n').length - (navigator.endsWith('\n') ? 1 : 0);
|
|
349
|
+
const maxLines = Number(navigator.match(/^maxLines: (\d+)$/m)?.[1]);
|
|
350
|
+
assert.ok(lineCount > NAVIGATOR_LINE_FLOOR);
|
|
351
|
+
const defaultRun = await runCli([`--root=${root}`]);
|
|
352
|
+
const indexCheck = await runCli(['--check-index', `--root=${root}`]);
|
|
353
|
+
assert.equal(defaultRun.code, 0, defaultRun.stdout);
|
|
354
|
+
assert.equal(maxLines, lineCount);
|
|
355
|
+
assert.equal(indexCheck.code, 0, indexCheck.stdout);
|
|
356
|
+
} finally {
|
|
357
|
+
drop(root);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -45,6 +45,8 @@ const pathsFor = (root) => {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
48
|
+
const NAVIGATOR_CAP_FLOOR = 80;
|
|
49
|
+
const countLines = (text) => text.split('\n').length - (text.endsWith('\n') ? 1 : 0);
|
|
48
50
|
|
|
49
51
|
// The one token every `--ensure-index` outcome line opens with — deploy/upgrade prose relays it
|
|
50
52
|
// verbatim and the kit's ensure op reads it, so it is a contract, not a message.
|
|
@@ -263,7 +265,7 @@ export const inspectFile = async (found, today, root = ROOT) => {
|
|
|
263
265
|
return { path: rel, lineCount: 0, frontmatter: null, spec: null, refused: true, errors: [refusal], warnings: [] };
|
|
264
266
|
}
|
|
265
267
|
const text = await readFile(filePath, 'utf8');
|
|
266
|
-
const lineCount =
|
|
268
|
+
const lineCount = countLines(text);
|
|
267
269
|
const fm = parseFrontmatter(text);
|
|
268
270
|
const spec = readSpecVerdict(rel, text);
|
|
269
271
|
|
|
@@ -332,7 +334,7 @@ lastUpdated: __TODAY__
|
|
|
332
334
|
scope: permanent
|
|
333
335
|
staleAfter: 30d
|
|
334
336
|
owner: none
|
|
335
|
-
maxLines:
|
|
337
|
+
maxLines: __CAP__
|
|
336
338
|
---
|
|
337
339
|
|
|
338
340
|
# Memory Map — __PROJECT__ \`docs/ai/\`
|
|
@@ -398,7 +400,6 @@ export const buildIndex = (rows, todayStr, meta = {}) => {
|
|
|
398
400
|
const projectName = meta.projectName ?? DEFAULT_PROJECT_NAME;
|
|
399
401
|
const onDemandLinks = meta.onDemandLinks ?? [];
|
|
400
402
|
const hierarchicalLinks = meta.hierarchicalLinks ?? [];
|
|
401
|
-
const header = INDEX_HEADER.replace('__TODAY__', todayStr).replace('__PROJECT__', projectName);
|
|
402
403
|
const tableHeader = `| File | Type | Lines/Max | Updated | Stale after |\n|------|------|-----------|---------|-------------|`;
|
|
403
404
|
const singles = [];
|
|
404
405
|
const grouped = COLLAPSE_GROUPS.map(() => []);
|
|
@@ -421,7 +422,8 @@ export const buildIndex = (rows, todayStr, meta = {}) => {
|
|
|
421
422
|
hierarchicalLinks.length > 0
|
|
422
423
|
? `\n\n## Subdirectory \`AGENTS.md\` (hierarchical)\n\n${hierarchicalLinks.map((link) => `- ${link}`).join('\n')}`
|
|
423
424
|
: '';
|
|
424
|
-
|
|
425
|
+
const render = (cap) => `${INDEX_HEADER.replace('__CAP__', cap).replace('__TODAY__', todayStr).replace('__PROJECT__', projectName)}${tableHeader}\n${tableRows}${onDemandSection}${hierarchicalSection}\n`;
|
|
426
|
+
return render(Math.max(NAVIGATOR_CAP_FLOOR, countLines(render(NAVIGATOR_CAP_FLOOR))));
|
|
425
427
|
};
|
|
426
428
|
|
|
427
429
|
// Freshness regenerates the index with the on-disk header's OWN `lastUpdated`, so a mere day-rollover
|