@sabaiway/agent-workflow-memory 1.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +61 -0
  4. package/SKILL.md +201 -0
  5. package/bin/install.mjs +185 -0
  6. package/bin/install.test.mjs +76 -0
  7. package/capability.json +27 -0
  8. package/migrations/1.1.0-communication-language.md +51 -0
  9. package/migrations/1.2.0-agent-attribution.md +57 -0
  10. package/migrations/README.md +46 -0
  11. package/migrations/legacy-stamp-takeover.md +72 -0
  12. package/package.json +53 -0
  13. package/references/contracts.md +62 -0
  14. package/references/scripts/_expect-shim.mjs +41 -0
  15. package/references/scripts/archive-changelog.mjs +438 -0
  16. package/references/scripts/archive-changelog.test.mjs +212 -0
  17. package/references/scripts/archive-issues.mjs +179 -0
  18. package/references/scripts/archive-issues.test.mjs +95 -0
  19. package/references/scripts/check-docs-size.mjs +353 -0
  20. package/references/scripts/check-docs-size.test.mjs +180 -0
  21. package/references/scripts/install-git-hooks.mjs +83 -0
  22. package/references/templates/AGENTS.md +96 -0
  23. package/references/templates/active_plan.md +31 -0
  24. package/references/templates/agent_rules.md +85 -0
  25. package/references/templates/architecture.md +49 -0
  26. package/references/templates/changelog.md +24 -0
  27. package/references/templates/current_state.md +36 -0
  28. package/references/templates/decisions.md +44 -0
  29. package/references/templates/env_commands.md +41 -0
  30. package/references/templates/handover.md +37 -0
  31. package/references/templates/known_issues.md +33 -0
  32. package/references/templates/pages/PAGE_TEMPLATE.md +53 -0
  33. package/references/templates/pages/index.md +23 -0
  34. package/references/templates/pages/shared-patterns.md +30 -0
  35. package/references/templates/tech_reference.md +34 -0
  36. package/references/templates/technical_specification.md +37 -0
  37. package/scripts/package-content.test.mjs +78 -0
  38. package/scripts/stamp-takeover.mjs +181 -0
  39. package/scripts/stamp-takeover.test.mjs +166 -0
  40. package/scripts/standalone-bootstrap.test.mjs +111 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog — @sabaiway/agent-workflow-memory
2
+
3
+ All notable changes to the memory substrate. Versions are this **package's** npm versions;
4
+ they are distinct from the **deployment-lineage** stamp written into a project's
5
+ `docs/ai/.memory-version` (which tracks the shared `agent-workflow` lineage, head `1.3.0`).
6
+
7
+ ## 1.0.0
8
+
9
+ Initial standalone release. The **memory substrate** extracted into its own package as Plan 1
10
+ of the agent-workflow family refactor (deployment-lineage head `1.3.0`). Additive: the family
11
+ composition root keeps its own bundled copy as a fallback, so nothing breaks for existing users.
12
+
13
+ ### Added
14
+ - Standalone npm package + `bin/install.mjs` installer targeting
15
+ `~/.claude/skills/agent-workflow-memory` (`AGENT_WORKFLOW_MEMORY_DIR`), with a
16
+ symlink-traversal guard — the installer never writes *through* a destination symlink
17
+ (root / intermediate / leaf).
18
+ - `capability.json` — the `agent-workflow` family manifest (`kind: memory-substrate`,
19
+ `provides: ["context"]`). The package ships **no** family-wide schema/validator tooling
20
+ (owned by the composition root) — it knows nobody.
21
+ - An **empty** delimited methodology slot in `templates/AGENTS.md`
22
+ (`<!-- workflow:methodology:start -->` / `:end`); the substrate only ever ships it empty
23
+ and preserves any filled content on upgrade. The composition root injects the methodology.
24
+ - `scripts/stamp-takeover.mjs` — a pure, unit-tested state machine for the legacy
25
+ `.workflow-version` → `.memory-version` lineage takeover, with **atomic** (write-temp +
26
+ rename) stamp writes, plus `migrations/legacy-stamp-takeover.md` (the no-Node manual
27
+ fallback).
28
+
29
+ ### Changed
30
+ - The deployment stamp is `docs/ai/.memory-version` (the legacy `.workflow-version` is never
31
+ deleted; both track the shared lineage). Hidden-mode ignore lists now include
32
+ `.memory-version`.
33
+ - `references/templates/agent_rules.md` no longer embeds the planning-methodology vocabulary —
34
+ it points at the methodology owner. `contracts.md` attribution wiring is de-attributed from
35
+ "the kit" to "the bootstrap".
36
+
37
+ ### Carried over from the original substrate (deployment-lineage 1.3.0)
38
+ - `docs/ai/` templates, the Node enforcement scripts (caps + index freshness + 3-tier archive
39
+ + hook installer) and their tests, the three setup contracts, and migrations `1.1.0`
40
+ (conversational language) and `1.2.0` (agent attribution).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sabaiway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @sabaiway/agent-workflow-memory
2
+
3
+ **Portable, cross-agent memory substrate for AI coding agents.** Deploys an `AGENTS.md`
4
+ entry point (+ a `CLAUDE.md` symlink for Claude Code) and a structured `docs/ai/` context
5
+ store with cap / archive / index-freshness enforcement into any repo. After it runs, any
6
+ future agent — including a fresh session of yourself — can reconstruct project context in
7
+ ~60 seconds, find the current task, and avoid repeating past mistakes.
8
+
9
+ This is the **memory layer** of the `agent-workflow` family.
10
+ It owns the substrate only — `docs/ai/`, the entry point, caps / 3-tier archive / index gate,
11
+ the Node enforcement scripts, the pre-commit hook, the templates, and the three setup
12
+ contracts (visibility / conversational language / agent attribution). It deliberately **knows
13
+ nobody else** in the `agent-workflow` family: the **workflow methodology** (plan → execute →
14
+ review vocabulary, lifecycle, `queue.md`, mandatory Cleanup) lives elsewhere and is injected
15
+ into a delimited slot in `AGENTS.md` by the family composition root, not by this package.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npx @sabaiway/agent-workflow-memory init
21
+ ```
22
+
23
+ Installs/refreshes the skill at `~/.claude/skills/agent-workflow-memory` (override with
24
+ `--dir <path>` or `AGENT_WORKFLOW_MEMORY_DIR`). `init` is additive — it never deletes your
25
+ settings and never writes through a symlink. Re-running updates the skill's own files; that is
26
+ how you upgrade the *skill*. Migrating a *project's* deployment is a separate in-agent step
27
+ (below).
28
+
29
+ ## Use
30
+
31
+ Open your agent inside a project and run the skill:
32
+
33
+ - **`/agent-workflow-memory`** — bootstrap a new or empty project. Asks the three setup
34
+ questions (visibility, conversational language, agent attribution), then writes `docs/ai/`,
35
+ `AGENTS.md`, the enforcement scripts + pre-commit hook, and stamps
36
+ `docs/ai/.memory-version`. The methodology slot in `AGENTS.md` is left **empty**.
37
+ - **`/agent-workflow-memory upgrade`** — migrate an existing deployment to the skill's current
38
+ version. Reads `docs/ai/.memory-version`, runs only newer migrations in semver order, and
39
+ **preserves** anything already in the methodology slot (extract-and-reinsert — it never
40
+ regenerates `AGENTS.md` wholesale).
41
+
42
+ ## Stamps & lineage
43
+
44
+ Two independent axes: this package's **npm version** vs the **deployment-lineage** stamp the
45
+ substrate writes into a project. They are not the same number. `.memory-version` tracks the
46
+ shared `agent-workflow` deployment lineage (head `1.3.0` today), **not** this package's
47
+ version. A project bootstrapped by the kit's fallback carries a `.workflow-version`; when this
48
+ substrate is later installed, `migrations/legacy-stamp-takeover.md` copies that value verbatim
49
+ into `.memory-version` and never deletes the legacy stamp. See `migrations/README.md`.
50
+
51
+ ## What this package ships
52
+
53
+ `SKILL.md` (procedure + ownership table), `references/` (templates + the deployable Node
54
+ enforcement scripts + the setup contracts), `scripts/stamp-takeover.mjs` (the upgrade-time
55
+ lineage state machine + tests), `migrations/`, `capability.json` (the family manifest), and
56
+ this installer. It ships **no** family-wide tooling (the schema/validator are owned by the
57
+ composition root) — preserving "knows nobody".
58
+
59
+ ## License
60
+
61
+ MIT — see [LICENSE](./LICENSE).
package/SKILL.md ADDED
@@ -0,0 +1,201 @@
1
+ ---
2
+ name: agent-workflow-memory
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
+ disable-model-invocation: true
5
+ metadata:
6
+ version: '1.0.0'
7
+ ---
8
+
9
+ # agent-workflow-memory
10
+
11
+ Deploys a **portable AI-agent memory substrate** into a project, and upgrades it as the
12
+ substrate evolves. After it runs, any future agent (including a fresh session of yourself) can
13
+ reconstruct project context in ~60 seconds, find the current task, and avoid repeating past
14
+ mistakes.
15
+
16
+ The substrate is **stack-agnostic memory** — `docs/ai/` structure, entry-point doc, session
17
+ protocols, frontmatter caps, 3-tier archive, index-freshness gate. Enforcement ships as **Node
18
+ `.mjs` scripts** (the reference implementation; non-Node stacks follow the same policy
19
+ manually).
20
+
21
+ This skill is the **memory layer** of the `agent-workflow` family. It **knows nobody else** in
22
+ the family. In particular it does **not** own the **workflow methodology** (plan → execute →
23
+ review vocabulary, lifecycle, `docs/plans/queue.md`, mandatory Cleanup, plan-then-execute) —
24
+ that is injected into a delimited slot in `AGENTS.md` by the family **composition root**, never by
25
+ this skill. (This substrate does not name or depend on any specific sibling — it only honours the
26
+ slot contract.) This skill only ever ships the slot **empty** and **preserves** whatever is already
27
+ in it on upgrade.
28
+
29
+ The substrate **artifacts** (this skill, the templates, the deployed `docs/ai/` files) stay in
30
+ their **source language** — for cross-agent and cross-team portability. That is separate from the
31
+ **conversational language** chosen at bootstrap (which governs dialogue only, never file
32
+ contents).
33
+
34
+ ---
35
+
36
+ ## Ownership table
37
+
38
+ What this substrate owns vs what it only points at. The methodology pointer is the empty slot
39
+ the composition root fills — never author methodology text here.
40
+
41
+ | Concern | Owner | In the deployed `AGENTS.md` |
42
+ |---|---|---|
43
+ | Entry point, Memory Map, session protocols (Start / During / Complete) | **memory** (this skill) | authored from templates |
44
+ | `docs/ai/` files, frontmatter caps, 3-tier archive, index-freshness gate | **memory** | `docs/ai/*` + scripts + hook |
45
+ | Visibility / conversational-language / agent-attribution contracts | **memory** | the three `AGENTS.md` blocks |
46
+ | Deployment-lineage stamp | **memory** | `docs/ai/.memory-version` |
47
+ | Plan→Phase→Step vocabulary, lifecycle, `queue.md`, mandatory Cleanup | **methodology** (not this skill) | the empty `workflow:methodology` slot — filled by the composition root |
48
+
49
+ ---
50
+
51
+ ## Two modes
52
+
53
+ Pick the mode from the user's invocation. Auto-detect an existing `docs/ai/` to guard against
54
+ bootstrapping over a live system, but the user makes the final call.
55
+
56
+ - **`/agent-workflow-memory`** (default) — bootstrap a new or empty project. If `docs/ai/`
57
+ already exists, stop and ask whether they meant `upgrade`.
58
+ - **`/agent-workflow-memory upgrade`** — upgrade an existing deployment.
59
+
60
+ ### Mode: bootstrap
61
+
62
+ > Bundled sources below (templates, scripts) live in **this skill's own directory** —
63
+ > `${CLAUDE_SKILL_DIR}/` in Claude Code, or the folder containing this `SKILL.md` elsewhere.
64
+ > Use that as the copy/read source; the working directory is the **target project**.
65
+
66
+ > The three setup questions (steps 2–4) are decisions only the user can make and are hard to
67
+ > reverse after a commit. Ask each as a **structured multiple-choice prompt where supported**
68
+ > (`AskUserQuestion` in Claude Code, recommended option first), otherwise in prose — and
69
+ > **wait for the answer before writing anything**.
70
+
71
+ 1. **Recon (read-only).** Before writing anything: `package.json` / `pyproject.toml` / `go.mod`
72
+ / `Cargo.toml` → stack, package manager, scripts; `ls -la` root → README, existing
73
+ `AGENTS.md`/`CLAUDE.md`, CI / linter configs; `git log --oneline -30` + `git status`; `src/`
74
+ 2–3 levels deep; tests + linter rules. Record stack, package manager, daily commands, layers.
75
+ 2. **Choose visibility — ASK explicitly and wait.** `visible` (committed — canonical,
76
+ recommended) or `hidden` (in-tree, hidden via `~/.gitignore_global`). See
77
+ [Visibility contract](references/contracts.md#visibility-contract).
78
+ 3. **Choose conversational language — ASK explicitly and wait.** Which language the agent
79
+ *talks to them* in. Offer the language they're already writing in as the default. Carry it
80
+ into the `{{COMM_LANGUAGE}}` slot of the *Communication language* block (step 5). See
81
+ [Communication contract](references/contracts.md#communication-contract). Dialogue only —
82
+ never the files.
83
+ 4. **Choose agent attribution — ASK explicitly and wait.** May the agent attribute work to
84
+ itself / AI — `Co-Authored-By` trailers, "Generated with …" footers, AI/agent/model mentions?
85
+ **Default `off`.** Carry it into `{{AGENT_ATTRIBUTION}}` (step 5). **If `off` and the project
86
+ uses Claude Code**, also set `"includeCoAuthoredBy": false` in the project's
87
+ `.claude/settings.json` (create it if absent). See
88
+ [Attribution contract](references/contracts.md#attribution-contract).
89
+ 5. **Entry-point doc.** If `AGENTS.md` / `CLAUDE.md` already exist (recon), do **not**
90
+ overwrite — show the user and ask whether to merge or replace. Otherwise create `AGENTS.md`
91
+ from `${CLAUDE_SKILL_DIR}/references/templates/AGENTS.md` and symlink `CLAUDE.md -> AGENTS.md`
92
+ (`ln -s AGENTS.md CLAUDE.md`). **Leave the `workflow:methodology` slot exactly as shipped —
93
+ empty.** Filling it is the composition root's job.
94
+ 6. **Deploy `docs/ai/`.** Create the files + `pages/` from
95
+ `${CLAUDE_SKILL_DIR}/references/templates/`. Keep each file's frontmatter.
96
+ 7. **Fill templates** per the table below.
97
+ 8. **Install enforcement (Node projects).** Copy `${CLAUDE_SKILL_DIR}/references/scripts/*.mjs`
98
+ (+ `*.test.mjs`) into the project's `scripts/`. **No Node runtime** → skip this + the hook;
99
+ follow the cap/archive/index policy manually.
100
+ 9. **Wire / hide** per visibility (see contract). Install the pre-commit hook (Node projects):
101
+ `node scripts/install-git-hooks.mjs`. If it reports a pre-existing non-marker hook, stop and
102
+ ask the user to merge it manually.
103
+ 10. **Stamp the deployment lineage.** Write the **deployment-lineage head** into
104
+ `docs/ai/.memory-version` (one semver line). The lineage head is **`1.3.0`** (the
105
+ `LINEAGE_HEAD` constant in `scripts/stamp-takeover.mjs`) — the shared `agent-workflow`
106
+ lineage, **not** this package's npm version (`1.0.0`). Use the atomic writer in
107
+ `scripts/stamp-takeover.mjs` (write-temp + rename).
108
+ 11. **Report & ask.** Show `tree docs/ai/`, 2–3 lines on filled-vs-TODO, then **ask before
109
+ committing** — never auto-commit. **Exception — delegated mode (below): skip this gate.**
110
+
111
+ > **Delegated mode (invoked by the composition root) — applies to BOTH bootstrap and upgrade.**
112
+ > When the family composition root drives this substrate as part of a family bootstrap **or
113
+ > upgrade**, do the write steps (bootstrap 1–10 / upgrade 1–7: write `docs/ai/` + `AGENTS.md` +
114
+ > `.memory-version`) but **do NOT** run **any** commit gate and **do NOT** ask to commit — the
115
+ > composition root owns the **single** commit gate, raised after it injects the methodology slot.
116
+ > The three setup answers and the target dir are passed in by the composition root; you perform no
117
+ > commit and no slot injection. **Standalone** invocation keeps its own commit gate (bootstrap step
118
+ > 11 / upgrade step 7).
119
+
120
+ Fill strategy:
121
+
122
+ | File | Strategy |
123
+ |------|----------|
124
+ | `current_state.md`, `architecture.md`, `env_commands.md`, `technical_specification.md`, `pages/index.md` | Fill with **real** recon data. |
125
+ | `tech_reference.md` | Carry over real configs/patterns found in deps. |
126
+ | `active_plan.md`, `handover.md` | TODO seed. |
127
+ | `decisions.md` | Seed `AD-001` (adopting this memory system). |
128
+ | `known_issues.md`, `changelog.md`, `pages/shared-patterns.md` | Empty template / first bootstrap entry. |
129
+
130
+ ### Mode: upgrade
131
+
132
+ 1. **Resolve the stamp.** Read `docs/ai/.memory-version`. If it is absent but a legacy
133
+ `docs/ai/.workflow-version` exists (a kit fallback bootstrap), run
134
+ `migrations/legacy-stamp-takeover.md` **first** — it copies that value verbatim into
135
+ `.memory-version` (and never deletes the legacy stamp). The pure state machine in
136
+ `scripts/stamp-takeover.mjs` decides the action per state; the Markdown migration is the
137
+ no-Node manual fallback. If **no** stamp exists at all, offer a conservative re-bootstrap.
138
+ 2. **Compare** the stamp to the **deployment-lineage head** (`LINEAGE_HEAD`, `1.3.0`). Equal →
139
+ report "up to date" and stop. **Greater than the head, or unparseable → STOP and report**
140
+ (never downgrade or guess).
141
+ 3. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` context (entries newer than the stamp).
142
+ 4. Apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those
143
+ newer than the stamp. Migrations are **idempotent**.
144
+ 5. Reconcile drift: add any substrate files/scripts the project is missing; **never clobber
145
+ project-authored content** (their `decisions.md`, `known_issues.md`, page specs stay). For a
146
+ pre-1.1.0 deployment with no *Communication language* block, ask + insert
147
+ (`migrations/1.1.0-communication-language.md`); pre-1.2.0 with no *Attribution* block, ask +
148
+ insert defaulting to `off` (`migrations/1.2.0-agent-attribution.md`).
149
+ 6. **Preserve the methodology slot.** If `AGENTS.md` has the `workflow:methodology` markers,
150
+ **never regenerate the file wholesale** — extract any bytes between the markers and reinsert
151
+ them unchanged. If the markers are absent (a legacy `AGENTS.md`), gracefully **no-op** on the
152
+ slot (adding the slot to already-deployed files is a separate methodology migration). On any
153
+ malformed marker state (single, reversed, nested, or duplicate pair), **no-op with an error**
154
+ — never edit.
155
+ 7. **Re-stamp** `docs/ai/.memory-version` to the lineage head (atomic write). Report changes;
156
+ **ask before committing** — **except in delegated mode** (see the *Delegated mode* note above),
157
+ where the composition root owns the single gate and this step raises none.
158
+
159
+ ---
160
+
161
+ ## Gotchas
162
+
163
+ - **Source vs target directory.** Templates/scripts are read from the skill's own dir; the
164
+ **working directory is the target project** — never write substrate files back into the skill.
165
+ - **Stamp = lineage head, not package version.** `.memory-version` carries `1.3.0` (the shared
166
+ `agent-workflow` lineage), not the npm `1.0.0`. They are independent axes.
167
+ - **The methodology slot ships empty and stays the user's.** Never author methodology text into
168
+ it; on upgrade, preserve its content byte-for-byte. The composition root is its only writer.
169
+ - **The `Co-Authored-By` trailer is added by the harness, not by prose.** When attribution is
170
+ `off` + Claude Code, also set `"includeCoAuthoredBy": false` in `.claude/settings.json`.
171
+ - **Hidden mode must never touch `package.json`.** It also ignores `docs/ai/.memory-version`
172
+ (and any kit-fallback `.workflow-version`). After hiding, verify `git status` shows the
173
+ artifacts as ignored.
174
+ - **`CLAUDE.md` is a symlink, not a copy.** `ln -s AGENTS.md CLAUDE.md`.
175
+ - **Never overwrite an existing entry point or hook.** Stop and ask to merge vs replace.
176
+ - **No Node runtime → skip enforcement** (scripts + hook); follow the policy manually.
177
+ - **Never auto-commit.** Report quality-gate results and wait for explicit approval — both modes.
178
+
179
+ ---
180
+
181
+ ## Setup contracts
182
+
183
+ The three setup choices each have a full contract in
184
+ [`references/contracts.md`](references/contracts.md). Defaults: visibility = `visible`; language
185
+ = whatever the user is already writing in; attribution = `off`.
186
+
187
+ ---
188
+
189
+ ## References
190
+
191
+ - [`references/contracts.md`](references/contracts.md) — the three setup contracts in full.
192
+ - [`references/templates/`](references/templates/) — stack-agnostic `AGENTS.md` (with the empty
193
+ methodology slot), `agent_rules.md`, and all `docs/ai/` files to deploy.
194
+ - [`references/scripts/`](references/scripts/) — the Node enforcement scripts (caps + staleness +
195
+ index-freshness gate, 3-tier archive, hook installer) and their unit tests.
196
+ - [`scripts/stamp-takeover.mjs`](scripts/stamp-takeover.mjs) — the upgrade-time lineage state
197
+ machine (`LINEAGE_HEAD`, atomic stamp writes) + tests.
198
+ - [`migrations/`](migrations/) — per-version upgrade steps, incl. `legacy-stamp-takeover.md`;
199
+ see `migrations/README.md`.
200
+ - [`capability.json`](capability.json) — the `agent-workflow` family manifest.
201
+ - [`CHANGELOG.md`](CHANGELOG.md) — version history of this substrate.
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ // One-shot installer for @sabaiway/agent-workflow-memory.
3
+ //
4
+ // npx @sabaiway/agent-workflow-memory init
5
+ //
6
+ // Copies the memory substrate into the canonical skill home
7
+ // (~/.claude/skills/agent-workflow-memory, override with --dir or
8
+ // AGENT_WORKFLOW_MEMORY_DIR). Re-running refreshes the skill to this package's
9
+ // version — that is how you upgrade the *skill files* themselves:
10
+ //
11
+ // npx @sabaiway/agent-workflow-memory@latest init
12
+ //
13
+ // That is distinct from `/agent-workflow-memory upgrade`, which migrates a
14
+ // *project's* docs/ai deployment — see README "Use".
15
+ //
16
+ // No telemetry, no phone-home. Dependency-free, Node >= 18.
17
+
18
+ import { readFile, mkdir, readdir, copyFile, lstat, readlink, symlink } from 'node:fs/promises';
19
+ import { existsSync, lstatSync } from 'node:fs';
20
+ import { dirname, join, resolve, relative, sep, isAbsolute } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { homedir } from 'node:os';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const PKG_ROOT = resolve(__dirname, '..');
26
+
27
+ // The deployable skill = everything except the npm wrapper (package.json, bin/).
28
+ // Enumerated explicitly (mirrors package.json "files") so a missing payload entry
29
+ // is a loud failure, not a silent partial install.
30
+ const PAYLOAD = [
31
+ 'SKILL.md',
32
+ 'README.md',
33
+ 'CHANGELOG.md',
34
+ 'LICENSE',
35
+ 'capability.json',
36
+ 'references',
37
+ 'scripts',
38
+ 'migrations',
39
+ ];
40
+
41
+ const tildify = (path) => path.replace(homedir(), '~');
42
+
43
+ const readVersion = async () => {
44
+ try {
45
+ const pkg = JSON.parse(await readFile(resolve(PKG_ROOT, 'package.json'), 'utf8'));
46
+ return pkg.version ?? 'unknown';
47
+ } catch {
48
+ return 'unknown';
49
+ }
50
+ };
51
+
52
+ // lstat without following symlinks; null when the path does not exist. Using lstat (not
53
+ // existsSync, which FOLLOWS symlinks and so reports a *dangling* symlink as absent) is what
54
+ // makes the guard below catch a dangling destination symlink too.
55
+ const lstatNoFollow = (path) => {
56
+ try {
57
+ return lstatSync(path);
58
+ } catch (err) {
59
+ if (err && err.code === 'ENOENT') return null;
60
+ throw err; // EACCES/EIO etc. must NOT fail open (be read as "not a symlink")
61
+ }
62
+ };
63
+
64
+ // Symlink-traversal guard: refuse to write *through* any symlink at or above `dest`
65
+ // within `root`. Walks `root` plus each existing path component down to `dest`; if the
66
+ // root, an intermediate dir, or the leaf is a symlink (including a dangling one), a copy
67
+ // could escape the target — STOP rather than follow it. Also refuses a dest outside `root`.
68
+ const assertContainedRealPath = (root, dest) => {
69
+ const rel = relative(root, dest);
70
+ if (rel.startsWith('..') || isAbsolute(rel)) {
71
+ throw new Error(`[agent-workflow-memory] refusing to write outside the target dir: ${dest}`);
72
+ }
73
+ if (lstatNoFollow(root)?.isSymbolicLink()) {
74
+ throw new Error(`[agent-workflow-memory] refusing to install into a symlinked target dir: ${root}`);
75
+ }
76
+ const walk = (acc, part) => {
77
+ const cur = join(acc, part);
78
+ if (lstatNoFollow(cur)?.isSymbolicLink()) {
79
+ throw new Error(
80
+ `[agent-workflow-memory] refusing to write through a symlink at ${cur} (would escape ${root}).`,
81
+ );
82
+ }
83
+ return cur;
84
+ };
85
+ rel.split(sep).filter(Boolean).reduce(walk, root);
86
+ };
87
+
88
+ const copyRecursive = async (src, dest, root) => {
89
+ assertContainedRealPath(root, dest); // never write through a dest symlink (root/intermediate/leaf)
90
+ const stat = await lstat(src);
91
+ if (stat.isSymbolicLink()) {
92
+ if (existsSync(dest)) return; // additive: never delete/replace an existing entry
93
+ const linkTarget = await readlink(src);
94
+ await symlink(linkTarget, dest);
95
+ } else if (stat.isDirectory()) {
96
+ await mkdir(dest, { recursive: true });
97
+ const entries = await readdir(src);
98
+ await Promise.all(entries.map((entry) => copyRecursive(join(src, entry), join(dest, entry), root)));
99
+ } else {
100
+ await mkdir(dirname(dest), { recursive: true });
101
+ await copyFile(src, dest);
102
+ }
103
+ };
104
+
105
+ const parseArgs = (argv) => {
106
+ const dirFlag = argv.indexOf('--dir');
107
+ return {
108
+ help: argv.includes('--help') || argv.includes('-h'),
109
+ version: argv.includes('--version') || argv.includes('-v'),
110
+ dir: dirFlag >= 0 ? argv[dirFlag + 1] : undefined,
111
+ };
112
+ };
113
+
114
+ const resolveTarget = (dirArg) => {
115
+ if (dirArg) return resolve(dirArg);
116
+ if (process.env.AGENT_WORKFLOW_MEMORY_DIR) return resolve(process.env.AGENT_WORKFLOW_MEMORY_DIR);
117
+ return resolve(homedir(), '.claude/skills/agent-workflow-memory');
118
+ };
119
+
120
+ const printHelp = (version) => {
121
+ console.log(`agent-workflow-memory ${version}
122
+
123
+ Usage:
124
+ npx @sabaiway/agent-workflow-memory init [--dir <path>]
125
+ npx @sabaiway/agent-workflow-memory --version
126
+ npx @sabaiway/agent-workflow-memory --help
127
+
128
+ Installs/refreshes the memory substrate at ~/.claude/skills/agent-workflow-memory
129
+ (override with --dir <path> or AGENT_WORKFLOW_MEMORY_DIR). init is additive —
130
+ it never deletes your settings and never writes through a symlink.
131
+
132
+ After install, invoke the skill in your agent, inside a project:
133
+ first time in the project -> /agent-workflow-memory
134
+ project already has it -> /agent-workflow-memory upgrade
135
+
136
+ Re-running this npx command updates the skill's own files; /agent-workflow-memory
137
+ upgrade then migrates a project's deployment to that version.`);
138
+ };
139
+
140
+ const main = async () => {
141
+ const version = await readVersion();
142
+ const args = parseArgs(process.argv.slice(2));
143
+
144
+ if (args.help) return printHelp(version);
145
+ if (args.version) return console.log(version);
146
+
147
+ const missing = PAYLOAD.filter((entry) => !existsSync(resolve(PKG_ROOT, entry)));
148
+ if (missing.includes('SKILL.md')) {
149
+ console.error('[agent-workflow-memory] package payload missing (no SKILL.md) — corrupt install?');
150
+ process.exit(1);
151
+ }
152
+ if (missing.length > 0) {
153
+ console.error(`[agent-workflow-memory] package payload incomplete — missing: ${missing.join(', ')}`);
154
+ process.exit(1);
155
+ }
156
+
157
+ const target = resolveTarget(args.dir);
158
+ const wasPresent = existsSync(resolve(target, 'SKILL.md'));
159
+ // If the target dir itself is a symlink (incl. dangling), refuse before writing through it.
160
+ // (copyRecursive re-checks per entry; this fails fast with a clear message.)
161
+ if (lstatNoFollow(target)?.isSymbolicLink()) {
162
+ console.error(`[agent-workflow-memory] target dir is a symlink — refusing to write through it: ${tildify(target)}`);
163
+ process.exit(1);
164
+ }
165
+ await mkdir(target, { recursive: true });
166
+ await Promise.all(
167
+ PAYLOAD.map((entry) => copyRecursive(resolve(PKG_ROOT, entry), resolve(target, entry), target)),
168
+ );
169
+ console.log(
170
+ `[agent-workflow-memory] ${wasPresent ? 'updated the substrate to' : 'installed'} v${version} -> ${tildify(target)}`,
171
+ );
172
+
173
+ console.log(`
174
+ Next — open your agent inside a project and run the skill:
175
+ • first time in this project -> /agent-workflow-memory
176
+ • project already has it -> /agent-workflow-memory upgrade
177
+
178
+ This command only installs/updates the skill itself (in ${tildify(target)}).
179
+ To update it later, re-run: npx @sabaiway/agent-workflow-memory@latest init`);
180
+ };
181
+
182
+ main().catch((err) => {
183
+ console.error(err);
184
+ process.exit(1);
185
+ });
@@ -0,0 +1,76 @@
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 } from 'node:os';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const INSTALLER = join(dirname(fileURLToPath(import.meta.url)), 'install.mjs');
11
+ const runInstaller = (target) => spawnSync(process.execPath, [INSTALLER, '--dir', target], { encoding: 'utf8' });
12
+
13
+ describe('memory installer — payload + symlink-traversal hardening', () => {
14
+ let dir;
15
+ beforeEach(async () => {
16
+ dir = await mkdtemp(join(tmpdir(), 'aw-memory-install-'));
17
+ });
18
+ afterEach(async () => {
19
+ await rm(dir, { recursive: true, force: true });
20
+ });
21
+
22
+ it('installs the full enumerated payload (capability.json, LICENSE, stamp-takeover.mjs) and not the npm wrapper', () => {
23
+ const target = join(dir, 'agent-workflow-memory');
24
+ const res = runInstaller(target);
25
+ assert.equal(res.status, 0, res.stderr);
26
+ for (const f of [
27
+ 'SKILL.md',
28
+ 'capability.json',
29
+ 'LICENSE',
30
+ 'README.md',
31
+ 'CHANGELOG.md',
32
+ 'scripts/stamp-takeover.mjs',
33
+ 'references/contracts.md',
34
+ 'references/templates/AGENTS.md',
35
+ 'migrations/legacy-stamp-takeover.md',
36
+ ]) {
37
+ assert.ok(existsSync(join(target, f)), `missing installed entry: ${f}`);
38
+ }
39
+ assert.equal(existsSync(join(target, 'bin/install.mjs')), false, 'the npm wrapper must not be copied into the skill dir');
40
+ });
41
+
42
+ it('refuses to write through a symlinked INTERMEDIATE dest component (no leak)', async () => {
43
+ const target = join(dir, 'target');
44
+ const evil = join(dir, 'evil');
45
+ await mkdir(target, { recursive: true });
46
+ await mkdir(evil, { recursive: true });
47
+ await symlink(evil, join(target, 'references'));
48
+ const res = runInstaller(target);
49
+ assert.notEqual(res.status, 0);
50
+ assert.match(res.stderr, /symlink/i);
51
+ assert.deepEqual(await readdir(evil), [], 'nothing should be written through the symlink');
52
+ });
53
+
54
+ it('refuses a DANGLING destination symlink (existsSync would have followed it to absent)', async () => {
55
+ const target = join(dir, 'target');
56
+ await mkdir(target, { recursive: true });
57
+ // references -> a path that does not exist (dangling). copyFile through it would create
58
+ // the file at the dangling target, outside `target`.
59
+ await symlink(join(dir, 'nowhere'), join(target, 'references'));
60
+ const res = runInstaller(target);
61
+ assert.notEqual(res.status, 0);
62
+ assert.match(res.stderr, /symlink/i);
63
+ assert.equal(existsSync(join(dir, 'nowhere')), false, 'dangling target must not be materialised');
64
+ });
65
+
66
+ it('refuses a symlinked TARGET root (no leak)', async () => {
67
+ const real = join(dir, 'real');
68
+ const root = join(dir, 'root');
69
+ await mkdir(real, { recursive: true });
70
+ await symlink(real, root);
71
+ const res = runInstaller(root);
72
+ assert.notEqual(res.status, 0);
73
+ assert.match(res.stderr, /symlink/i);
74
+ assert.deepEqual(await readdir(real), [], 'nothing should be written through the symlinked target');
75
+ });
76
+ });
@@ -0,0 +1,27 @@
1
+ {
2
+ "family": "agent-workflow",
3
+ "schema": 1,
4
+ "name": "agent-workflow-memory",
5
+ "kind": "memory-substrate",
6
+ "version": "1.0.0",
7
+ "provides": ["context"],
8
+ "roles": {},
9
+ "detect": {
10
+ "installed": {
11
+ "env": "AGENT_WORKFLOW_MEMORY_DIR",
12
+ "default": "~/.claude/skills/agent-workflow-memory",
13
+ "file": "SKILL.md"
14
+ },
15
+ "deployed": {
16
+ "file": "docs/ai/.memory-version"
17
+ }
18
+ },
19
+ "install": { "npm": "@sabaiway/agent-workflow-memory" },
20
+ "uninstall": {
21
+ "removeResolved": "detect.installed",
22
+ "note": "init copies into the skill dir; uninstall removes the resolved detect.installed dir — not an npm dependency"
23
+ },
24
+ "cost": "none",
25
+ "quota": null,
26
+ "provenance": { "author": "sabaiway", "source": "github:sabaiway/agent-workflow" }
27
+ }