@sabaiway/agent-workflow-memory 4.6.0 → 4.7.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 +46 -0
- package/SKILL.md +15 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/scripts/spec-schema.mjs +37 -5
- package/references/scripts/spec-schema.test.mjs +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ 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
|
+
## 4.7.0 — the reader's verdict gains the additive `structure` extraction (AD-114)
|
|
8
|
+
|
|
9
|
+
The slice-2 checker (`spec-check`, next release) must read a document's structure through the SAME
|
|
10
|
+
module that defines "malformed" — a second parser of the frozen grammar would fork that definition.
|
|
11
|
+
`readSpecDocument` therefore now returns, beside the untouched errors and warnings, an additive
|
|
12
|
+
`structure` field in a shape frozen at plan time:
|
|
13
|
+
|
|
14
|
+
- **`{scenarios: [{ordinal, binding: {file, marker} | null}], children: [{name, target}], parts:
|
|
15
|
+
[{name, target}], module: {form: 'root' | 'fileSet', paths: [...]} | null}`** — every target
|
|
16
|
+
VERBATIM as written (`./x.md` and `./x/index.md` stay distinct strings).
|
|
17
|
+
- **Null ONLY on the early refusals** (missing frontmatter, a frontmatter-key defect, an unknown
|
|
18
|
+
kind); for a known kind with errors it is the DETERMINISTIC extraction of what parsed: a
|
|
19
|
+
grammar-malformed scenario/child/part line is simply absent (valid lines before and after it
|
|
20
|
+
extract), while a line that parses but breaks a rule still extracts verbatim beside its error.
|
|
21
|
+
- **The module is a conjunction** — ONE `dir/` root or an all-file list; prose, a refused path, a
|
|
22
|
+
dir/file mix or `*(empty)*` extracts `module: null`. `## Links` stays free prose, never
|
|
23
|
+
extracted.
|
|
24
|
+
- The 33 rule ids, every refusal and every existing verdict field stay behaviour-identical (the
|
|
25
|
+
engine corpus suite is green untouched; engine stays 3.3.0). The deep-equal suite pins the shape
|
|
26
|
+
per kind, both module forms, every early-refusal branch and the partial extractions; the new
|
|
27
|
+
tests were red-proofed against the pre-change reader.
|
|
28
|
+
|
|
29
|
+
MINOR: a shipped script gains a capability. One prose alignment rides along: the upgrade's
|
|
30
|
+
refresh-lane sentence now names a file of EITHER deployed pair (reader or checker) on a shipped
|
|
31
|
+
body as refreshed by the composition root's upgrade — this substrate still carries no catalog and
|
|
32
|
+
never overwrites a deployed script itself.
|
|
33
|
+
|
|
34
|
+
## 4.6.1 — the standalone upgrade delivers the spec layer behind a checker it can prove (AD-113)
|
|
35
|
+
|
|
36
|
+
4.6.0 shipped the feature-spec layer for FRESH bootstraps; an EXISTING deployment at lineage head
|
|
37
|
+
`3.0.0` had no lane to it. The standalone (skill-only) upgrade now closes that gap in prose, with no
|
|
38
|
+
digest catalog and no change to the shipped payload:
|
|
39
|
+
|
|
40
|
+
- **SKILL.md upgrade step 2 gains the spec-layer twin.** The reader pair (`scripts/spec-schema.mjs`
|
|
41
|
+
+ its test) seeds create-only if missing. The store root `docs/ai/specs/index.md` seeds ONLY when
|
|
42
|
+
BOTH deployed pairs — reader and checker — are regular files byte-equal to the bundled copies
|
|
43
|
+
(checked via lstat; a dangling symlink anywhere means nothing is written), with the template's
|
|
44
|
+
date placeholder rendered. Every other state — a custom or partial pair, reader or checker — is
|
|
45
|
+
reported as is, with the composition root's upgrade named as the refresh lane and NO store root
|
|
46
|
+
written.
|
|
47
|
+
- **`standalone-bootstrap.test.mjs` models the equal-head upgrade both ways:** both pairs
|
|
48
|
+
bundle-equal -> store seeded and the installed pre-commit hook exits 0 over a seeded spec; a
|
|
49
|
+
custom or partial pair -> reported, no store.
|
|
50
|
+
|
|
51
|
+
PATCH: prose + the E2E model only.
|
|
52
|
+
|
|
7
53
|
## 4.6.0 — the feature-spec layer: a store under `docs/ai/specs/`, one text-only reader, a counted navigator row (AD-112)
|
|
8
54
|
|
|
9
55
|
A deployed project gains a middle altitude of durable knowledge between the whole-project
|
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: '4.
|
|
6
|
+
version: '4.7.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-memory
|
|
@@ -225,6 +225,20 @@ Fill strategy:
|
|
|
225
225
|
hook gains its `archive-decisions.mjs --check` line only when the hook is next refreshed via
|
|
226
226
|
`node scripts/install-git-hooks.mjs`; an old hook without the line stays consistent-safe (the
|
|
227
227
|
decisions gate is simply not enforced yet — never a broken hook). **Same gate, also
|
|
228
|
+
stamp-independent — ensure the SPEC LAYER (Node projects; skip on a No-Node project like the
|
|
229
|
+
pair above):** copy `spec-schema.mjs` + `spec-schema.test.mjs` from
|
|
230
|
+
`${CLAUDE_SKILL_DIR}/references/scripts/` into the project's `scripts/` **if missing** (preserve
|
|
231
|
+
an existing file byte-for-byte). Then seed `docs/ai/specs/index.md` **if missing** from
|
|
232
|
+
`${CLAUDE_SKILL_DIR}/references/templates/specs/` (date filled; an existing root is preserved
|
|
233
|
+
byte-for-byte) **ONLY if BOTH pairs** — that reader pair AND `check-docs-size.mjs` +
|
|
234
|
+
`check-docs-size.test.mjs` — are regular files (probe with `ls -l`, never a follow-the-link
|
|
235
|
+
test: a symlink, dangling or not, or a directory at any of the five paths means NOTHING is
|
|
236
|
+
written) **byte-equal** to this skill's bundled copies (compare with `cmp`): the deployed checker
|
|
237
|
+
is what the pre-commit hook runs, and an older or edited one renders the store row by row and
|
|
238
|
+
reds its own index check. Otherwise seed NO store root and report which file differs: a
|
|
239
|
+
file of either pair still on a body a release shipped is refreshed by the composition root's own upgrade
|
|
240
|
+
(it carries the catalog of shipped bodies), an edited one is the user's to refresh by hand — this
|
|
241
|
+
substrate carries no such catalog and never overwrites a deployed script. **Same gate, also
|
|
228
242
|
stamp-independent — ensure the NAVIGATOR:** `docs/ai/index.md` is a GENERATED artifact the entry
|
|
229
243
|
point declares always-loaded, so a deployment that never had one must gain it even at head — run
|
|
230
244
|
`node ${CLAUDE_SKILL_DIR}/references/scripts/check-docs-size.mjs --ensure-index --root=<target>`
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-memory",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.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",
|
|
@@ -310,13 +310,45 @@ const checkParts = (parsed, at, errors) => {
|
|
|
310
310
|
if (duplicate !== undefined) errors.push({ rule: 'parts', message: `part "${duplicate}" is listed twice` });
|
|
311
311
|
};
|
|
312
312
|
|
|
313
|
-
// The verdict
|
|
314
|
-
//
|
|
315
|
-
//
|
|
313
|
+
// The structure verdict (additive, slice 2a): the DETERMINISTIC extraction of what parsed, per
|
|
314
|
+
// section, targets VERBATIM as written (`./x.md` and `./x/index.md` stay distinct strings). The
|
|
315
|
+
// grammar is per line for scenarios/children/parts — a malformed line is simply absent — while the
|
|
316
|
+
// module is a conjunction (ONE `dir/` root or an all-file list): prose, a refused path, a mix or
|
|
317
|
+
// `*(empty)*` extracts null. `## Links` is free prose and is never extracted.
|
|
318
|
+
const scenarioEntry = (line) => {
|
|
319
|
+
const scenario = parseScenario(line);
|
|
320
|
+
if (scenario === null) return null;
|
|
321
|
+
return { ordinal: scenario.n, binding: scenario.bound ? { file: scenario.path, marker: scenario.marker } : null };
|
|
322
|
+
};
|
|
323
|
+
const linkEntry = (re) => (line) => {
|
|
324
|
+
const m = line.match(re);
|
|
325
|
+
return m && SLUG_RE.test(m[2]) ? { name: m[1], target: `./${m[2]}${m[3] ?? ''}.md` } : null;
|
|
326
|
+
};
|
|
327
|
+
const entriesOf = (parsed, heading, entryOf) =>
|
|
328
|
+
contentOf(sectionLines(parsed, heading) ?? []).map(entryOf).filter((entry) => entry !== null);
|
|
329
|
+
const extractModule = (parsed) => {
|
|
330
|
+
const content = contentOf(sectionLines(parsed, '## Module') ?? []);
|
|
331
|
+
const paths = bulletsOf(content);
|
|
332
|
+
if (paths.length === 0 || content.some((line) => !isBullet(line))) return null;
|
|
333
|
+
const kinds = paths.map(classifyPath);
|
|
334
|
+
if (paths.length === 1 && kinds[0] === 'dir') return { form: 'root', paths };
|
|
335
|
+
return kinds.every((kind) => kind === 'file') ? { form: 'fileSet', paths } : null;
|
|
336
|
+
};
|
|
337
|
+
const extractStructure = (parsed) => ({
|
|
338
|
+
scenarios: entriesOf(parsed, '## Scenarios', scenarioEntry),
|
|
339
|
+
children: entriesOf(parsed, '## Children', linkEntry(CHILD_LINK_RE)),
|
|
340
|
+
parts: entriesOf(parsed, '## Parts', linkEntry(PART_LINK_RE)),
|
|
341
|
+
module: extractModule(parsed),
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// The verdict: { kind, status, revision, structure, errors: [{ rule, message }], warnings: [{ rule,
|
|
345
|
+
// message }] }. Errors are collected past the first defect wherever later checks stay meaningful; a
|
|
346
|
+
// missing or defective frontmatter and an unknown kind each end the read (structure stays null),
|
|
347
|
+
// because no shape can be judged without them.
|
|
316
348
|
export const readSpecDocument = (text, rel) => {
|
|
317
349
|
const errors = [];
|
|
318
350
|
const warnings = [];
|
|
319
|
-
const verdict = (kind, status, revision) => ({ kind, status, revision, errors, warnings });
|
|
351
|
+
const verdict = (kind, status, revision, structure = null) => ({ kind, status, revision, structure, errors, warnings });
|
|
320
352
|
const front = parseFrontmatter(text.replace(/\r\n/g, '\n'));
|
|
321
353
|
if (front === null) {
|
|
322
354
|
errors.push({ rule: 'frontmatter', message: 'missing YAML frontmatter' });
|
|
@@ -351,5 +383,5 @@ export const readSpecDocument = (text, rel) => {
|
|
|
351
383
|
}
|
|
352
384
|
const status = kind === 'spec' ? fields.status ?? null : null;
|
|
353
385
|
const revision = kind === 'spec' && REVISION_RE.test(fields.revision ?? '') ? Number(fields.revision) : null;
|
|
354
|
-
return verdict(kind, status, revision);
|
|
386
|
+
return verdict(kind, status, revision, extractStructure(parsed));
|
|
355
387
|
};
|
|
@@ -194,6 +194,73 @@ describe('readSpecDocument — refuse, exactly one rule per defect', () => {
|
|
|
194
194
|
});
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
+
describe('readSpecDocument — the structure verdict (additive, slice 2a)', () => {
|
|
198
|
+
const structureOf = (text, rel) => readSpecDocument(text, rel).structure;
|
|
199
|
+
const BOTH_SCENARIOS = [
|
|
200
|
+
{ ordinal: 1, binding: { file: 'test/login.test.mjs', marker: 'spec:login/S1' } },
|
|
201
|
+
{ ordinal: 2, binding: null },
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
it('a flat spec extracts scenarios (bound + unbound) and its one dir/ module root', () => {
|
|
205
|
+
expect(structureOf(specDoc(), 'login.md')).toEqual({
|
|
206
|
+
scenarios: BOTH_SCENARIOS,
|
|
207
|
+
children: [],
|
|
208
|
+
parts: [],
|
|
209
|
+
module: { form: 'root', paths: ['src/login/'] },
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('a promoted root extracts parts and a fileSet module; ## Links stays free prose, never extracted', () => {
|
|
214
|
+
const text = specDoc({ module: '- src/a.mjs\n- src/b.mjs', extra: '\n## Parts\n\n- [sessions](./sessions.md)\n\n## Links\n\n- [[AD-112]]\n' });
|
|
215
|
+
expect(structureOf(text, 'auth/login/index.md')).toEqual({
|
|
216
|
+
scenarios: BOTH_SCENARIOS,
|
|
217
|
+
children: [],
|
|
218
|
+
parts: [{ name: 'sessions', target: './sessions.md' }],
|
|
219
|
+
module: { form: 'fileSet', paths: ['src/a.mjs', 'src/b.mjs'] },
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('an index extracts children with VERBATIM targets — ./x.md and ./x/index.md stay distinct strings', () => {
|
|
224
|
+
expect(structureOf(indexDoc(), 'auth/index.md')).toEqual({
|
|
225
|
+
scenarios: [],
|
|
226
|
+
children: [{ name: 'login', target: './login.md' }, { name: 'billing', target: './billing/index.md' }],
|
|
227
|
+
parts: [],
|
|
228
|
+
module: null,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('a part extracts the empty structure; a retired *(empty)* module extracts null on a CLEAN document', () => {
|
|
233
|
+
expect(structureOf(partDoc(), 'auth/login/sessions.md')).toEqual({ scenarios: [], children: [], parts: [], module: null });
|
|
234
|
+
const retired = specDoc({ fields: { status: 'retired' }, module: '*(empty)*', scenarios: ['- S1 gone :: unbound'] });
|
|
235
|
+
expect(structureOf(retired, 'login.md').module).toBeNull();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('EVERY early refusal reads structure null: missing frontmatter, a frontmatter defect, an unknown or absent kind', () => {
|
|
239
|
+
expect(structureOf('# Spec: Login\n', 'login.md')).toBeNull();
|
|
240
|
+
expect(structureOf(specDoc({ fields: { priority: 'high' } }), 'login.md')).toBeNull();
|
|
241
|
+
expect(structureOf(specDoc({ fields: { kind: 'feature' } }), 'login.md')).toBeNull();
|
|
242
|
+
expect(structureOf(specDoc({ drop: ['kind'] }), 'login.md')).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('a grammar-malformed scenario/child/part line is simply ABSENT — valid lines before and after it extract', () => {
|
|
246
|
+
const s = structureOf(specDoc({ scenarios: ['- S1 a :: unbound', '- S2 broken', '- S3 c :: unbound'] }), 'login.md');
|
|
247
|
+
expect(s.scenarios).toEqual([{ ordinal: 1, binding: null }, { ordinal: 3, binding: null }]);
|
|
248
|
+
const c = structureOf(indexDoc({ children: ['- [a](./a.md)', '- broken', '- [b](./b/index.md)'] }), 'auth/index.md');
|
|
249
|
+
expect(c.children).toEqual([{ name: 'a', target: './a.md' }, { name: 'b', target: './b/index.md' }]);
|
|
250
|
+
const p = structureOf(specDoc({ extra: '\n## Parts\n\n- [a](./a.md)\n- broken\n- [b](./b.md)\n' }), 'auth/login/index.md');
|
|
251
|
+
expect(p.parts).toEqual([{ name: 'a', target: './a.md' }, { name: 'b', target: './b.md' }]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('the module is a CONJUNCTION — prose, a refused path, a dir/file mix each extract null; a rule-refused scenario line still extracts verbatim', () => {
|
|
255
|
+
for (const module of ['the root is\n- src/login/', '- ../src/', '- src/login/\n- src/login/a.mjs']) {
|
|
256
|
+
expect(structureOf(specDoc({ module }), 'login.md').module).toBeNull();
|
|
257
|
+
}
|
|
258
|
+
const v = readSpecDocument(specDoc({ scenarios: ['- S1 a :: test/a.mjs :: spec:login/S2'] }), 'login.md');
|
|
259
|
+
expect(v.errors.map((e) => e.rule)).toEqual(['scenario-marker']);
|
|
260
|
+
expect(v.structure.scenarios).toEqual([{ ordinal: 1, binding: { file: 'test/a.mjs', marker: 'spec:login/S2' } }]);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
197
264
|
describe('classifyPath + the frozen constants', () => {
|
|
198
265
|
it('classifies the lexical path forms', () => {
|
|
199
266
|
expect(['src/', 'src/a.mjs', '../x', '/x', 'C:/x', 'a\\b', 'src/*.mjs'].map(classifyPath)).toEqual([
|