@sabaiway/agent-workflow-memory 7.2.0 → 8.0.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,35 @@ 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
|
+
## 8.0.0 — the reader refuses an open-list clause: a guarantee is an invariant, never a list of cases (AD-133)
|
|
8
|
+
|
|
9
|
+
**BREAKING — the 35th rule id, `open-list`.** `references/scripts/spec-schema.mjs` refuses a Contract clause that
|
|
10
|
+
carries a check verb (`refuse` / `refusal`, case-sensitive substrings) AND an enumeration of two or more items
|
|
11
|
+
(`a, b or c` / `a · b`) AND no closure token inside that same clause. The unit is the CLAUSE: a top-level bullet joined
|
|
12
|
+
with its continuation lines (a blank line, the next top-level bullet — an empty `-` included — or any ATX heading,
|
|
13
|
+
indented up to three spaces, ends it), split at `. ` and `; ` outside a backtick span — the span is opaque to the split
|
|
14
|
+
and to nothing else. Every `## Contract` section of a `kind: spec`, every top-level bullet of a `kind: part`; never an
|
|
15
|
+
index or prose outside bullets. Twelve closure tokens (`only if`, `only when`, `the only`, `everything else`,
|
|
16
|
+
`anything else`, `nothing else`, `every other`, `any other`, `otherwise`, `exactly`, `one of`, `closed`) match
|
|
17
|
+
case-insensitively at word boundaries; `ONLY` is the one case-sensitive emphatic word (a lowercase `only` alone closes
|
|
18
|
+
nothing; `READ-ONLY` and `closed-key` glue). A single-case check is its own closure; a closure that is FALSE is out of
|
|
19
|
+
scope — a form rule. The remedy for a spec valid under the old canon that now refuses: state the invariant in the
|
|
20
|
+
clause ("accepted only if …; everything else is the named refusal …"), never a fourth case; the refusal names the
|
|
21
|
+
clause's first six words. `SPEC_SCHEMA.openList` carries the frozen fragment (every list frozen); `rules` gains
|
|
22
|
+
`open-list` after `fence`. The comment blocks became one-line pointers naming no sibling package; the module is 415
|
|
23
|
+
lines, recorded. The test twin gains the "open-list clauses" describe (clause and bullet boundaries, the span mask, the
|
|
24
|
+
matching rules, every Contract section, part scope, frozenness). Mirrored byte-identically into the kit and the root
|
|
25
|
+
`scripts/`.
|
|
26
|
+
|
|
27
|
+
## 7.2.1 — the orchestration seed's note names four activities (AD-132)
|
|
28
|
+
|
|
29
|
+
`references/templates/orchestration.json`: the `_README` is the kit's new canonical byte for byte —
|
|
30
|
+
four activities (`feedback-triage` with its one `review` slot), the seeded slots `solo`, a silent slot
|
|
31
|
+
its computed default (`feedback-triage.review` reviewed as soon as a review backend is ready, solo
|
|
32
|
+
until then). The seed's slot shape is unchanged (plan-authoring · plan-execution, every value `solo`),
|
|
33
|
+
so an older kit still accepts it; a deployed file carrying the outgoing note refreshes on the next
|
|
34
|
+
write. No script changed.
|
|
35
|
+
|
|
7
36
|
## 7.2.0 — the standalone bootstrap offers enforcement on every project (AD-131)
|
|
8
37
|
|
|
9
38
|
`SKILL.md` steps 8–9 no longer decide by the stack recon ("Node projects" / "No Node runtime → skip
|
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: '
|
|
6
|
+
version: '8.0.0'
|
|
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
|
+
"version": "8.0.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",
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Pure text in, verdict out: readSpecDocument(text, rel) never touches the filesystem and imports
|
|
5
|
-
// nothing, so it seeds layout-free into any deployment. The navigator's collapse (check-docs-size.mjs)
|
|
6
|
-
// and the future spec-check read through THIS module — "malformed" has one definition, never two.
|
|
7
|
-
// `rel` is the path INSIDE docs/ai/specs/ (the store root navigator is `index.md`).
|
|
8
|
-
//
|
|
9
|
-
// SPEC_SCHEMA carries the frozen values; the engine canon (references/specs.md) is pinned against it.
|
|
10
|
-
// A refusal names exactly one rule id per defect, so a fixture corpus can be read rule by rule.
|
|
2
|
+
// Canon: the methodology engine's references/specs.md (frozen schema, refusals table).
|
|
11
3
|
|
|
12
4
|
export const SPEC_SCHEMA = Object.freeze({
|
|
13
5
|
storePrefix: 'docs/ai/specs/',
|
|
@@ -34,17 +26,25 @@ export const SPEC_SCHEMA = Object.freeze({
|
|
|
34
26
|
rootOwnedKeys: Object.freeze(['status', 'revision']),
|
|
35
27
|
rootOwnedSections: Object.freeze(['## Scenarios', '## Out of scope', '## Module', '## Parts']),
|
|
36
28
|
scenarioGrammar: '- S<N> <name> :: <repo-relative test path> :: spec:<slug>/S<N> | - S<N> <name> :: unbound',
|
|
29
|
+
openList: Object.freeze({
|
|
30
|
+
checkVerbs: Object.freeze(['refuse', 'refusal', 'REFUSE', 'REFUSAL']),
|
|
31
|
+
enumerationForms: Object.freeze([Object.freeze([',', ' or ']), Object.freeze([' · '])]),
|
|
32
|
+
closureTokens: Object.freeze([
|
|
33
|
+
'only if', 'only when', 'the only', 'everything else', 'anything else', 'nothing else',
|
|
34
|
+
'every other', 'any other', 'otherwise', 'exactly', 'one of', 'closed',
|
|
35
|
+
]),
|
|
36
|
+
emphaticToken: 'ONLY',
|
|
37
|
+
}),
|
|
37
38
|
rules: Object.freeze([
|
|
38
39
|
'frontmatter', 'frontmatter-key', 'substrate-key', 'type', 'kind', 'maxlines', 'status', 'revision',
|
|
39
40
|
'root-owns', 'slug', 'kind-path', 'root-uplink', 'title', 'section-missing', 'section-order',
|
|
40
|
-
'section-forbidden', 'fence', 'children-link', 'children-duplicate', 'fan-out', 'scenario-line',
|
|
41
|
+
'section-forbidden', 'fence', 'open-list', 'children-link', 'children-duplicate', 'fan-out', 'scenario-line',
|
|
41
42
|
'scenario-number', 'scenario-marker', 'scenario-path', 'scenarios-empty', 'out-of-scope', 'module-line', 'module-empty',
|
|
42
43
|
'module-traversal', 'module-absolute', 'module-backslash', 'module-glob', 'module-mix', 'parts',
|
|
43
44
|
]),
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
//
|
|
47
|
-
// is clean collapse into ONE navigator row linking `navPath`; a row with reader errors stays visible.
|
|
47
|
+
// Consumer contract: scripts/check-docs-size.mjs.
|
|
48
48
|
export const SPECS_COLLAPSE = Object.freeze({
|
|
49
49
|
prefix: SPEC_SCHEMA.storePrefix,
|
|
50
50
|
navPath: `${SPEC_SCHEMA.storePrefix}${SPEC_SCHEMA.navigatorFile}`,
|
|
@@ -72,8 +72,6 @@ const FORBIDDEN_SECTIONS = Object.freeze({
|
|
|
72
72
|
|
|
73
73
|
const KNOWN_KEYS = Object.freeze([...SPEC_SCHEMA.substrateKeys, 'kind', ...SPEC_SCHEMA.rootOwnedKeys]);
|
|
74
74
|
|
|
75
|
-
// The frontmatter is a closed key set: an unknown key, a repeated key or a line that is not
|
|
76
|
-
// `key: value` is a defect (`frontmatter-key`), never silently dropped or last-one-wins.
|
|
77
75
|
const parseFrontmatter = (text) => {
|
|
78
76
|
const match = text.match(FRONTMATTER_RE);
|
|
79
77
|
if (!match) return null;
|
|
@@ -91,10 +89,6 @@ const parseFrontmatter = (text) => {
|
|
|
91
89
|
|
|
92
90
|
const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
|
|
93
91
|
|
|
94
|
-
// The body as { title, sections: [{ heading, lines }], preamble, fenced } — the title is the FIRST
|
|
95
|
-
// `# ` line, every `## ` line opens a section, the lines before the first section are the preamble.
|
|
96
|
-
// The reader parses NO markdown code: a fence line is recorded (the `fence` refusal) rather than
|
|
97
|
-
// modelled, so a spec carries no code sample and no line is ever ambiguous between code and structure.
|
|
98
92
|
const parseBody = (body) => {
|
|
99
93
|
const sections = [];
|
|
100
94
|
const preamble = [];
|
|
@@ -116,8 +110,6 @@ const parseBody = (body) => {
|
|
|
116
110
|
return { title: state.title, sections, preamble, fenced, sectionBeforeTitle: state.sectionBeforeTitle };
|
|
117
111
|
};
|
|
118
112
|
|
|
119
|
-
// Lexical classification of a repo-relative path field — the same vocabulary for module roots and
|
|
120
|
-
// scenario bindings. Realpath/symlink containment needs the filesystem and is the checker's duty.
|
|
121
113
|
export const classifyPath = (path) => {
|
|
122
114
|
if (path.trim() === '') return 'empty';
|
|
123
115
|
if (path.includes('\\')) return 'backslash';
|
|
@@ -139,8 +131,47 @@ const MODULE_PATH_RULES = Object.freeze({
|
|
|
139
131
|
const isBullet = (line) => line === '-' || line.startsWith('- ');
|
|
140
132
|
const bulletsOf = (lines) => lines.filter(isBullet).map((line) => line.slice(2));
|
|
141
133
|
const contentOf = (lines) => lines.filter((line) => line.trim() !== '');
|
|
134
|
+
const isAtxHeading = (line) => /^ {0,3}#{1,6}(?:[ \t]|$)/.test(line);
|
|
135
|
+
const topLevelBulletsOf = (lines) => lines.reduce((state, raw) => {
|
|
136
|
+
const line = raw.replace(/\s+$/, '');
|
|
137
|
+
if (isBullet(line)) {
|
|
138
|
+
state.current = [line === '-' ? '' : line.slice(2)];
|
|
139
|
+
state.bullets.push(state.current);
|
|
140
|
+
} else if (line.trim() === '' || isAtxHeading(line)) state.current = null;
|
|
141
|
+
else if (state.current !== null) state.current.push(line.trim());
|
|
142
|
+
return state;
|
|
143
|
+
}, { bullets: [], current: null }).bullets.map((parts) => parts.join(' ').replace(/\s+/g, ' ').trim());
|
|
144
|
+
|
|
145
|
+
const CLAUSE_BREAK = String.fromCharCode(0);
|
|
146
|
+
const MASKED_DOT = String.fromCharCode(1);
|
|
147
|
+
const MASKED_SEMICOLON = String.fromCharCode(2);
|
|
148
|
+
const maskClauseSpans = (text) => [...text].reduce((state, character) => {
|
|
149
|
+
if (character === '`') state.inSpan = !state.inSpan;
|
|
150
|
+
state.output.push(state.inSpan && character === '.' ? MASKED_DOT : state.inSpan && character === ';' ? MASKED_SEMICOLON : character);
|
|
151
|
+
return state;
|
|
152
|
+
}, { inSpan: false, output: [] }).output.join('');
|
|
153
|
+
const clausesOf = (bullet) => maskClauseSpans(bullet).replace(/([.;][*`\)\]]*) /g, `$1${CLAUSE_BREAK}`).split(CLAUSE_BREAK)
|
|
154
|
+
.map((clause) => clause.split(MASKED_DOT).join('.').split(MASKED_SEMICOLON).join(';').trim()).filter(Boolean);
|
|
155
|
+
const containsInOrder = (text, tokens) => tokens.reduce((position, token) => {
|
|
156
|
+
const found = position === -1 ? -1 : text.indexOf(token, position);
|
|
157
|
+
return found === -1 ? -1 : found + token.length;
|
|
158
|
+
}, 0) !== -1;
|
|
159
|
+
const hasBoundedToken = (clause, token, flags) => new RegExp(`(^|[^A-Za-z0-9_-])${token}([^A-Za-z0-9_-]|$)`, flags).test(clause);
|
|
160
|
+
const checkOpenLists = (parsed, kind, errors) => {
|
|
161
|
+
const groups = kind === 'spec' ? parsed.sections.filter((section) => section.heading === '## Contract').map((section) => section.lines)
|
|
162
|
+
: kind === 'part' ? [parsed.preamble, ...parsed.sections.map((section) => section.lines)] : [];
|
|
163
|
+
for (const clause of groups.flatMap(topLevelBulletsOf).flatMap(clausesOf)) {
|
|
164
|
+
const hasVerb = SPEC_SCHEMA.openList.checkVerbs.some((verb) => clause.includes(verb));
|
|
165
|
+
const hasEnumeration = SPEC_SCHEMA.openList.enumerationForms.some((form) => containsInOrder(clause, form));
|
|
166
|
+
const hasClosure = SPEC_SCHEMA.openList.closureTokens.some((token) => hasBoundedToken(clause, token, 'i'))
|
|
167
|
+
|| hasBoundedToken(clause, SPEC_SCHEMA.openList.emphaticToken, '');
|
|
168
|
+
if (hasVerb && hasEnumeration && !hasClosure) {
|
|
169
|
+
const prefix = clause.split(/\s+/).slice(0, 6).join(' ');
|
|
170
|
+
errors.push({ rule: 'open-list', message: `the clause "${prefix}…" is an open list — add a closing form to it such as "only if …; everything else …"` });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
142
174
|
|
|
143
|
-
// The slug a document owns: the file stem for a flat file, the folder name for an index.md.
|
|
144
175
|
const describeRel = (rel) => {
|
|
145
176
|
const segments = rel.split('/');
|
|
146
177
|
const file = segments[segments.length - 1];
|
|
@@ -314,11 +345,7 @@ const checkParts = (parsed, at, errors) => {
|
|
|
314
345
|
if (duplicate !== undefined) errors.push({ rule: 'parts', message: `part "${duplicate}" is listed twice` });
|
|
315
346
|
};
|
|
316
347
|
|
|
317
|
-
//
|
|
318
|
-
// section, targets VERBATIM as written (`./x.md` and `./x/index.md` stay distinct strings). The
|
|
319
|
-
// grammar is per line for scenarios/children/parts — a malformed line is simply absent — while the
|
|
320
|
-
// module is a conjunction (ONE `dir/` root or an all-file list): prose, a refused path, a mix or
|
|
321
|
-
// `*(empty)*` extracts null. `## Links` is free prose and is never extracted.
|
|
348
|
+
// Structure verdict: the same canon, "The frozen schema".
|
|
322
349
|
const scenarioEntry = (line) => {
|
|
323
350
|
const scenario = parseScenario(line);
|
|
324
351
|
if (scenario === null) return null;
|
|
@@ -345,10 +372,6 @@ const extractStructure = (parsed) => ({
|
|
|
345
372
|
module: extractModule(parsed),
|
|
346
373
|
});
|
|
347
374
|
|
|
348
|
-
// The verdict: { kind, status, revision, structure, errors: [{ rule, message }], warnings: [{ rule,
|
|
349
|
-
// message }] }. Errors are collected past the first defect wherever later checks stay meaningful; a
|
|
350
|
-
// missing or defective frontmatter and an unknown kind each end the read (structure stays null),
|
|
351
|
-
// because no shape can be judged without them.
|
|
352
375
|
export const readSpecDocument = (text, rel) => {
|
|
353
376
|
const errors = [];
|
|
354
377
|
const warnings = [];
|
|
@@ -372,6 +395,7 @@ export const readSpecDocument = (text, rel) => {
|
|
|
372
395
|
const at = checkPath(rel, kind, errors);
|
|
373
396
|
const parsed = parseBody(body);
|
|
374
397
|
if (parsed.fenced.length > 0) errors.push({ rule: 'fence', message: `"${parsed.fenced[0]}" — a spec document carries no code fence` });
|
|
398
|
+
checkOpenLists(parsed, kind, errors);
|
|
375
399
|
checkSections(parsed, kind, errors);
|
|
376
400
|
if (kind === 'index') {
|
|
377
401
|
checkChildren(parsed, errors);
|
|
@@ -32,6 +32,8 @@ const refuses = (text, rel, rule) => {
|
|
|
32
32
|
const verdict = readSpecDocument(text, rel);
|
|
33
33
|
expect(rulesOf(verdict)).toEqual([rule]);
|
|
34
34
|
};
|
|
35
|
+
const withContract = (contract) => specDoc().replace('Accepts a credential pair.', contract);
|
|
36
|
+
const openListErrors = (contract, rel = 'login.md') => readSpecDocument(withContract(contract), rel).errors;
|
|
35
37
|
|
|
36
38
|
describe('readSpecDocument — accept', () => {
|
|
37
39
|
it('a flat draft spec reads clean with kind/status/revision', () => {
|
|
@@ -269,6 +271,69 @@ describe('readSpecDocument — the structure verdict (additive, slice 2a)', () =
|
|
|
269
271
|
});
|
|
270
272
|
});
|
|
271
273
|
|
|
274
|
+
describe('readSpecDocument — open-list clauses', () => {
|
|
275
|
+
const refusal = 'the clause "an anchor path reported modified, untracked…" is an open list — add a closing form to it such as "only if …; everything else …"';
|
|
276
|
+
const open = '- an anchor path reported modified, untracked or ignored is the named refusal `anchor-dirty`';
|
|
277
|
+
const closed = '- an anchor path is accepted only if `ls-files -v` lists it with tag `H` and `status` is silent — everything else, a path reported modified, untracked or ignored, is the named refusal `anchor-dirty`';
|
|
278
|
+
|
|
279
|
+
it('refuses the measured clause with the exact message; verb alone, form alone and an in-clause closure pass', () => {
|
|
280
|
+
expect(openListErrors(open)).toEqual([{ rule: 'open-list', message: refusal }]);
|
|
281
|
+
for (const text of [closed, '- refuse one case', '- alpha, beta or gamma']) expect(openListErrors(text)).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('joins continuation lines, collapses whitespace and keeps an indented bullet in its top-level bullet', () => {
|
|
285
|
+
for (const text of ['- refuse alpha,\n beta or ignored', '- refuse alpha,\n - beta or ignored']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('ends a bullet at a blank, the next top-level bullet or every ATX heading; Contract prose is ignored', () => {
|
|
289
|
+
for (const tail of ['\n\nEverything else closes', '\n- Everything else closes', ...Array.from({ length: 6 }, (_, i) => `\n${'#'.repeat(i + 1)} Everything else closes`)]) {
|
|
290
|
+
expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
|
|
291
|
+
}
|
|
292
|
+
expect(openListErrors('refuse alpha, beta or gamma')).toEqual([]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('ends a bullet at indented and tab-separated ATX headings', () => {
|
|
296
|
+
for (const tail of ['\n ## Everything else closes', '\n#\tEverything else closes']) expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('starts a new empty top-level bullet before following prose', () => {
|
|
300
|
+
for (const tail of ['\n- \nEverything else closes', '\n-\nonly if closes']) expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('judges an open list in every Contract section', () => {
|
|
304
|
+
const duplicate = specDoc().replace('## Scenarios', '## Contract\n\n- refuse alpha, beta or gamma\n\n## Scenarios');
|
|
305
|
+
expect(rulesOf(readSpecDocument(duplicate, 'login.md')).includes('open-list')).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('splits clauses at punctuation plus markdown tails, but not inside backtick spans', () => {
|
|
309
|
+
for (const tail of ['.** ', '.` ', '.) ', '.] ', '; ']) expect(rulesOf(readSpecDocument(withContract(`- refuse alpha, beta or gamma${tail}Everything else closes`), 'login.md'))).toEqual(['open-list']);
|
|
310
|
+
for (const text of ['- refuse `alpha. beta`, gamma or delta', '- refuse `alpha; beta`, gamma or delta', '- refuse `alpha. beta, gamma or delta']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
|
|
311
|
+
expect(openListErrors('- refuse alpha``. beta, gamma or delta')).toEqual([]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('keeps span content in judgement, and a closure in a neighbouring clause cannot lift a refusal', () => {
|
|
315
|
+
expect(rulesOf(readSpecDocument(withContract('- `refuse` alpha, beta or gamma'), 'login.md'))).toEqual(['open-list']);
|
|
316
|
+
expect(openListErrors('- refuse alpha, beta or gamma with `everything else`')).toEqual([]);
|
|
317
|
+
expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma. Everything else closes'), 'login.md'))).toEqual(['open-list']);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('matches verbs, forms and closure tokens with the frozen case and boundary rules', () => {
|
|
321
|
+
for (const verb of ['refuse', 'refuses', 'refused', 'refusal', 'refusals', 'REFUSE', 'REFUSAL']) expect(rulesOf(readSpecDocument(withContract(`- ${verb} alpha, beta or gamma`), 'login.md'))).toEqual(['open-list']);
|
|
322
|
+
for (const text of ['- Refuse alpha, beta or gamma', '- refuse alpha, beta and gamma']) expect(openListErrors(text)).toEqual([]);
|
|
323
|
+
expect(rulesOf(readSpecDocument(withContract('- refuse alpha · beta'), 'login.md'))).toEqual(['open-list']);
|
|
324
|
+
for (const token of SPEC_SCHEMA.openList.closureTokens) expect(openListErrors(`- ${token.toUpperCase()}: refuse alpha, beta or gamma`)).toEqual([]);
|
|
325
|
+
for (const text of ['- refuse alpha, beta or gamma — none of these', '- refuse alpha, beta or gamma — closed-key', '- refuse alpha, beta or gamma — READ-ONLY', '- refuse alpha, beta or gamma — if only this', '- refuse alpha, beta or gamma — not only this']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
|
|
326
|
+
expect(openListErrors('- refuse alpha, beta or gamma — ONLY this')).toEqual([]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('judges every part bullet, never an index, and freezes every schema list and inner pair', () => {
|
|
330
|
+
expect(rulesOf(readSpecDocument(partDoc({ extra: `\n${open}\n` }), 'auth/login/sessions.md'))).toEqual(['open-list']);
|
|
331
|
+
expect(readSpecDocument(indexDoc({ children: [], preamble: `\n${open}\n` }), 'auth/index.md').errors).toEqual([]);
|
|
332
|
+
expect(SPEC_SCHEMA.openList).toEqual({ checkVerbs: ['refuse', 'refusal', 'REFUSE', 'REFUSAL'], enumerationForms: [[',', ' or '], [' · ']], closureTokens: ['only if', 'only when', 'the only', 'everything else', 'anything else', 'nothing else', 'every other', 'any other', 'otherwise', 'exactly', 'one of', 'closed'], emphaticToken: 'ONLY' });
|
|
333
|
+
expect([SPEC_SCHEMA.openList, SPEC_SCHEMA.openList.checkVerbs, SPEC_SCHEMA.openList.enumerationForms, ...SPEC_SCHEMA.openList.enumerationForms, SPEC_SCHEMA.openList.closureTokens].every(Object.isFrozen)).toBe(true);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
272
337
|
describe('classifyPath + the frozen constants', () => {
|
|
273
338
|
it('classifies the lexical path forms', () => {
|
|
274
339
|
expect(['src/', 'src/a.mjs', '../x', '/x', 'C:/x', 'a\\b', 'src/*.mjs'].map(classifyPath)).toEqual([
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_README": "Per-project orchestration config: the recipe used at each step (slot) of each named activity. Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, previews the change, and writes valid JSON for you. You can still hand-edit this file directly whenever you prefer; that option never goes away.
|
|
2
|
+
"_README": "Per-project orchestration config: the recipe used at each step (slot) of each named activity. Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, previews the change, and writes valid JSON for you. You can still hand-edit this file directly whenever you prefer; that option never goes away. Four activities are configured independently, and so is each slot within them: 'plan-authoring' (slots author, fold, review), 'plan-execution' (slots execute, review), 'routine' (slots carrier, parallel) and 'feedback-triage' (slot review). A slot's value is a recipe: a 'review' slot accepts solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize), or an explicit roster array such as [\"codex-review\", \"agy-review\", \"review-lens\"] in hand-edit form; an 'execute' slot accepts solo | delegated | subagent (you implement / a backend runs a bounded sub-task / a full-tool frontier subagent carries a bounded slice you verify); the carrier slots 'plan-authoring.author', 'plan-authoring.fold' and 'routine.carrier' accept solo | subagent. 'routine.parallel' is a flag rather than a recipe: it accepts on | off and decides whether file-disjoint subagent slices dispatch concurrently. Every slot seeded below is 'solo' — no execution backend required. A slot the seed leaves silent takes the computed default stated further down: 'feedback-triage.review' is reviewed as soon as a review backend is ready (solo until then); 'plan-authoring.author', 'plan-authoring.fold' and 'routine.carrier' stay 'solo', and 'routine.parallel' stays 'on'. Raise a slot to reviewed or council for a second opinion, or to delegated to hand off execution; those need an execution backend set up first. 'subagent' needs the executor vehicle placed in this project — the composition root's `agents` writer places it; without it the slot resolves to solo with the reason stated. Remove a slot's line, or a whole activity block (or run `set-recipe --unset <activity>.<slot>`), to fall back to the computed default: reviewed when a review backend is ready and otherwise solo for a review slot, solo for author, fold, execute and carrier, on for parallel. Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your environment. Strict JSON — no comments.",
|
|
3
3
|
"plan-authoring": {
|
|
4
4
|
"review": "solo"
|
|
5
5
|
},
|