@sabaiway/agent-workflow-memory 8.3.0 → 8.4.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,22 @@ 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.4.0 — the commit hook runs your own scripts/pre-commit.local first (AD-195)
|
|
8
|
+
|
|
9
|
+
**Your own commit checks now have a place of their own: `scripts/pre-commit.local`.** Every hook that
|
|
10
|
+
`references/scripts/install-git-hooks.mjs` writes carries, right after `set -e`, a comment line naming that file and
|
|
11
|
+
`if [ -f scripts/pre-commit.local ]; then bash scripts/pre-commit.local; fi`, so the file runs with bash before the
|
|
12
|
+
docs and scripts lines.
|
|
13
|
+
|
|
14
|
+
- A non-zero exit of the file stops the commit; with no such file the hook runs as before.
|
|
15
|
+
- The installer never writes, reads or ships the file, and no flag turns the line off.
|
|
16
|
+
- A hook this installer wrote gains the line the next time the installer runs (`prepare`, or by hand); a hook it
|
|
17
|
+
did not write is refused as before.
|
|
18
|
+
- `install-git-hooks-repo-exec.test.mjs` proves it through real commits: the file runs before the docs check, an exit
|
|
19
|
+
1 stops the commit, and an absent file lets it through.
|
|
20
|
+
|
|
21
|
+
MINOR: two new lines in every written hook.
|
|
22
|
+
|
|
7
23
|
## 8.3.0 — the hook installer arms the project-rules check and the layout check (AD-193)
|
|
8
24
|
|
|
9
25
|
**`references/scripts/install-git-hooks.mjs` gains `--rules-check <path>` and `--layout-check <path>`.** Each writes
|
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.4.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": "8.
|
|
3
|
+
"version": "8.4.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",
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
// deploy-copy suite (install-git-hooks.test.mjs) keeps the consumer-shaped behavior pins. Every
|
|
5
5
|
// branch of the installer runs from here: fresh install, the guard arm/carry/disable/conflict
|
|
6
6
|
// lanes, the rules-check and layout-check arm/carry/duplicate/refusal lanes (win32 included), the
|
|
7
|
-
// unmanaged refusal, the up-to-date short-circuit,
|
|
7
|
+
// unmanaged refusal, the up-to-date short-circuit, the not-a-git-checkout skip, and the S71
|
|
8
|
+
// real-commit lane (real `git commit` through the written hook over stand-in scripts). Every child
|
|
8
9
|
// runs with each inherited GIT_* variable stripped, so a hook's environment never leaks in.
|
|
9
10
|
|
|
10
11
|
import { describe, it } from 'node:test';
|
|
11
12
|
import assert from 'node:assert/strict';
|
|
12
|
-
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, realpathSync } from 'node:fs';
|
|
13
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, realpathSync, chmodSync, statSync } from 'node:fs';
|
|
13
14
|
import { tmpdir } from 'node:os';
|
|
14
15
|
import { join, dirname } from 'node:path';
|
|
15
16
|
import { fileURLToPath } from 'node:url';
|
|
@@ -130,3 +131,55 @@ describe('install-git-hooks — the in-place installer under a GIT_DIR-pinned fi
|
|
|
130
131
|
rmSync(nowhere, { recursive: true, force: true });
|
|
131
132
|
});
|
|
132
133
|
});
|
|
134
|
+
|
|
135
|
+
// The commit lane also drops NODE_TEST_CONTEXT: an inherited one makes the hook's nested `node --test` skip every file.
|
|
136
|
+
const IDENTITY = { GIT_AUTHOR_NAME: 'p', GIT_AUTHOR_EMAIL: 'p@e', GIT_COMMITTER_NAME: 'p', GIT_COMMITTER_EMAIL: 'p@e' };
|
|
137
|
+
const bareAt = (home, extra = {}) => { const { NODE_TEST_CONTEXT: _, ...env } = envAt(home, { ...IDENTITY, ...extra }); return env; };
|
|
138
|
+
const passes = (r) => (assert.equal(r.status, 0, r.stderr), r);
|
|
139
|
+
|
|
140
|
+
// Passing stand-ins for every script the hook runs; check-docs-size.mjs, run right after the local line, records
|
|
141
|
+
// whether the marker existed. The marker and the record live in a sibling dir, outside the work tree.
|
|
142
|
+
const commitRepo = (root, side) => {
|
|
143
|
+
const [marker, record, local] = [join(side, 'marker'), join(side, 'record'), join(root, 'scripts', 'pre-commit.local')];
|
|
144
|
+
const git = (...args) => spawnChecked('git', args, { cwd: root, env: bareAt(root) });
|
|
145
|
+
mkdirSync(join(root, 'scripts'));
|
|
146
|
+
writeFileSync(join(root, 'scripts', 'check-docs-size.mjs'), `import { existsSync, writeFileSync } from 'node:fs';
|
|
147
|
+
if (!process.argv.includes('--check-index')) writeFileSync(${JSON.stringify(record)}, String(existsSync(${JSON.stringify(marker)})));\n`);
|
|
148
|
+
for (const name of ['archive-changelog', 'archive-issues', 'archive-decisions']) writeFileSync(join(root, 'scripts', `${name}.mjs`), 'process.exit(0);\n');
|
|
149
|
+
writeFileSync(join(root, 'scripts', 'stand-in.test.mjs'), "import { test } from 'node:test';\ntest('a passing stand-in', () => {});\n");
|
|
150
|
+
for (const args of [['init', '-q'], ['add', '-A'], ['commit', '-qm', 'base']]) passes(git(...args));
|
|
151
|
+
passes(spawnChecked(process.execPath, [INSTALLER], { env: bareAt(root, { GIT_DIR: join(root, '.git') }) }));
|
|
152
|
+
const head = () => git('rev-parse', 'HEAD').stdout.trim();
|
|
153
|
+
const commit = (body) => {
|
|
154
|
+
if (body !== undefined) { writeFileSync(local, body); chmodSync(local, 0o644); assert.equal(statSync(local).mode & 0o111, 0, 'the local file is not executable'); }
|
|
155
|
+
writeFileSync(join(root, 'change.txt'), 'change\n');
|
|
156
|
+
passes(git('add', 'change.txt'));
|
|
157
|
+
return git('commit', '-qm', 'change');
|
|
158
|
+
};
|
|
159
|
+
return { marker, record, head, commit };
|
|
160
|
+
};
|
|
161
|
+
const inCommitRepo = (cell) => () => {
|
|
162
|
+
const dirs = ['hooks-commit-', 'hooks-commit-side-'].map((prefix) => realpathSync(mkdtempSync(join(tmpdir(), prefix))));
|
|
163
|
+
try { cell(commitRepo(...dirs)); } finally { for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); }
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
describe('spec:init-project/S71 the local hook file through real commits over stand-ins', () => {
|
|
167
|
+
it('a local file that writes a marker runs before the docs check, and the commit exits 0', { timeout: 120_000 }, inCommitRepo((fx) => {
|
|
168
|
+
passes(fx.commit(`printf made > "${fx.marker}"\n`));
|
|
169
|
+
assert.ok(existsSync(fx.marker), 'the local file ran: its marker appeared');
|
|
170
|
+
assert.equal(readFileSync(fx.record, 'utf8'), 'true', 'the marker existed when check-docs-size.mjs ran');
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
it('a local file that exits 1 stops the commit: non-zero, no new commit', { timeout: 120_000 }, inCommitRepo((fx) => {
|
|
174
|
+
const before = fx.head();
|
|
175
|
+
assert.notEqual(fx.commit('exit 1\n').status, 0, 'a local file exiting 1 stops the commit');
|
|
176
|
+
assert.equal(fx.head(), before, 'no new commit');
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
it('with no local file the commit exits 0', { timeout: 120_000 }, inCommitRepo((fx) => {
|
|
180
|
+
const before = fx.head();
|
|
181
|
+
passes(fx.commit());
|
|
182
|
+
assert.notEqual(fx.head(), before, 'the commit landed');
|
|
183
|
+
assert.equal(readFileSync(fx.record, 'utf8'), 'false', 'the hook ran its stand-ins, with no marker');
|
|
184
|
+
}));
|
|
185
|
+
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// suite, so docs files cannot drift over their declared `maxLines`, the auto-generated
|
|
7
7
|
// `index.md` navigator cannot silently fall out of sync, stale archive entries never
|
|
8
8
|
// reach a commit, and regressions to the scripts themselves are caught at commit time.
|
|
9
|
+
// First after `set -e` it runs the project's own `scripts/pre-commit.local` with bash when present.
|
|
9
10
|
//
|
|
10
11
|
// The hooks location comes from git's OWN answer (`git rev-parse --git-path hooks`) — a linked
|
|
11
12
|
// worktree resolves ITS OWN hooks path (its `.git` is a FILE; a hardcoded `.git/hooks` would
|
|
@@ -161,6 +162,8 @@ ${MAGIC_MARKER}
|
|
|
161
162
|
# declared maxLines, the auto-generated index.md cannot silently go stale, stale
|
|
162
163
|
# archive entries never reach a commit, and regressions to the scripts are caught.
|
|
163
164
|
set -e
|
|
165
|
+
# Your own commit checks go in scripts/pre-commit.local; it runs first, with bash, when present.
|
|
166
|
+
if [ -f scripts/pre-commit.local ]; then bash scripts/pre-commit.local; fi
|
|
164
167
|
node scripts/check-docs-size.mjs
|
|
165
168
|
node scripts/check-docs-size.mjs --check-index
|
|
166
169
|
node scripts/archive-changelog.mjs --check
|
|
@@ -59,7 +59,7 @@ const markerFor = (name) => `# ${name}:install-git-hooks.mjs`;
|
|
|
59
59
|
const readHook = (root) => readFileSync(join(hooksPathOf(root), 'pre-commit'), 'utf8');
|
|
60
60
|
const stub = (root, rel) => (mkdirSync(dirname(join(root, rel)), { recursive: true }), writeFileSync(join(root, rel), '// a stand-in tool\n'), join(root, rel));
|
|
61
61
|
|
|
62
|
-
// The hook the
|
|
62
|
+
// The hook the 8.3.0 installer body writes with no guard: the six docs and scripts lines.
|
|
63
63
|
const plainHook = (root) => `#!/usr/bin/env bash
|
|
64
64
|
${markerFor(basename(root))}
|
|
65
65
|
# Auto-installed by scripts/install-git-hooks.mjs (run it from "prepare" or by hand).
|
|
@@ -75,6 +75,16 @@ node scripts/archive-issues.mjs --check
|
|
|
75
75
|
node scripts/archive-decisions.mjs --check
|
|
76
76
|
node --test scripts/*.test.mjs
|
|
77
77
|
`;
|
|
78
|
+
const LOCAL_LINE = 'if [ -f scripts/pre-commit.local ]; then bash scripts/pre-commit.local; fi';
|
|
79
|
+
// The plain hook every write carries: the 8.3.0 hook with, right after set -e, one `# ` comment naming
|
|
80
|
+
// scripts/pre-commit.local (its wording is free, so it is read from the written hook) and the exact local line.
|
|
81
|
+
const localHook = (root) => {
|
|
82
|
+
const lines = readHook(root).split('\n');
|
|
83
|
+
const comment = lines[lines.indexOf('set -e') + 1];
|
|
84
|
+
assert.ok(comment?.startsWith('# ') && comment.includes('scripts/pre-commit.local'), `right after set -e a # comment names scripts/pre-commit.local, got ${JSON.stringify(comment)}`);
|
|
85
|
+
assert.deepEqual(lines.filter((l) => l.includes('scripts/pre-commit.local')), [comment, LOCAL_LINE], 'the comment and the local line are the only lines naming scripts/pre-commit.local');
|
|
86
|
+
return plainHook(root).replace('set -e\n', `set -e\n${comment}\n${LOCAL_LINE}\n`);
|
|
87
|
+
};
|
|
78
88
|
const line = (path, role) => `node "${role ? shown(path) : path}" --check${role ? ` # ${role}` : ''}\n`;
|
|
79
89
|
|
|
80
90
|
describe('install-git-hooks — beside no package.json, or a malformed one, the marker names the directory basename', () => {
|
|
@@ -148,8 +158,9 @@ describe('spec:init-project/S58 install-git-hooks — the rules-check and layout
|
|
|
148
158
|
const [rules, layout, guard, guard2] = ['tools/rules-cli.mjs', 'tools/source-size-check.mjs', 'guard.mjs', 'g2/guard.mjs'].map((rel) => stub(root, rel));
|
|
149
159
|
installs(root, ['--commit-guard', 'guard.mjs']);
|
|
150
160
|
installs(root, ['--layout-check', 'tools/source-size-check.mjs']);
|
|
161
|
+
assert.equal(readHook(root), localHook(root) + line(layout, 'layout-check') + line(guard), 'the --layout-check run carried the guard line');
|
|
151
162
|
installs(root, ['--rules-check', rules]);
|
|
152
|
-
const armed =
|
|
163
|
+
const armed = localHook(root) + line(rules, 'rules-check') + line(layout, 'layout-check');
|
|
153
164
|
assert.equal(readHook(root), armed + line(guard), 'each arming run carried the other armed lines');
|
|
154
165
|
assert.match(installs(root).stdout, /already up to date/, 'a flagless re-run keeps all three');
|
|
155
166
|
assert.equal(readHook(root), armed + line(guard));
|
|
@@ -163,7 +174,7 @@ describe('spec:init-project/S58 install-git-hooks — the rules-check and layout
|
|
|
163
174
|
const [legacy, rules] = ['legacy/rules-cli.mjs', 'tools/rules-cli.mjs'].map((rel) => stub(root, rel));
|
|
164
175
|
installs(root, ['--commit-guard', legacy]);
|
|
165
176
|
installs(root, ['--rules-check', rules]);
|
|
166
|
-
const expected =
|
|
177
|
+
const expected = localHook(root) + line(rules, 'rules-check') + line(legacy);
|
|
167
178
|
assert.equal(readHook(root), expected);
|
|
168
179
|
installs(root);
|
|
169
180
|
assert.equal(readHook(root), expected, 'the flagless re-run keeps the legacy line as the guard');
|
|
@@ -183,11 +194,11 @@ describe('spec:init-project/S58 install-git-hooks — the rules-check and layout
|
|
|
183
194
|
}
|
|
184
195
|
}));
|
|
185
196
|
|
|
186
|
-
it('over a hook with no rules or layout line, a flagless and a --commit-guard run write the
|
|
197
|
+
it('over a hook with no rules or layout line, a flagless and a --commit-guard run write the plain hook with the comment line and the local line right after set -e', inProject(({ root }) => {
|
|
187
198
|
installs(root);
|
|
188
|
-
assert.equal(readHook(root),
|
|
199
|
+
assert.equal(readHook(root), localHook(root));
|
|
189
200
|
installs(root, ['--commit-guard', 'guard.mjs']);
|
|
190
|
-
assert.equal(readHook(root),
|
|
201
|
+
assert.equal(readHook(root), localHook(root) + line(join(root, 'guard.mjs')));
|
|
191
202
|
}));
|
|
192
203
|
|
|
193
204
|
it('a wrong file name, a shell-active path or a missing value exits 2, a missing file exits 1, each writing nothing', inProject(({ root }) => {
|
|
@@ -220,11 +231,11 @@ describe('spec:init-project/S58 install-git-hooks — the rules-check and layout
|
|
|
220
231
|
const armedAt = stub(root, 'a/b/rules-cli.mjs');
|
|
221
232
|
if (process.platform !== 'win32') { // there the backslash is refused, writing nothing
|
|
222
233
|
const posix = runInstaller(root, ['--rules-check', given]);
|
|
223
|
-
assert.deepEqual([posix.status, /a backslash, CR or LF/.test(posix.stderr), readHook(root)], [2, true,
|
|
234
|
+
assert.deepEqual([posix.status, /a backslash, CR or LF/.test(posix.stderr), readHook(root)], [2, true, localHook(root)], posix.stderr);
|
|
224
235
|
}
|
|
225
236
|
installs(root, ['--rules-check', given], WIN32);
|
|
226
|
-
assert.equal(readHook(root),
|
|
237
|
+
assert.equal(readHook(root), localHook(root) + line(armedAt, 'rules-check'));
|
|
227
238
|
assert.match(installs(root).stdout, /already up to date/, 'the flagless re-run recognises the win32-armed line');
|
|
228
|
-
assert.equal(readHook(root),
|
|
239
|
+
assert.equal(readHook(root), localHook(root) + line(armedAt, 'rules-check'));
|
|
229
240
|
}));
|
|
230
241
|
});
|