@sabaiway/agent-workflow-memory 1.2.1 → 1.3.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 +37 -0
- package/SKILL.md +15 -10
- package/capability.json +1 -1
- package/package.json +4 -2
- package/references/templates/agent_rules.md +7 -0
- package/references/templates/orchestration.json +8 -3
- package/bin/install.test.mjs +0 -110
- package/scripts/package-content.test.mjs +0 -78
- package/scripts/stamp-takeover.test.mjs +0 -166
- package/scripts/standalone-bootstrap.test.mjs +0 -180
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ 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 `1.3.0`).
|
|
6
6
|
|
|
7
|
+
## 1.3.0 — Agent-writable config note; §2.5 Communication (memory)
|
|
8
|
+
|
|
9
|
+
A **feature** release. The seeded `docs/ai/orchestration.json` note now frames the config as
|
|
10
|
+
agent-writable (via the `set-recipe` writer) **and** still hand-editable — the old "never written for
|
|
11
|
+
you" wording is gone. The `agent_rules.md` substrate gains a **§2.5 Communication** self-review item
|
|
12
|
+
(deliver the artifact inline; lead with the result; large-artifact carve-out). The deployment-lineage
|
|
13
|
+
head stays **`1.3.0`** (no `docs/ai` structural change, no migration file); the npm package version is a
|
|
14
|
+
separate axis.
|
|
15
|
+
|
|
16
|
+
- **Seed-or-preserve only (unchanged contract).** This substrate still only **seeds** the config from its
|
|
17
|
+
template when missing and **preserves** an existing one byte-for-byte — a NEW deployment gets the updated
|
|
18
|
+
note from the seed. Refreshing the note *in place* on an already-deployed file is the **composition
|
|
19
|
+
root's** job (memory stays standalone and owns no cross-package refresh helper).
|
|
20
|
+
- **Template parity preserved** — the seeded `orchestration.json` stays byte-identical to the composition
|
|
21
|
+
root's fallback copy; the new note is identical across both.
|
|
22
|
+
- **Knows-nobody invariant intact** — the note + SKILL.md refer to the writer generically, never naming a
|
|
23
|
+
specific sibling skill.
|
|
24
|
+
|
|
25
|
+
## 1.2.2 — Strip the package's own tests from the npm tarball (memory)
|
|
26
|
+
|
|
27
|
+
Packaging only — no API/behaviour change; removed the package's own colocated tests from the
|
|
28
|
+
published tarball, deploy payload tests retained. The deployment-lineage head stays **`1.3.0`** (no
|
|
29
|
+
`docs/ai` structural change, no migration file). The npm package version is a separate axis.
|
|
30
|
+
|
|
31
|
+
- **`files[]` scoped negation.** Appended `!bin/*.test.mjs` and `!scripts/*.test.mjs` to the
|
|
32
|
+
package allowlist (npm ignores a root `.npmignore` when `files[]` is present, so negation entries
|
|
33
|
+
in `files[]` are the mechanism). Tarball **41 → 37 files**: 4 of the package's own colocated tests
|
|
34
|
+
no longer ship.
|
|
35
|
+
- **Deploy payload tests retained.** `references/scripts/*.test.mjs` are deployed into a consumer
|
|
36
|
+
repo's `scripts/`, so they still ship — `!scripts/*.test.mjs` does not cross `/` and never touches
|
|
37
|
+
`references/scripts/`. **Never broaden it to `!references/**`** — those tests are deploy payload.
|
|
38
|
+
- **Tarball guard.** `scripts/package-content.test.mjs` (dev-only) gains an `npm pack --dry-run
|
|
39
|
+
--json` invariant: no own-test leak, payload tests + runtime files present, exact file count
|
|
40
|
+
`=== 37`.
|
|
41
|
+
- Test files stay on disk; the gate + publish CI run them from the checkout, unchanged. This is a
|
|
42
|
+
tarball-only exclusion.
|
|
43
|
+
|
|
7
44
|
## 1.2.1 — Hidden-mode maintenance invariant (memory)
|
|
8
45
|
|
|
9
46
|
Patch: documentation only. The deployment-lineage head stays **`1.3.0`** (no `docs/ai` structural
|
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: '1.
|
|
6
|
+
version: '1.3.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-memory
|
|
@@ -47,7 +47,7 @@ What this substrate owns vs what it only points at. The methodology + orchestrat
|
|
|
47
47
|
| Deployment-lineage stamp | **memory** | `docs/ai/.memory-version` |
|
|
48
48
|
| Plan→Phase→Step vocabulary, lifecycle, `queue.md`, mandatory Cleanup | **methodology** (not this skill) | the empty `workflow:methodology` slot — filled by the composition root |
|
|
49
49
|
| Orchestration recipes (Solo / Reviewed / Council / Delegated) | **methodology engine** (not this skill) | the empty `workflow:orchestration` slot — filled by the composition root |
|
|
50
|
-
| Per-project recipe **CONFIG** (which recipe each activity/slot uses) | **memory** seeds an *editable default* | `docs/ai/orchestration.json` (hand-edited; the recipe **canon** + the slot **vocabulary** live in the engine / composition root, never here) |
|
|
50
|
+
| Per-project recipe **CONFIG** (which recipe each activity/slot uses) | **memory** seeds an *editable default* | `docs/ai/orchestration.json` (agent-writable via the composition root's `set-recipe` writer, or hand-edited; the recipe **canon** + the slot **vocabulary** live in the engine / composition root, never here) |
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -113,7 +113,8 @@ bootstrapping over a live system, but the user makes the final call.
|
|
|
113
113
|
10. **Stamp the deployment lineage.** Write the **deployment-lineage head** into
|
|
114
114
|
`docs/ai/.memory-version` (one semver line). The lineage head is **`1.3.0`** (the
|
|
115
115
|
`LINEAGE_HEAD` constant in `scripts/stamp-takeover.mjs`) — the shared `agent-workflow`
|
|
116
|
-
lineage, **
|
|
116
|
+
lineage, a **separate axis** from this package's npm version (the two may even coincide by
|
|
117
|
+
accident; see *Stamp = lineage head, not package version*). Use the atomic writer in
|
|
117
118
|
`scripts/stamp-takeover.mjs` (write-temp + rename).
|
|
118
119
|
11. **Report & ask.** Show `tree docs/ai/`, 2–3 lines on filled-vs-TODO, then **ask before
|
|
119
120
|
committing** — never auto-commit. **Exception — delegated mode (below): skip this gate.**
|
|
@@ -163,11 +164,14 @@ Fill strategy:
|
|
|
163
164
|
**behind** the never-downgrade STOP above. **Also stamp-independent (same gate, before the equal-head
|
|
164
165
|
short-circuit): ensure `docs/ai/orchestration.json`** — **create it from
|
|
165
166
|
`${CLAUDE_SKILL_DIR}/references/templates/orchestration.json` if missing**, **preserve it byte-for-byte
|
|
166
|
-
if it already exists** (a user may have edited it; never clobber it).
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
if it already exists** (a user may have edited it; never clobber it). The shipped template's `_README`
|
|
168
|
+
already frames the config as agent-writable (set it with the `set-recipe` writer) and still
|
|
169
|
+
hand-editable. (Refreshing that note in place on an existing file is the **composition root's** job on
|
|
170
|
+
its own reconcile — this substrate is standalone and only seeds-or-preserves; it owns no cross-package
|
|
171
|
+
refresh helper.) This is why an equal-head (`1.3.0`) deployment still gains the config seed **without a
|
|
172
|
+
lineage-head bump or a migration file** (the stamp-independent-reconcile precedent — like the pointer
|
|
173
|
+
slots + the hidden-mode footprint). **Then**, if the stamp **equals** the head → report "up to date"
|
|
174
|
+
(plus any footprint move / config seed just made) and stop.
|
|
171
175
|
3. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` context (entries newer than the stamp).
|
|
172
176
|
4. Apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those
|
|
173
177
|
newer than the stamp. Migrations are **idempotent**.
|
|
@@ -192,8 +196,9 @@ Fill strategy:
|
|
|
192
196
|
|
|
193
197
|
- **Source vs target directory.** Templates/scripts are read from the skill's own dir; the
|
|
194
198
|
**working directory is the target project** — never write substrate files back into the skill.
|
|
195
|
-
- **Stamp = lineage head, not package version.** `.memory-version` carries
|
|
196
|
-
`agent-workflow` lineage)
|
|
199
|
+
- **Stamp = lineage head, not package version.** `.memory-version` carries the **deployment-lineage
|
|
200
|
+
head** (`1.3.0`, the shared `agent-workflow` lineage) — a **separate axis** from this package's npm
|
|
201
|
+
version (the two may even coincide by accident). They move independently.
|
|
197
202
|
- **Both pointer slots ship empty and stay the user's.** Never author methodology or orchestration
|
|
198
203
|
text into them; on upgrade, preserve their content byte-for-byte. The composition root is their only
|
|
199
204
|
writer.
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
"capability.json",
|
|
43
43
|
"references/",
|
|
44
44
|
"scripts/",
|
|
45
|
-
"migrations/"
|
|
45
|
+
"migrations/",
|
|
46
|
+
"!bin/*.test.mjs",
|
|
47
|
+
"!scripts/*.test.mjs"
|
|
46
48
|
],
|
|
47
49
|
"engines": {
|
|
48
50
|
"node": ">=18"
|
|
@@ -63,6 +63,13 @@ Before proposing changes or committing, review against:
|
|
|
63
63
|
### 2.4. Quality Gates
|
|
64
64
|
- Always run type-checker, linter, and all tests before committing.
|
|
65
65
|
|
|
66
|
+
### 2.5. Communication (user-facing messages)
|
|
67
|
+
Apply this as part of §2 before any user-facing summary:
|
|
68
|
+
- **Deliver the artifact IN the message** — paste the prompt / diff / version / command inline; never "see §X / open the file / run it and you'll see" as a *substitute* for showing what was asked.
|
|
69
|
+
- **Lead with the result**, then the details; show exactly what was asked — no deflection, no "almost done" when the ask was the finished thing.
|
|
70
|
+
- **No condescension, no filler.** Own a miss plainly and fix it in the same message.
|
|
71
|
+
- **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.
|
|
72
|
+
|
|
66
73
|
---
|
|
67
74
|
|
|
68
75
|
## 3. Token & Session Optimization
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_README": "Per-project orchestration config: the recipe used at each step (slot) of each named activity.
|
|
3
|
-
"plan-authoring": {
|
|
4
|
-
|
|
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. Each activity is configured independently (e.g. plan-authoring, plan-execution), and so is each slot within it. 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); an 'execute' slot accepts solo | delegated (you implement / a backend runs a bounded sub-task). The default below is 'solo' everywhere — no execution backend required. 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. Remove a slot's line (or run `set-recipe --unset <activity>.<slot>`) to fall back to the computed default (reviewed when a review backend is ready, otherwise solo). Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your environment. Strict JSON — no comments.",
|
|
3
|
+
"plan-authoring": {
|
|
4
|
+
"review": "solo"
|
|
5
|
+
},
|
|
6
|
+
"plan-execution": {
|
|
7
|
+
"execute": "solo",
|
|
8
|
+
"review": "solo"
|
|
9
|
+
}
|
|
5
10
|
}
|
package/bin/install.test.mjs
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { mkdtemp, rm, mkdir, symlink, readdir } from 'node:fs/promises';
|
|
5
|
-
import { existsSync } from 'node:fs';
|
|
6
|
-
import { tmpdir, homedir } from 'node:os';
|
|
7
|
-
import { dirname, join, sep } from 'node:path';
|
|
8
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
-
import { assertContainedRealPath, tildify } from './install.mjs';
|
|
10
|
-
|
|
11
|
-
const INSTALLER = join(dirname(fileURLToPath(import.meta.url)), 'install.mjs');
|
|
12
|
-
const runInstaller = (target) => spawnSync(process.execPath, [INSTALLER, '--dir', target], { encoding: 'utf8' });
|
|
13
|
-
|
|
14
|
-
describe('memory installer — payload + symlink-traversal hardening', () => {
|
|
15
|
-
let dir;
|
|
16
|
-
beforeEach(async () => {
|
|
17
|
-
dir = await mkdtemp(join(tmpdir(), 'aw-memory-install-'));
|
|
18
|
-
});
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
await rm(dir, { recursive: true, force: true });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('installs the full enumerated payload (capability.json, LICENSE, stamp-takeover.mjs) and not the npm wrapper', () => {
|
|
24
|
-
const target = join(dir, 'agent-workflow-memory');
|
|
25
|
-
const res = runInstaller(target);
|
|
26
|
-
assert.equal(res.status, 0, res.stderr);
|
|
27
|
-
for (const f of [
|
|
28
|
-
'SKILL.md',
|
|
29
|
-
'capability.json',
|
|
30
|
-
'LICENSE',
|
|
31
|
-
'README.md',
|
|
32
|
-
'CHANGELOG.md',
|
|
33
|
-
'scripts/stamp-takeover.mjs',
|
|
34
|
-
'references/contracts.md',
|
|
35
|
-
'references/templates/AGENTS.md',
|
|
36
|
-
'references/templates/orchestration.json',
|
|
37
|
-
'migrations/legacy-stamp-takeover.md',
|
|
38
|
-
]) {
|
|
39
|
-
assert.ok(existsSync(join(target, f)), `missing installed entry: ${f}`);
|
|
40
|
-
}
|
|
41
|
-
assert.equal(existsSync(join(target, 'bin/install.mjs')), false, 'the npm wrapper must not be copied into the skill dir');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('refuses to write through a symlinked INTERMEDIATE dest component (no leak)', async () => {
|
|
45
|
-
const target = join(dir, 'target');
|
|
46
|
-
const evil = join(dir, 'evil');
|
|
47
|
-
await mkdir(target, { recursive: true });
|
|
48
|
-
await mkdir(evil, { recursive: true });
|
|
49
|
-
await symlink(evil, join(target, 'references'));
|
|
50
|
-
const res = runInstaller(target);
|
|
51
|
-
assert.notEqual(res.status, 0);
|
|
52
|
-
assert.match(res.stderr, /symlink/i);
|
|
53
|
-
assert.deepEqual(await readdir(evil), [], 'nothing should be written through the symlink');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('refuses a DANGLING destination symlink (existsSync would have followed it to absent)', async () => {
|
|
57
|
-
const target = join(dir, 'target');
|
|
58
|
-
await mkdir(target, { recursive: true });
|
|
59
|
-
// references -> a path that does not exist (dangling). copyFile through it would create
|
|
60
|
-
// the file at the dangling target, outside `target`.
|
|
61
|
-
await symlink(join(dir, 'nowhere'), join(target, 'references'));
|
|
62
|
-
const res = runInstaller(target);
|
|
63
|
-
assert.notEqual(res.status, 0);
|
|
64
|
-
assert.match(res.stderr, /symlink/i);
|
|
65
|
-
assert.equal(existsSync(join(dir, 'nowhere')), false, 'dangling target must not be materialised');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('refuses a symlinked TARGET root (no leak)', async () => {
|
|
69
|
-
const real = join(dir, 'real');
|
|
70
|
-
const root = join(dir, 'root');
|
|
71
|
-
await mkdir(real, { recursive: true });
|
|
72
|
-
await symlink(real, root);
|
|
73
|
-
const res = runInstaller(root);
|
|
74
|
-
assert.notEqual(res.status, 0);
|
|
75
|
-
assert.match(res.stderr, /symlink/i);
|
|
76
|
-
assert.deepEqual(await readdir(real), [], 'nothing should be written through the symlinked target');
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('memory installer — Issue-004 regressions (exported helpers, in-process)', () => {
|
|
81
|
-
it('assertContainedRealPath accepts a contained child literally named "..foo"', () => {
|
|
82
|
-
const root = join(tmpdir(), 'aw-mem-contain-root');
|
|
83
|
-
assert.doesNotThrow(() => assertContainedRealPath(root, join(root, '..foo')));
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('assertContainedRealPath rejects a true `../` escape', () => {
|
|
87
|
-
const root = join(tmpdir(), 'aw-mem-contain-root');
|
|
88
|
-
assert.throws(() => assertContainedRealPath(root, join(root, '..', 'escape')), /outside the target dir/);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('tildify collapses only a LEADING homedir, never a mid-path occurrence', () => {
|
|
92
|
-
assert.equal(tildify(`${homedir()}${sep}skills${sep}x`), `~${sep}skills${sep}x`, 'leading home → ~');
|
|
93
|
-
const midPath = `${sep}tmp${homedir()}${sep}x`;
|
|
94
|
-
assert.equal(tildify(midPath), midPath, 'a mid-path home occurrence is left untouched');
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('memory installer — module hygiene', () => {
|
|
99
|
-
it('importing install.mjs runs nothing (main() is guarded by isDirectRun)', () => {
|
|
100
|
-
const url = JSON.stringify(pathToFileURL(INSTALLER).href);
|
|
101
|
-
const res = spawnSync(
|
|
102
|
-
process.execPath,
|
|
103
|
-
['--input-type=module', '-e', `import(${url}).then(() => console.log('IMPORT_OK'));`],
|
|
104
|
-
{ encoding: 'utf8' },
|
|
105
|
-
);
|
|
106
|
-
assert.equal(res.status, 0, res.stderr);
|
|
107
|
-
assert.match(res.stdout, /IMPORT_OK/);
|
|
108
|
-
assert.doesNotMatch(res.stdout, /installed v|updated the substrate/);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
-
import { dirname, join, basename } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
// The memory substrate must "know nobody": it ships ONLY its own capability.json and NO
|
|
8
|
-
// family-wide tooling (schema/validator/injection/scanner — those are owned by the composition
|
|
9
|
-
// root). This guards the DAG at the package-content level.
|
|
10
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
-
|
|
12
|
-
const FORBIDDEN_FILES = new Set([
|
|
13
|
-
'validate.mjs',
|
|
14
|
-
'schema.md',
|
|
15
|
-
'inject-methodology.mjs',
|
|
16
|
-
'methodology-slot.md',
|
|
17
|
-
'release-scan.mjs',
|
|
18
|
-
]);
|
|
19
|
-
const FORBIDDEN_DIRS = new Set(['tools', 'manifest']);
|
|
20
|
-
|
|
21
|
-
const walk = (dir, acc = []) => {
|
|
22
|
-
for (const entry of readdirSync(dir)) {
|
|
23
|
-
if (entry === 'node_modules' || entry === '.git') continue;
|
|
24
|
-
const full = join(dir, entry);
|
|
25
|
-
if (statSync(full).isDirectory()) {
|
|
26
|
-
acc.push({ kind: 'dir', name: entry, full });
|
|
27
|
-
walk(full, acc);
|
|
28
|
-
} else {
|
|
29
|
-
acc.push({ kind: 'file', name: entry, full });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return acc;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
describe('memory package content — DAG guard (knows nobody)', () => {
|
|
36
|
-
const entries = walk(ROOT);
|
|
37
|
-
|
|
38
|
-
it('ships no family-wide tooling files', () => {
|
|
39
|
-
const leaks = entries.filter((e) => e.kind === 'file' && FORBIDDEN_FILES.has(e.name));
|
|
40
|
-
assert.deepEqual(leaks.map((e) => e.full), [], 'family tooling must not ship in the memory package');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('has no tools/ or manifest/ directory', () => {
|
|
44
|
-
const leaks = entries.filter((e) => e.kind === 'dir' && FORBIDDEN_DIRS.has(e.name));
|
|
45
|
-
assert.deepEqual(leaks.map((e) => e.full), []);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('package.json files[] does not enumerate any family tooling', () => {
|
|
49
|
-
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
|
|
50
|
-
const offending = (pkg.files ?? []).filter((f) => /tools|manifest|validate|schema\.md|inject-methodology|release-scan/.test(f));
|
|
51
|
-
assert.deepEqual(offending, []);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('does ship its own capability.json (the only manifest it owns)', () => {
|
|
55
|
-
assert.ok(entries.some((e) => e.kind === 'file' && e.name === 'capability.json' && basename(dirname(e.full)) === basename(ROOT)));
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('no source file names a specific sibling skill (knows nobody — generic "composition root" only)', () => {
|
|
59
|
-
// Built from fragments so the literal sibling names never appear in THIS test's own source
|
|
60
|
-
// (otherwise the walk below would flag this file).
|
|
61
|
-
const d = '-';
|
|
62
|
-
const SIBLINGS = [
|
|
63
|
-
['agent', 'workflow', 'kit'].join(d),
|
|
64
|
-
['codex', 'cli', 'bridge'].join(d),
|
|
65
|
-
['antigravity', 'cli', 'bridge'].join(d),
|
|
66
|
-
['agent', 'workflow', 'engine'].join(d),
|
|
67
|
-
];
|
|
68
|
-
const offenders = [];
|
|
69
|
-
for (const e of entries) {
|
|
70
|
-
if (e.kind !== 'file' || !/\.(md|mjs|json|sh|ya?ml)$/.test(e.name)) continue;
|
|
71
|
-
const text = readFileSync(e.full, 'utf8');
|
|
72
|
-
for (const sibling of SIBLINGS) {
|
|
73
|
-
if (text.includes(sibling)) offenders.push(`${e.full} → "${sibling}"`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
assert.deepEqual(offenders, [], 'memory must reference only the generic composition root, never a named sibling');
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtemp, rm, writeFile, readFile, readdir } from 'node:fs/promises';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import {
|
|
8
|
-
LINEAGE_HEAD,
|
|
9
|
-
parseSemver,
|
|
10
|
-
compareSemver,
|
|
11
|
-
decideTakeover,
|
|
12
|
-
selectMigrations,
|
|
13
|
-
writeStampAtomic,
|
|
14
|
-
applyTakeover,
|
|
15
|
-
readStamp,
|
|
16
|
-
} from './stamp-takeover.mjs';
|
|
17
|
-
|
|
18
|
-
describe('LINEAGE_HEAD', () => {
|
|
19
|
-
it('is the shared deployment-lineage head (not this package version)', () => {
|
|
20
|
-
assert.equal(LINEAGE_HEAD, '1.3.0');
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('parseSemver / compareSemver', () => {
|
|
25
|
-
it('parses plain and v-prefixed semver, rejects junk', () => {
|
|
26
|
-
assert.deepEqual(parseSemver('1.3.0'), [1, 3, 0]);
|
|
27
|
-
assert.deepEqual(parseSemver(' v2.0.1 '), [2, 0, 1]);
|
|
28
|
-
assert.equal(parseSemver('1.3'), null);
|
|
29
|
-
assert.equal(parseSemver('next'), null);
|
|
30
|
-
assert.equal(parseSemver(null), null);
|
|
31
|
-
});
|
|
32
|
-
it('orders versions and returns null on unparseable input', () => {
|
|
33
|
-
assert.equal(compareSemver('1.2.0', '1.3.0'), -1);
|
|
34
|
-
assert.equal(compareSemver('1.3.0', '1.3.0'), 0);
|
|
35
|
-
assert.equal(compareSemver('1.4.0', '1.3.0'), 1);
|
|
36
|
-
assert.equal(compareSemver('x', '1.0.0'), null);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('decideTakeover — §1.5 state table (pure, per row)', () => {
|
|
41
|
-
it('only .workflow-version=V → copy V verbatim → .memory-version; migrate from V', () => {
|
|
42
|
-
const d = decideTakeover({ memoryVersion: null, workflowVersion: '1.2.0' });
|
|
43
|
-
assert.equal(d.status, 'ok');
|
|
44
|
-
assert.equal(d.writeMemoryVersion, '1.2.0'); // exact stamp value asserted
|
|
45
|
-
assert.equal(d.migrateFrom, '1.2.0');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('both stamps → no takeover write; migrate from .memory-version', () => {
|
|
49
|
-
const d = decideTakeover({ memoryVersion: '1.3.0', workflowVersion: '1.1.0' });
|
|
50
|
-
assert.equal(d.status, 'ok');
|
|
51
|
-
assert.equal(d.writeMemoryVersion, null);
|
|
52
|
-
assert.equal(d.migrateFrom, '1.3.0');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('only .memory-version → migrate from it, no write', () => {
|
|
56
|
-
const d = decideTakeover({ memoryVersion: '1.1.0', workflowVersion: null });
|
|
57
|
-
assert.equal(d.status, 'ok');
|
|
58
|
-
assert.equal(d.writeMemoryVersion, null);
|
|
59
|
-
assert.equal(d.migrateFrom, '1.1.0');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('no stamp → conservative re-bootstrap', () => {
|
|
63
|
-
const d = decideTakeover({ memoryVersion: null, workflowVersion: null });
|
|
64
|
-
assert.equal(d.status, 'rebootstrap');
|
|
65
|
-
assert.equal(d.writeMemoryVersion, null);
|
|
66
|
-
assert.equal(d.migrateFrom, null);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('unparseable .memory-version → STOP', () => {
|
|
70
|
-
const d = decideTakeover({ memoryVersion: 'garbage', workflowVersion: '1.2.0' });
|
|
71
|
-
assert.equal(d.status, 'stop');
|
|
72
|
-
assert.equal(d.writeMemoryVersion, null);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('PRESENT but empty/whitespace stamp → STOP (not absent → rebootstrap)', () => {
|
|
76
|
-
assert.equal(decideTakeover({ memoryVersion: null, workflowVersion: '' }).status, 'stop');
|
|
77
|
-
assert.equal(decideTakeover({ memoryVersion: ' ', workflowVersion: null }).status, 'stop');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('unparseable .workflow-version → STOP', () => {
|
|
81
|
-
const d = decideTakeover({ memoryVersion: null, workflowVersion: 'not-a-version' });
|
|
82
|
-
assert.equal(d.status, 'stop');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('future .memory-version (> head) → STOP', () => {
|
|
86
|
-
const d = decideTakeover({ memoryVersion: '2.0.0', workflowVersion: null });
|
|
87
|
-
assert.equal(d.status, 'stop');
|
|
88
|
-
assert.match(d.note, /newer than the lineage head/);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('future .workflow-version (> head) → STOP', () => {
|
|
92
|
-
const d = decideTakeover({ memoryVersion: null, workflowVersion: '9.9.9' });
|
|
93
|
-
assert.equal(d.status, 'stop');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('workflow exactly at head → takeover writes the head', () => {
|
|
97
|
-
const d = decideTakeover({ memoryVersion: null, workflowVersion: '1.3.0' });
|
|
98
|
-
assert.equal(d.status, 'ok');
|
|
99
|
-
assert.equal(d.writeMemoryVersion, '1.3.0');
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('selectMigrations', () => {
|
|
104
|
-
const available = ['1.1.0', '1.2.0', '1.3.0'];
|
|
105
|
-
it('selects strictly-newer migrations up to the head, ascending', () => {
|
|
106
|
-
assert.deepEqual(selectMigrations('1.0.0', available), ['1.1.0', '1.2.0', '1.3.0']);
|
|
107
|
-
assert.deepEqual(selectMigrations('1.2.0', available), ['1.3.0']);
|
|
108
|
-
assert.deepEqual(selectMigrations('1.3.0', available), []);
|
|
109
|
-
});
|
|
110
|
-
it('null migrateFrom selects all ≤ head; excludes future migrations', () => {
|
|
111
|
-
assert.deepEqual(selectMigrations(null, [...available, '1.4.0']), ['1.1.0', '1.2.0', '1.3.0']);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe('applyTakeover — atomic, idempotent, integration (temp fs)', () => {
|
|
116
|
-
let dir;
|
|
117
|
-
beforeEach(async () => {
|
|
118
|
-
dir = await mkdtemp(join(tmpdir(), 'stamp-takeover-'));
|
|
119
|
-
});
|
|
120
|
-
afterEach(async () => {
|
|
121
|
-
await rm(dir, { recursive: true, force: true });
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('legacy-only: writes .memory-version verbatim and preserves .workflow-version', async () => {
|
|
125
|
-
await writeFile(join(dir, '.workflow-version'), '1.2.0\n', 'utf8');
|
|
126
|
-
const d = await applyTakeover(dir);
|
|
127
|
-
assert.equal(d.writeMemoryVersion, '1.2.0');
|
|
128
|
-
assert.equal(await readStamp(join(dir, '.memory-version')), '1.2.0');
|
|
129
|
-
assert.equal(await readStamp(join(dir, '.workflow-version')), '1.2.0'); // never deleted
|
|
130
|
-
// single trailing newline, no leftover temp files
|
|
131
|
-
assert.equal(await readFile(join(dir, '.memory-version'), 'utf8'), '1.2.0\n');
|
|
132
|
-
assert.equal((await readdir(dir)).filter((f) => f.includes('.tmp-')).length, 0);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('is idempotent — a second run does not rewrite the stamp', async () => {
|
|
136
|
-
await writeFile(join(dir, '.workflow-version'), '1.2.0\n', 'utf8');
|
|
137
|
-
await applyTakeover(dir);
|
|
138
|
-
const second = await applyTakeover(dir);
|
|
139
|
-
assert.equal(second.writeMemoryVersion, null); // memory now present → no write
|
|
140
|
-
assert.equal(second.migrateFrom, '1.2.0');
|
|
141
|
-
assert.equal(await readStamp(join(dir, '.memory-version')), '1.2.0');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('both stamps present: leaves .memory-version untouched', async () => {
|
|
145
|
-
await writeFile(join(dir, '.memory-version'), '1.3.0\n', 'utf8');
|
|
146
|
-
await writeFile(join(dir, '.workflow-version'), '1.1.0\n', 'utf8');
|
|
147
|
-
const d = await applyTakeover(dir);
|
|
148
|
-
assert.equal(d.writeMemoryVersion, null);
|
|
149
|
-
assert.equal(await readStamp(join(dir, '.memory-version')), '1.3.0');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('STOP state performs no write (future stamp leaves prior state intact)', async () => {
|
|
153
|
-
await writeFile(join(dir, '.workflow-version'), '5.0.0\n', 'utf8');
|
|
154
|
-
const d = await applyTakeover(dir);
|
|
155
|
-
assert.equal(d.status, 'stop');
|
|
156
|
-
assert.equal(existsSync(join(dir, '.memory-version')), false); // nothing written
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('writeStampAtomic overwrites cleanly with a single trailing newline', async () => {
|
|
160
|
-
const target = join(dir, '.memory-version');
|
|
161
|
-
await writeStampAtomic(target, '1.1.0');
|
|
162
|
-
await writeStampAtomic(target, '1.3.0');
|
|
163
|
-
assert.equal(await readFile(target, 'utf8'), '1.3.0\n');
|
|
164
|
-
assert.equal((await readdir(dir)).filter((f) => f.includes('.tmp-')).length, 0);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
// End-to-end acceptance: a standalone substrate bootstrap into a fresh project.
|
|
2
|
-
//
|
|
3
|
-
// The unit suites cover each module in isolation (the stamp state machine, the atomic
|
|
4
|
-
// writer, the hook installer). This drives the documented bootstrap WRITE steps end to end
|
|
5
|
-
// over a real temp project + a real git checkout, then asserts the deployed artifact set:
|
|
6
|
-
// docs/ai + entry point exist, the pre-commit hook is installed, the deployment-lineage
|
|
7
|
-
// stamp is .memory-version (the lineage head) ONLY, and the methodology slot ships empty.
|
|
8
|
-
//
|
|
9
|
-
// Standalone here means substrate-only: nothing fills the slot, so it stays empty and no
|
|
10
|
-
// second (composition-root) stamp appears. The composition path is covered separately.
|
|
11
|
-
|
|
12
|
-
import { describe, it, afterEach } from 'node:test';
|
|
13
|
-
import assert from 'node:assert/strict';
|
|
14
|
-
import {
|
|
15
|
-
mkdtempSync,
|
|
16
|
-
mkdirSync,
|
|
17
|
-
rmSync,
|
|
18
|
-
cpSync,
|
|
19
|
-
readdirSync,
|
|
20
|
-
readFileSync,
|
|
21
|
-
writeFileSync,
|
|
22
|
-
existsSync,
|
|
23
|
-
symlinkSync,
|
|
24
|
-
} from 'node:fs';
|
|
25
|
-
import { tmpdir } from 'node:os';
|
|
26
|
-
import { join, resolve, dirname } from 'node:path';
|
|
27
|
-
import { fileURLToPath } from 'node:url';
|
|
28
|
-
import { execFileSync } from 'node:child_process';
|
|
29
|
-
import { writeStampAtomic, LINEAGE_HEAD } from './stamp-takeover.mjs';
|
|
30
|
-
|
|
31
|
-
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
-
const TEMPLATES = join(SKILL_ROOT, 'references', 'templates');
|
|
33
|
-
const ENFORCEMENT = join(SKILL_ROOT, 'references', 'scripts');
|
|
34
|
-
|
|
35
|
-
// The empty delimited slot the substrate ships in its entry point. Hard-coded (not imported
|
|
36
|
-
// from the composition root) so this substrate test stays self-contained and dependency-free.
|
|
37
|
-
const SLOT_START = '<!-- workflow:methodology:start -->';
|
|
38
|
-
const SLOT_END = '<!-- workflow:methodology:end -->';
|
|
39
|
-
const ORCH_START = '<!-- workflow:orchestration:start -->';
|
|
40
|
-
const ORCH_END = '<!-- workflow:orchestration:end -->';
|
|
41
|
-
// The deployed AGENTS.md line budget the composition root fills BOTH pointers inside (D-CAP). A
|
|
42
|
-
// representative single-line fragment per slot models what the composition root injects.
|
|
43
|
-
const AGENTS_MD_CAP = 100;
|
|
44
|
-
const lineCount = (text) => text.split('\n').length - (text.endsWith('\n') ? 1 : 0);
|
|
45
|
-
const extractPair = (text, start, end) => {
|
|
46
|
-
const a = text.indexOf(start);
|
|
47
|
-
const b = text.indexOf(end);
|
|
48
|
-
return a !== -1 && b !== -1 && b > a ? text.slice(a + start.length, b) : null;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const tempDirs = [];
|
|
52
|
-
const makeProject = () => {
|
|
53
|
-
const dir = mkdtempSync(join(tmpdir(), 'substrate-bootstrap-'));
|
|
54
|
-
tempDirs.push(dir);
|
|
55
|
-
return dir;
|
|
56
|
-
};
|
|
57
|
-
afterEach(() => {
|
|
58
|
-
while (tempDirs.length) rmSync(tempDirs.pop(), { recursive: true, force: true });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Mirrors the bootstrap WRITE steps: entry point at the root (+ tool alias symlink), every
|
|
62
|
-
// other template under docs/ai, enforcement scripts copied in, the hook installed against a
|
|
63
|
-
// real checkout, and the lineage head stamped atomically. Returns the docs/ai dir.
|
|
64
|
-
const bootstrap = (project) => {
|
|
65
|
-
cpSync(join(TEMPLATES, 'AGENTS.md'), join(project, 'AGENTS.md'));
|
|
66
|
-
symlinkSync('AGENTS.md', join(project, 'CLAUDE.md'));
|
|
67
|
-
|
|
68
|
-
const docsAi = join(project, 'docs', 'ai');
|
|
69
|
-
mkdirSync(docsAi, { recursive: true });
|
|
70
|
-
for (const entry of readdirSync(TEMPLATES)) {
|
|
71
|
-
if (entry === 'AGENTS.md') continue;
|
|
72
|
-
cpSync(join(TEMPLATES, entry), join(docsAi, entry), { recursive: true });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const projectScripts = join(project, 'scripts');
|
|
76
|
-
mkdirSync(projectScripts, { recursive: true });
|
|
77
|
-
cpSync(ENFORCEMENT, projectScripts, { recursive: true });
|
|
78
|
-
|
|
79
|
-
// The hook installer resolves its target from its OWN location, so run the copied-in copy
|
|
80
|
-
// (project/scripts/...) to install into project/.git/hooks — exactly as the procedure does.
|
|
81
|
-
execFileSync('git', ['init', '-q'], { cwd: project });
|
|
82
|
-
execFileSync(process.execPath, [join(projectScripts, 'install-git-hooks.mjs')], {
|
|
83
|
-
cwd: project,
|
|
84
|
-
stdio: 'pipe',
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
return docsAi;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
describe('standalone substrate bootstrap (end-to-end, real temp project)', () => {
|
|
91
|
-
it('deploys docs/ai + entry point and installs the pre-commit hook', () => {
|
|
92
|
-
const project = makeProject();
|
|
93
|
-
const docsAi = bootstrap(project);
|
|
94
|
-
|
|
95
|
-
assert.ok(existsSync(join(project, 'AGENTS.md')), 'entry point exists');
|
|
96
|
-
assert.ok(existsSync(docsAi), 'docs/ai exists');
|
|
97
|
-
assert.ok(existsSync(join(docsAi, 'agent_rules.md')), 'a representative docs/ai file landed');
|
|
98
|
-
|
|
99
|
-
const hook = join(project, '.git', 'hooks', 'pre-commit');
|
|
100
|
-
assert.ok(existsSync(hook), 'pre-commit hook installed');
|
|
101
|
-
assert.match(readFileSync(hook, 'utf8'), /install-git-hooks\.mjs/, 'hook carries the installer marker');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('stamps .memory-version (lineage head) ONLY — no second stamp', async () => {
|
|
105
|
-
const project = makeProject();
|
|
106
|
-
const docsAi = bootstrap(project);
|
|
107
|
-
await writeStampAtomic(join(docsAi, '.memory-version'), LINEAGE_HEAD);
|
|
108
|
-
|
|
109
|
-
assert.equal(readFileSync(join(docsAi, '.memory-version'), 'utf8').trim(), LINEAGE_HEAD);
|
|
110
|
-
assert.ok(!existsSync(join(docsAi, '.workflow-version')), 'no composition-root stamp in a standalone bootstrap');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('ships BOTH pointer slots present-but-empty (methodology + orchestration)', () => {
|
|
114
|
-
const project = makeProject();
|
|
115
|
-
bootstrap(project);
|
|
116
|
-
|
|
117
|
-
const entry = readFileSync(join(project, 'AGENTS.md'), 'utf8');
|
|
118
|
-
const meth = extractPair(entry, SLOT_START, SLOT_END);
|
|
119
|
-
const orch = extractPair(entry, ORCH_START, ORCH_END);
|
|
120
|
-
assert.notEqual(meth, null, 'an ordered methodology marker pair is present');
|
|
121
|
-
assert.equal(meth.trim(), '', 'the methodology slot is empty as shipped');
|
|
122
|
-
assert.notEqual(orch, null, 'an ordered orchestration marker pair is present');
|
|
123
|
-
assert.equal(orch.trim(), '', 'the orchestration slot is empty as shipped');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('seeds docs/ai/orchestration.json from the template (the bootstrap loop deploys it)', () => {
|
|
127
|
-
const project = makeProject();
|
|
128
|
-
const docsAi = bootstrap(project);
|
|
129
|
-
const seeded = join(docsAi, 'orchestration.json');
|
|
130
|
-
assert.ok(existsSync(seeded), 'the orchestration.json config is seeded into docs/ai');
|
|
131
|
-
assert.equal(
|
|
132
|
-
readFileSync(seeded, 'utf8'),
|
|
133
|
-
readFileSync(join(TEMPLATES, 'orchestration.json'), 'utf8'),
|
|
134
|
-
'the seeded config is byte-identical to the template',
|
|
135
|
-
);
|
|
136
|
-
// strict JSON valid + the conservative all-solo default the maintainer chose.
|
|
137
|
-
const config = JSON.parse(readFileSync(seeded, 'utf8'));
|
|
138
|
-
assert.equal(typeof config._README, 'string', 'an onboarding _README is present');
|
|
139
|
-
assert.equal(config['plan-authoring'].review, 'solo', 'default review recipe is solo');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// The stamp-independent upgrade "ensure" (SKILL.md upgrade step 2): create-if-missing /
|
|
143
|
-
// preserve-if-edited. Modeled here the way the documented prose performs it — so an equal-head
|
|
144
|
-
// re-run never clobbers a user's edited config, and a deleted one is re-seeded.
|
|
145
|
-
it('the upgrade ensure preserves an edited config and re-creates a deleted one', () => {
|
|
146
|
-
const project = makeProject();
|
|
147
|
-
const docsAi = bootstrap(project);
|
|
148
|
-
const dest = join(docsAi, 'orchestration.json');
|
|
149
|
-
const ensureConfig = () => {
|
|
150
|
-
if (!existsSync(dest)) cpSync(join(TEMPLATES, 'orchestration.json'), dest);
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// A user edits the deployed config.
|
|
154
|
-
writeFileSync(dest, '{ "plan-authoring": { "review": "council" } }\n');
|
|
155
|
-
ensureConfig(); // an equal-head upgrade re-runs the ensure
|
|
156
|
-
assert.match(readFileSync(dest, 'utf8'), /council/, 'an edited config is preserved (never clobbered)');
|
|
157
|
-
|
|
158
|
-
// A missing config is re-seeded.
|
|
159
|
-
rmSync(dest);
|
|
160
|
-
ensureConfig();
|
|
161
|
-
assert.ok(existsSync(dest), 'a missing config is re-created from the template');
|
|
162
|
-
assert.equal(readFileSync(dest, 'utf8'), readFileSync(join(TEMPLATES, 'orchestration.json'), 'utf8'));
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('stays ≤ the cap when the composition root fills BOTH pointer slots (D-CAP headroom)', () => {
|
|
166
|
-
const project = makeProject();
|
|
167
|
-
bootstrap(project);
|
|
168
|
-
const entry = readFileSync(join(project, 'AGENTS.md'), 'utf8');
|
|
169
|
-
assert.ok(lineCount(entry) <= AGENTS_MD_CAP, `shipped (empty) AGENTS.md is ${lineCount(entry)} lines (cap ${AGENTS_MD_CAP})`);
|
|
170
|
-
// Fill each one-line pointer the way the composition root does (replace the empty body), then re-count.
|
|
171
|
-
const fill = (text, start, end, body) => {
|
|
172
|
-
const a = text.indexOf(start);
|
|
173
|
-
const b = text.indexOf(end);
|
|
174
|
-
return `${text.slice(0, a + start.length)}\n${body}\n${text.slice(b)}`;
|
|
175
|
-
};
|
|
176
|
-
let filled = fill(entry, SLOT_START, SLOT_END, '> methodology pointer (one line)');
|
|
177
|
-
filled = fill(filled, ORCH_START, ORCH_END, '> orchestration recipes pointer (one line)');
|
|
178
|
-
assert.ok(lineCount(filled) <= AGENTS_MD_CAP, `dual-filled AGENTS.md is ${lineCount(filled)} lines (cap ${AGENTS_MD_CAP})`);
|
|
179
|
-
});
|
|
180
|
-
});
|