@sabaiway/agent-workflow-kit 1.11.0 → 1.12.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,7 +4,39 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
- ## 1.11.0 — One source of truth: the kit reads the methodology live from the installed engine
7
+ ## 1.12.0 — See the whole family, and uninstall it cleanly
8
+
9
+ Two new in-agent modes, built on a single **unified family registry**. `/agent-workflow-kit status`
10
+ shows — read-only — which family members (kit / memory / engine / the two bridges) are installed, at
11
+ what version, and (in a project) what is deployed (`docs/ai`, the version stamps, the hidden-mode
12
+ fence). `/agent-workflow-kit uninstall` is the **guarded teardown**: it reverses what `init` and
13
+ `setup` placed — installed skill dirs + bridge wrappers, and in a project the hidden-mode fence + the
14
+ marker pre-commit hook — but it **never deletes user-authored content** (`docs/ai`, `AGENTS.md`, your
15
+ `.claude/settings.json`); for those it prints the exact commands and lets you run them. It removes only
16
+ what is **provably ours** (a valid manifest, name + kind match; a wrapper symlink that points at our
17
+ source) — anything else is left untouched — and it **previews with `--dry-run` and preflights before
18
+ it touches anything**, so a conflict makes zero changes. The deployment-lineage head stays **`1.3.0`**
19
+ (no `docs/ai` structural change; no migration file). See **AD-017**.
20
+
21
+ ### Added
22
+ - `tools/family-registry.mjs` — the unified, kit-owned registry over every family member (the
23
+ `KNOWN_BACKENDS` precedent, generalized to all five). Resolves each member's `detect.installed`,
24
+ manifest health, and installed version; powers `/agent-workflow-kit status`. A drift-guard test pins
25
+ it to the five in-repo `capability.json` files.
26
+ - `tools/uninstall.mjs` — the guarded uninstaller behind `/agent-workflow-kit uninstall`: a pure
27
+ classifier (`buildPlan`) + a preflight-then-mutate executor (`executePlan`). Four surface classes —
28
+ safe-remove (provably-ours skill dirs), managed-marker (wrapper symlinks / the hidden-mode fence /
29
+ the marker hook), report-only (never-deleted user content), and stop (present-but-not-ours).
30
+ - `tools/fs-safe.mjs` gains `removeTreeManaged` + `unlinkManaged` — the symlink-safe inverses of
31
+ `copyTreeRefresh` / `linkManaged` (refuse to delete through a symlink or outside the root; remove
32
+ only a symlink whose target is ours).
33
+
34
+ ### Changed
35
+ - `tools/manifest/validate.mjs` exports `readAuthoritativeVersion` so the registry reports an installed
36
+ member's version from the same authoritative source the validator checks.
37
+ - The kit's own `capability.json` now declares `uninstall.removeResolved` (uniform with memory +
38
+ engine); the guarded uninstaller's behavior matches it — it removes exactly the resolved
39
+ `detect.installed` dir, so the long-declared teardown is now realized, not just documented.
8
40
 
9
41
  The bounded methodology fragment the kit writes into a project's `AGENTS.md` is now read **live from
10
42
  the installed `@sabaiway/agent-workflow-engine`** — the family's single source of truth. The kit's old
package/README.md CHANGED
@@ -220,6 +220,8 @@ command is printed).
220
220
  | `/agent-workflow-kit upgrade` | existing deployment | reads `docs/ai/.workflow-version`, shows the changelog diff, preserves your authored memory, applies migrations, re-stamps — then prints a **read-only** one-line backend-status line (what's set up vs missing); never installs a bridge — set one up with `/agent-workflow-kit setup` |
221
221
  | `/agent-workflow-kit backends` | any time | **read-only** check of the optional execution-backends (the `codex` / `agy` bridges): what's set up vs missing and the next step. Never writes, never commits, never runs a subscription CLI (credentials = marker-file presence, not a live login). |
222
222
  | `/agent-workflow-kit setup [backend]` | opt-in, any time | **link-only** auto-setup of a bridge: places the bundled bridge skill (only into an absent / empty / managed dir — never overwrites an unmanaged one) + links its wrappers onto `PATH` via managed symlinks (idempotent; refuses to clobber a non-symlink; try `--dry-run` to preview). The binary install + the one-time subscription login stay **manual**: it prints the exact **login** command and points the binary install at each bridge's `setup/README.md`. POSIX wrappers — on Windows use WSL. Never commits, never runs a subscription CLI. |
223
+ | `/agent-workflow-kit status` | any time | **read-only** view of the whole family: which members (kit / memory / engine / the two bridges) are installed and at what version, and — with a project — what's deployed (`docs/ai`, the version stamps, and whether the AI files are git-ignored for hidden mode). Never writes, never commits, never runs a subscription CLI. |
224
+ | `/agent-workflow-kit uninstall` | opt-in, any time | **guarded teardown** — the inverse of `init` / `setup`. Removes only what's **provably ours** (managed skill dirs + bridge wrappers; in a project, the hidden-mode git-ignore block it added + the pre-commit hook it installed); **never deletes** your `docs/ai` / `AGENTS.md` / settings — for those it prints the exact `rm` commands to run by hand. Always `--dry-run` first; preflight-then-mutate; never commits. |
223
225
 
224
226
  It **never auto-commits** and **never overwrites** an existing `AGENTS.md` without asking.
225
227
 
@@ -306,7 +308,9 @@ agent-workflow-kit/
306
308
  │ ├── engine-source.mjs ← live engine fragment read (fail-loud)
307
309
  │ ├── detect-backends.mjs ← read-only backend detector
308
310
  │ ├── setup-backends.mjs ← link-only backend setup
309
- │ ├── fs-safe.mjs ← symlink-safe copy/link
311
+ │ ├── fs-safe.mjs ← symlink-safe copy/link/remove/unlink
312
+ │ ├── family-registry.mjs ← unified family registry (status)
313
+ │ ├── uninstall.mjs ← guarded teardown (uninstall)
310
314
  │ └── release-scan.mjs ← attribution / release gate
311
315
  ├── bridges/ ← bundled bridge skill mirrors (codex / antigravity)
312
316
  ├── launchers/ ← Codex / Devin Desktop / Cursor entries
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '1.11.0'
6
+ version: '1.12.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -96,6 +96,8 @@ Pick the mode from the user's invocation. Auto-detect an existing `docs/ai/` to
96
96
  - **`/agent-workflow-kit upgrade`** — upgrade an existing deployment to the skill's current `version`.
97
97
  - **`/agent-workflow-kit backends`** — read-only environment check: which optional **execution-backends** (the `codex` / `agy` bridges) are set up vs missing. Never writes, never commits, never runs a subscription CLI.
98
98
  - **`/agent-workflow-kit setup [backend]`** — the **link-only**, opt-in companion to `backends`: place the bundled bridge skill + link its wrappers onto `PATH`. **In-agent only** — `init` (npx) never places bridges. The binary install + the interactive subscription login stay **manual** (it prints the exact commands); idempotent; refuses to clobber a non-symlink; never commits, never runs a subscription CLI.
99
+ - **`/agent-workflow-kit status`** — read-only view of the **whole family**: which members (kit / memory / engine / the two bridges) are installed, at what version, and — in a project — what is deployed (`docs/ai`, the version stamps, the hidden-mode fence). Never writes, never commits, never runs a subscription CLI.
100
+ - **`/agent-workflow-kit uninstall`** — the **guarded teardown** companion to `init`/`setup`. Removes what they placed — installed skill dirs + the bridge wrappers — and, in a project, reverses the hidden-mode fence + the marker pre-commit hook. **Never deletes user-authored content** (`docs/ai`, `AGENTS.md`, `.claude/settings.json`): it prints the exact commands for you to run by hand. `--dry-run` first, always; preflight-then-mutate; never commits.
99
101
 
100
102
  ### Version status & the two axes — surface this on every invocation
101
103
 
@@ -235,6 +237,36 @@ For each backend it:
235
237
 
236
238
  **Exit codes:** `0` = done / already set up / only manual steps remain (guidance is never a failure); **non-zero** = a STOP (a dir/symlink it refuses to clobber), a bad argument, a missing bundle, or a native fs error (the underlying reason is preserved in the message).
237
239
 
240
+ ### Mode: status
241
+
242
+ Read-only. Answers *"which family members are installed (and at what version), and what is deployed in this project?"* across the **whole family** — the unified registry behind it (`tools/family-registry.mjs`) aggregates every member's `capability.json`. It **never writes, never commits, and never runs a subscription CLI**.
243
+
244
+ Run `node ${CLAUDE_SKILL_DIR}/tools/family-registry.mjs [--dir <project>]` and present its two-axis table:
245
+
246
+ 1. **Skill axis (always):** per member — `installed` + the manifest health (`ok` / `not-installed` / `foreign` / `stub` / `invalid-manifest` / `unsupported-schema`, the same precedence the backend detector uses) + the installed version (read from the member's own `SKILL.md`, the authoritative source).
247
+ 2. **Deploy axis (`--dir <project>`):** the deployment stamps (`docs/ai/.workflow-version`, `.memory-version`), whether `docs/ai/` exists, and whether the hidden-mode managed fence is present.
248
+
249
+ State plainly that the two version axes stay decoupled (an installed *skill* version is not a project's deployment-lineage stamp — see *Version status & the two axes*). The installed version reflects whatever is on disk under `~/.claude/skills/…`; a stale install shows its real (older) version, honestly.
250
+
251
+ ### Mode: uninstall
252
+
253
+ The **guarded teardown** — the inverse of `init` (the kit + engine skills) + `setup` (the bridges) + a hidden deploy. **In-agent, opt-in**, and built around one hard rule: **it never deletes user-authored content.** Run **`--dry-run` first, always**, show the user the classified plan in plain language, get explicit consent, then re-run with `--yes`. It **never commits**.
254
+
255
+ Run `node ${CLAUDE_SKILL_DIR}/tools/uninstall.mjs [<member>] [--dir <project>] [--bindir <path>] [--dry-run | --yes]`:
256
+
257
+ - `<member>` — limit the skill axis to one member (`agent-workflow-kit` / `-memory` / `-engine` / a bridge); omit for the **whole family**.
258
+ - `--dir <project>` — also reverse the **project-deployment** surfaces in `<project>`.
259
+ - `--bindir <path>` — where the bridge wrappers were linked (default `~/.local/bin`, mirrors `setup`).
260
+ - `--dry-run` — print the plan and change **nothing** (run this first). `--yes` — apply the **auto-removable** set.
261
+
262
+ It classifies every surface into four classes and acts accordingly:
263
+
264
+ - **remove** (safe) — an installed skill dir that is **provably ours** (valid manifest, `name`+`kind` match). A dir present but **not provably ours** (`foreign`/`stub`/`invalid`/unreadable) → **STOP**: left untouched **and reported**, while the teardown still removes the members that ARE ours (a not-ours surface is never clobbered, and never blocks removing the rest — the per-item `setup` posture). **Preflight-then-mutate:** if a mutable surface **changed since the dry-run** (a skill no longer ours, a wrapper turned foreign, a hook that lost our marker, a malformed fence), the run **aborts with zero changes**.
265
+ - **reverse** (managed-marker) — a bridge **wrapper symlink that points at our source** (a foreign/non-symlink one → STOP); the hidden-mode **managed fence** (via the existing `--unhide` path — only the fenced lines); a **pre-commit hook carrying our marker** (an unmarked / user hook → left + reported).
266
+ - **KEEP — never deleted** (report-only) — `docs/ai`, `AGENTS.md`, `CLAUDE.md`, `docs/plans`, and the `.claude/settings.json` `includeCoAuthoredBy` edit. The tool **prints the exact `rm` / `git rm --cached` commands**; the **user** runs them. Surface this in plain language; never delete on their behalf.
267
+
268
+ **Shared globals:** removing `agent-workflow-memory` / `agent-workflow-engine` / a bridge removes a **global** skill that another project on the machine may use — say so before applying. **Windows:** the wrappers are POSIX; the skill-dir + project arms still work, the wrapper arm reports *use WSL*.
269
+
238
270
  ---
239
271
 
240
272
  ## Gotchas
@@ -251,6 +283,7 @@ The non-obvious traps — scan these before bootstrapping or upgrading. Each is
251
283
  - **Conversational language never translates artifacts.** It governs *dialogue only*. Code, identifiers, paths, commands, log output, abbreviations, and every deployed `docs/ai/` / `AGENTS.md` file stay in their source language. See [Communication contract](references/contracts.md#communication-contract).
252
284
  - **Never auto-commit.** Report quality-gate results and wait for explicit approval — in both modes.
253
285
  - **Never leak kit internals to the user.** No ADR ids, tool / function / operation names (`reconcile`, `inject`, `ensureSlot`), marker / slot / fragment / anchor terminology, or verbatim tool stderr in anything the user reads. Translate every tool outcome into plain language a third-party user — who has never read this `SKILL.md` — can understand and act on (e.g. the cap-refusal report in *Mode: upgrade* step 3).
286
+ - **Uninstall never deletes user-authored content, and dry-runs first.** `/agent-workflow-kit uninstall` removes only what is **provably ours** (a managed skill dir / wrapper symlink / fenced block / marker hook) and **prints — never runs** the `rm` / `git rm --cached` for `docs/ai`, the entry-point docs, and `.claude/settings.json`. Always run `--dry-run` first, show the plan, get consent, then `--yes`. A skill dir or symlink that is not provably ours is a STOP, never a clobber (the `setup` posture, inverted). Removing a shared global (memory/engine/a bridge) may affect another project — say so.
254
287
 
255
288
  ---
256
289
 
@@ -306,6 +339,6 @@ Deploy these into `AGENTS.md`; remove rows that don't apply to the stack.
306
339
  - [`references/scripts/`](references/scripts/) — the Node enforcement scripts (caps + staleness + index-freshness gate, 3-tier archive, hook installer) and their unit tests.
307
340
  - [`migrations/`](migrations/) — per-version upgrade steps; see `migrations/README.md`.
308
341
  - [`launchers/`](launchers/) — run the bootstrapper from non-Claude agents (`SKILL.md` is a native Codex skill; a Devin Desktop workflow launcher + install script). See `launchers/README.md`.
309
- - [`tools/`](tools/) — the family-wide tooling the kit **owns and ships**: `manifest/{schema.md,validate.mjs}` (the `capability.json` schema + the validator the kit runs as the memory detector, and root CI invokes), `delegation.mjs` (the executable delegate/fallback decision + hand-off plan), `inject-methodology.mjs` + `engine-source.mjs` (the bounded slot reconciliation — ensure-slot / inject-if-empty / cap; the fragment is read **live** from the installed `agent-workflow-engine` via `engine-source.mjs` — the family's one source of truth, no bundled mirror; fail-loud when the engine is needed but absent), `detect-backends.mjs` (the read-only **backend detector** behind `/agent-workflow-kit backends`, plus the axis-aware `guideFor`), `setup-backends.mjs` (the **link-only** backend setup behind `/agent-workflow-kit setup` — place the bundled bridge + link wrappers), `fs-safe.mjs` (the shared symlink-traversal-safe copy/link primitives both `setup-backends` and the npx installer use), `known-footprint.mjs` + `hide-footprint.mjs` (the **hidden-mode** registry + the single hide-writer behind step 9 / the upgrade reconcile — one managed block in the **project-local** `.git/info/exclude` covering the full AI/agent footprint; pinned by `known-footprint.test.mjs` drift-guard + `hide-footprint.test.mjs` / `.integration.test.mjs`), and `release-scan.mjs` (the attribution-off release gate). The bundled bridge skill mirrors live under [`bridges/`](bridges/) (byte-identical to the repo-root bridges, pinned by `test/bridges-mirror.test.mjs`). See [`tools/manifest/schema.md`](tools/manifest/schema.md).
342
+ - [`tools/`](tools/) — the family-wide tooling the kit **owns and ships**: `manifest/{schema.md,validate.mjs}` (the `capability.json` schema + the validator the kit runs as the memory detector, and root CI invokes), `delegation.mjs` (the executable delegate/fallback decision + hand-off plan), `inject-methodology.mjs` + `engine-source.mjs` (the bounded slot reconciliation — ensure-slot / inject-if-empty / cap; the fragment is read **live** from the installed `agent-workflow-engine` via `engine-source.mjs` — the family's one source of truth, no bundled mirror; fail-loud when the engine is needed but absent), `detect-backends.mjs` (the read-only **backend detector** behind `/agent-workflow-kit backends`, plus the axis-aware `guideFor`), `setup-backends.mjs` (the **link-only** backend setup behind `/agent-workflow-kit setup` — place the bundled bridge + link wrappers), `fs-safe.mjs` (the shared symlink-traversal-safe copy/link/**remove/unlink** primitives that `setup-backends`, the npx installer, and the uninstaller use), `known-footprint.mjs` + `hide-footprint.mjs` (the **hidden-mode** registry + the single hide-writer behind step 9 / the upgrade reconcile — one managed block in the **project-local** `.git/info/exclude` covering the full AI/agent footprint; pinned by `known-footprint.test.mjs` drift-guard + `hide-footprint.test.mjs` / `.integration.test.mjs`), `family-registry.mjs` (the **unified family registry** behind `/agent-workflow-kit status` — aggregates every member's `capability.json`; pinned by a `family-registry.test.mjs` drift-guard), `uninstall.mjs` (the **guarded teardown** behind `/agent-workflow-kit uninstall` — classify each surface, preflight-then-mutate, never delete user-authored content), and `release-scan.mjs` (the attribution-off release gate). The bundled bridge skill mirrors live under [`bridges/`](bridges/) (byte-identical to the repo-root bridges, pinned by `test/bridges-mirror.test.mjs`). See [`tools/manifest/schema.md`](tools/manifest/schema.md).
310
343
  - [`capability.json`](capability.json) — the kit's own `agent-workflow` family manifest (`kind: composition-root`).
311
344
  - [`CHANGELOG.md`](CHANGELOG.md) — version history of this kernel.
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "1.11.0",
6
+ "version": "1.12.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
@@ -15,6 +15,10 @@
15
15
  "deployed": { "file": "docs/ai/.workflow-version" }
16
16
  },
17
17
  "install": { "npm": "@sabaiway/agent-workflow-kit" },
18
+ "uninstall": {
19
+ "removeResolved": "detect.installed",
20
+ "note": "the guarded `/agent-workflow-kit uninstall` removes the resolved detect.installed dir — never user-authored docs/ai (those are reported, not deleted)"
21
+ },
18
22
  "cost": "none",
19
23
  "quota": null,
20
24
  "provenance": { "author": "sabaiway", "source": "github:sabaiway/agent-workflow" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ // family-registry.mjs — the unified, kit-owned registry over EVERY agent-workflow family member.
3
+ //
4
+ // Until now "who the members are" was split across three disjoint kit-owned tables: KNOWN_BACKENDS
5
+ // (the 2 bridges, detect-backends.mjs), KIT_OWN_PATHS/KNOWN_FOOTPRINT (the hidden-mode paths,
6
+ // known-footprint.mjs), and the 5 per-member capability.json files. This module is the single
7
+ // authoritative aggregation: it answers "what is installed, what version, what kind" (the SKILL
8
+ // axis) and "what is deployed in this project" (the deploy axis). It is the substrate the read-only
9
+ // `/agent-workflow-kit status` mode and the guarded `/agent-workflow-kit uninstall` both consume.
10
+ //
11
+ // Source of truth = the in-tool FAMILY_MEMBERS table (the AD-008 KNOWN_BACKENDS precedent): a member
12
+ // that is NOT installed has no manifest on disk to read, so the enumeration + detect/install facts
13
+ // must live here. A drift-guard test (family-registry.test.mjs) pins FAMILY_MEMBERS to the 5 in-repo
14
+ // capability.json files, so the table cannot silently drift from the manifests it mirrors.
15
+ //
16
+ // Pure, dependency-injectable (fs/env/home/validator are deps), dependency-free, Node >= 18. No
17
+ // side effects on import (the isDirectRun idiom) — tests import the helpers with nothing run.
18
+
19
+ import { existsSync, statSync, readFileSync } from 'node:fs';
20
+ import { join, resolve } from 'node:path';
21
+ import { pathToFileURL } from 'node:url';
22
+ import os from 'node:os';
23
+ import { resolveDir } from './detect-backends.mjs';
24
+ import { validateManifest, readAuthoritativeVersion, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
25
+ import { START_MARKER, excludePath } from './hide-footprint.mjs';
26
+
27
+ // ── manifestState values (the detect-backends precedence, generalized to any member kind) ──────────
28
+ export const NOT_INSTALLED = 'not-installed';
29
+ export const UNSUPPORTED_SCHEMA = 'unsupported-schema';
30
+ export const INVALID_MANIFEST = 'invalid-manifest';
31
+ export const STUB = 'stub';
32
+ export const FOREIGN = 'foreign';
33
+ export const OK = 'ok';
34
+ // The marker could not be probed (a non-ENOENT fs error — EACCES/EIO). Surfaced explicitly instead of
35
+ // being masked as not-installed (no silent failure); uninstall treats it as "do not touch" (skip).
36
+ export const UNKNOWN = 'unknown';
37
+
38
+ // ── the unified registry ───────────────────────────────────────────────────────
39
+ // One entry per family member. `installed` is the detect.installed spec (env + home-relative default
40
+ // + marker file); `deployed` is the project-relative stamp a deploy writes (kit + memory only);
41
+ // `npm` is the install package (null for the bridges, which are placed by `setup`, not npm);
42
+ // `wrapperCmds` is the deduped roles[].cmd set the `setup` linker creates on PATH (bridges only).
43
+ // Kept in lockstep with the 5 in-repo capability.json by the drift-guard test. The two release skills
44
+ // (release-engineering / release-marketing) are deliberately NOT here — they are not family members
45
+ // (AD-013): no capability.json, not in the kit tarball, not in the role vocabulary.
46
+ export const FAMILY_MEMBERS = [
47
+ {
48
+ name: 'agent-workflow-kit',
49
+ kind: 'composition-root',
50
+ installed: { env: 'AGENT_WORKFLOW_KIT_DIR', default: '~/.claude/skills/agent-workflow-kit', file: 'SKILL.md' },
51
+ deployed: { file: 'docs/ai/.workflow-version' },
52
+ npm: '@sabaiway/agent-workflow-kit',
53
+ wrapperCmds: [],
54
+ },
55
+ {
56
+ name: 'agent-workflow-memory',
57
+ kind: 'memory-substrate',
58
+ installed: { env: 'AGENT_WORKFLOW_MEMORY_DIR', default: '~/.claude/skills/agent-workflow-memory', file: 'SKILL.md' },
59
+ deployed: { file: 'docs/ai/.memory-version' },
60
+ npm: '@sabaiway/agent-workflow-memory',
61
+ wrapperCmds: [],
62
+ },
63
+ {
64
+ name: 'agent-workflow-engine',
65
+ kind: 'methodology-engine',
66
+ installed: { env: 'AGENT_WORKFLOW_ENGINE_DIR', default: '~/.claude/skills/agent-workflow-engine', file: 'SKILL.md' },
67
+ deployed: null,
68
+ npm: '@sabaiway/agent-workflow-engine',
69
+ wrapperCmds: [],
70
+ },
71
+ {
72
+ name: 'codex-cli-bridge',
73
+ kind: 'execution-backend',
74
+ installed: { env: 'CODEX_CLI_BRIDGE_DIR', default: '~/.claude/skills/codex-cli-bridge', file: 'SKILL.md' },
75
+ deployed: null,
76
+ npm: null,
77
+ wrapperCmds: ['codex-exec', 'codex-review'],
78
+ },
79
+ {
80
+ name: 'antigravity-cli-bridge',
81
+ kind: 'execution-backend',
82
+ installed: { env: 'ANTIGRAVITY_CLI_BRIDGE_DIR', default: '~/.claude/skills/antigravity-cli-bridge', file: 'SKILL.md' },
83
+ deployed: null,
84
+ npm: null,
85
+ wrapperCmds: ['agy-run'],
86
+ },
87
+ ];
88
+
89
+ // A GLOBAL skill (lives under ~/.claude/skills) may be shared by other projects on the host — the
90
+ // uninstaller warns before removing one (there is no cross-project dependency tracking). All current
91
+ // members are global skills; the field is explicit so the warning is data-driven, not hardcoded.
92
+ export const isGlobalSkill = (member) => member.kind !== undefined; // every member is a global skill today
93
+
94
+ // ── pure probes ──────────────────────────────────────────────────────────────────
95
+ // Wrapped marker probe → 'present' (a regular file) | 'absent' (ENOENT / not a file) | 'unknown' (a
96
+ // non-ENOENT fs error, e.g. EACCES). 'unknown' is NOT collapsed to 'absent': a permission error must
97
+ // surface, never be masked as not-installed (no silent failure) — and uninstall then leaves it alone.
98
+ const probeMarker = (path, deps = {}) => {
99
+ const exists = deps.exists ?? existsSync;
100
+ const stat = deps.stat ?? statSync;
101
+ try {
102
+ if (!exists(path)) return 'absent';
103
+ return stat(path).isFile() ? 'present' : 'absent';
104
+ } catch (err) {
105
+ return err && err.code === 'ENOENT' ? 'absent' : 'unknown';
106
+ }
107
+ };
108
+
109
+ // Pure manifestState classifier — the detect-backends precedence, generalized to a member's own
110
+ // expected name + kind: not-installed → unsupported-schema → invalid-manifest → stub → foreign → ok.
111
+ const classifyState = (markerPresent, report, member) => {
112
+ if (!markerPresent) return NOT_INSTALLED;
113
+ if (report.result === UNSUPPORTED) return UNSUPPORTED_SCHEMA;
114
+ if (report.result === INVALID) return INVALID_MANIFEST;
115
+ if (report.available === false) return STUB;
116
+ if (report.kind !== member.kind || report.name !== member.name) return FOREIGN;
117
+ return OK;
118
+ };
119
+
120
+ // ── the SKILL axis ─────────────────────────────────────────────────────────────
121
+ // classifyMember → { name, kind, installed, skillDir, manifestState, version }. Reuses resolveDir
122
+ // (detect-backends), validateManifest + readAuthoritativeVersion (the manifest validator) — one
123
+ // authoritative version reader, no second drifting source. `version` is set only for an `ok` member.
124
+ export const classifyMember = (member, deps = {}) => {
125
+ const validate = deps.validate ?? validateManifest;
126
+ const readVersion = deps.readVersion ?? readAuthoritativeVersion;
127
+ const getenv = deps.getenv ?? process.env;
128
+ const home = deps.home ?? os.homedir();
129
+
130
+ const skillDir = resolveDir({ env: member.installed.env, default: member.installed.default }, getenv, home);
131
+ const marker = probeMarker(join(skillDir, member.installed.file), deps);
132
+ // A marker we cannot probe (EACCES/EIO) → 'unknown': reported, but NOT installed (so uninstall never
133
+ // removes a dir whose ownership it could not verify). Distinct from 'not-installed' (genuinely absent).
134
+ if (marker === 'unknown') {
135
+ return { name: member.name, kind: member.kind, installed: false, skillDir, manifestState: UNKNOWN, version: null };
136
+ }
137
+ const markerPresent = marker === 'present';
138
+ const report = markerPresent ? validate(skillDir) : { result: NOT_INSTALLED };
139
+ const manifestState = classifyState(markerPresent, report, member);
140
+ const installed = manifestState !== NOT_INSTALLED;
141
+ const version = manifestState === OK ? readVersion(skillDir).version ?? null : null;
142
+
143
+ return { name: member.name, kind: member.kind, installed, skillDir: installed ? skillDir : null, manifestState, version };
144
+ };
145
+
146
+ export const surveyFamily = (deps = {}) => FAMILY_MEMBERS.map((member) => classifyMember(member, deps));
147
+
148
+ // ── the DEPLOY axis ──────────────────────────────────────────────────────────────
149
+ // Read a one-line semver stamp (docs/ai/.workflow-version etc.). Returns the trimmed version or null.
150
+ const readStamp = (path, deps = {}) => {
151
+ const exists = deps.exists ?? existsSync;
152
+ const read = deps.readFile ?? readFileSync;
153
+ try {
154
+ if (!exists(path)) return null;
155
+ const v = String(read(path, 'utf8')).trim();
156
+ return v.length ? v : null;
157
+ } catch {
158
+ return null;
159
+ }
160
+ };
161
+
162
+ // Is our hidden-mode managed fence present? Resolve the exclude file via the SAME git-path-aware path
163
+ // hide-footprint uses (`git rev-parse --git-path info/exclude`), so a linked worktree / submodule is
164
+ // handled correctly (not the hardcoded `.git/info/exclude`). If git is unavailable or this is not a
165
+ // repo, fall back to the conventional path; any read error → not present (best-effort, read-only).
166
+ const hasHiddenFence = (projectDir, deps = {}) => {
167
+ const exists = deps.exists ?? existsSync;
168
+ const read = deps.readFile ?? readFileSync;
169
+ const ep = (() => {
170
+ try {
171
+ return excludePath(deps, projectDir);
172
+ } catch {
173
+ return join(projectDir, '.git', 'info', 'exclude');
174
+ }
175
+ })();
176
+ try {
177
+ return exists(ep) && String(read(ep, 'utf8')).includes(START_MARKER);
178
+ } catch {
179
+ return false;
180
+ }
181
+ };
182
+
183
+ // surveyProject → the deploy axis for a target project dir: the per-member deployment stamps, whether
184
+ // docs/ai/ exists, and whether the hidden-mode fence is present. Pure (fs reads only, all injectable),
185
+ // no git subprocess — the read-only `status` view must never mutate or spawn anything.
186
+ export const surveyProject = (projectDir, deps = {}) => {
187
+ const exists = deps.exists ?? existsSync;
188
+ const dir = resolve(projectDir);
189
+ const stamps = FAMILY_MEMBERS
190
+ .filter((m) => m.deployed)
191
+ .map((m) => ({ name: m.name, file: m.deployed.file, version: readStamp(join(dir, m.deployed.file), deps) }));
192
+ const docsAiPresent = (() => {
193
+ try {
194
+ return exists(join(dir, 'docs', 'ai'));
195
+ } catch {
196
+ return false;
197
+ }
198
+ })();
199
+ const deployed = stamps.some((s) => s.version != null) || docsAiPresent;
200
+ return { dir, deployed, docsAiPresent, hiddenFence: hasHiddenFence(dir, deps), stamps };
201
+ };
202
+
203
+ // ── report ───────────────────────────────────────────────────────────────────────
204
+ const pad = (s, n) => (s.length >= n ? s : s + ' '.repeat(n - s.length));
205
+
206
+ export const formatStatus = (family, project = null) => {
207
+ const lines = ['agent-workflow family — installed skills (skill axis)', ''];
208
+ for (const m of family) {
209
+ const ver = m.version ? `v${m.version}` : '—';
210
+ lines.push(` ${pad(m.name, 26)}[${pad(m.manifestState, 16)}] ${pad(ver, 10)} ${m.kind}`);
211
+ }
212
+ if (project) {
213
+ lines.push('', `project deployment (${project.dir})`, '');
214
+ if (!project.deployed) {
215
+ lines.push(' no agent-workflow deployment detected here (no docs/ai, no version stamp).');
216
+ } else {
217
+ for (const s of project.stamps) {
218
+ lines.push(` ${pad(s.file, 26)}${s.version ?? '—'}`);
219
+ }
220
+ lines.push(` ${pad('docs/ai present', 26)}${project.docsAiPresent ? 'yes' : 'no'}`);
221
+ lines.push(` ${pad('hidden-mode fence', 26)}${project.hiddenFence ? 'present' : 'absent'}`);
222
+ }
223
+ }
224
+ return lines.join('\n');
225
+ };
226
+
227
+ // ── CLI ────────────────────────────────────────────────────────────────────────
228
+ const parseArgs = (argv) => {
229
+ const dirFlag = argv.indexOf('--dir');
230
+ return { help: argv.includes('--help') || argv.includes('-h'), dir: dirFlag >= 0 ? argv[dirFlag + 1] : undefined };
231
+ };
232
+
233
+ const main = (argv) => {
234
+ const args = parseArgs(argv);
235
+ if (args.help) {
236
+ console.log(`family-registry — read-only view of the agent-workflow family.
237
+
238
+ Usage:
239
+ node family-registry.mjs [--dir <project>] # skill axis always; deploy axis when --dir is given
240
+
241
+ Detection only — never writes, never commits, never runs a subscription CLI.`);
242
+ return;
243
+ }
244
+ const family = surveyFamily();
245
+ const project = args.dir ? surveyProject(args.dir) : null;
246
+ console.log(formatStatus(family, project));
247
+ };
248
+
249
+ const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
250
+ if (isDirectRun) main(process.argv.slice(2));
@@ -0,0 +1,189 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFileSync } from 'node:fs';
4
+ import { resolve, dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import {
7
+ FAMILY_MEMBERS,
8
+ classifyMember,
9
+ surveyFamily,
10
+ surveyProject,
11
+ NOT_INSTALLED,
12
+ UNSUPPORTED_SCHEMA,
13
+ INVALID_MANIFEST,
14
+ STUB,
15
+ FOREIGN,
16
+ OK,
17
+ UNKNOWN,
18
+ } from './family-registry.mjs';
19
+ import { VALID, INVALID, UNSUPPORTED } from './manifest/validate.mjs';
20
+ import { START_MARKER } from './hide-footprint.mjs';
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const REPO_ROOT = resolve(__dirname, '../..'); // agent-workflow-kit/tools → repo root
24
+
25
+ // Build classifyMember deps that present the marker, with an injectable validate/readVersion.
26
+ const installedDeps = ({ report, version = '9.9.9', home = '/home/test' }) => ({
27
+ exists: () => true,
28
+ stat: () => ({ isFile: () => true }),
29
+ getenv: {},
30
+ home,
31
+ validate: () => report,
32
+ readVersion: () => ({ version }),
33
+ });
34
+
35
+ const KIT = FAMILY_MEMBERS.find((m) => m.name === 'agent-workflow-kit');
36
+
37
+ // ── classifyMember ───────────────────────────────────────────────────────────────
38
+
39
+ describe('classifyMember', () => {
40
+ it('marks an absent marker as not-installed (no skillDir, no version)', () => {
41
+ const r = classifyMember(KIT, { exists: () => false, getenv: {}, home: '/home/test' });
42
+ assert.equal(r.manifestState, NOT_INSTALLED);
43
+ assert.equal(r.installed, false);
44
+ assert.equal(r.skillDir, null);
45
+ assert.equal(r.version, null);
46
+ });
47
+
48
+ it('classifies a valid matching manifest as ok and reports the authoritative version', () => {
49
+ const r = classifyMember(KIT, installedDeps({
50
+ report: { result: VALID, name: 'agent-workflow-kit', kind: 'composition-root', available: true },
51
+ version: '1.12.0',
52
+ }));
53
+ assert.equal(r.manifestState, OK);
54
+ assert.equal(r.installed, true);
55
+ assert.equal(r.version, '1.12.0');
56
+ assert.match(r.skillDir, /\.claude\/skills\/agent-workflow-kit$/);
57
+ });
58
+
59
+ it('classifies a wrong name/kind as foreign (never ours, never removed by uninstall)', () => {
60
+ const r = classifyMember(KIT, installedDeps({
61
+ report: { result: VALID, name: 'something-else', kind: 'composition-root', available: true },
62
+ }));
63
+ assert.equal(r.manifestState, FOREIGN);
64
+ assert.equal(r.version, null); // version only for ok
65
+ });
66
+
67
+ it('classifies available:false as a stub', () => {
68
+ const r = classifyMember(KIT, installedDeps({
69
+ report: { result: VALID, name: 'agent-workflow-kit', kind: 'composition-root', available: false },
70
+ }));
71
+ assert.equal(r.manifestState, STUB);
72
+ });
73
+
74
+ it('maps validator INVALID → invalid-manifest and UNSUPPORTED → unsupported-schema', () => {
75
+ assert.equal(classifyMember(KIT, installedDeps({ report: { result: INVALID } })).manifestState, INVALID_MANIFEST);
76
+ assert.equal(classifyMember(KIT, installedDeps({ report: { result: UNSUPPORTED } })).manifestState, UNSUPPORTED_SCHEMA);
77
+ });
78
+
79
+ it('surfaces an EACCES marker probe as "unknown" — never masked as not-installed', () => {
80
+ const eacces = () => { throw Object.assign(new Error('EACCES'), { code: 'EACCES' }); };
81
+ const r = classifyMember(KIT, { exists: eacces, getenv: {}, home: '/home/test' });
82
+ assert.equal(r.manifestState, UNKNOWN);
83
+ assert.equal(r.installed, false); // not removed — ownership could not be verified
84
+ });
85
+
86
+ it('resolves the skill dir from the env override when set (resolveDir reuse)', () => {
87
+ const r = classifyMember(KIT, {
88
+ exists: () => true,
89
+ stat: () => ({ isFile: () => true }),
90
+ getenv: { AGENT_WORKFLOW_KIT_DIR: '/custom/kit' },
91
+ home: '/home/test',
92
+ validate: () => ({ result: VALID, name: 'agent-workflow-kit', kind: 'composition-root', available: true }),
93
+ readVersion: () => ({ version: '1.12.0' }),
94
+ });
95
+ assert.equal(r.skillDir, '/custom/kit');
96
+ });
97
+ });
98
+
99
+ describe('surveyFamily', () => {
100
+ it('returns one row per member; all not-installed when no markers present', () => {
101
+ const rows = surveyFamily({ exists: () => false, getenv: {}, home: '/home/test' });
102
+ assert.equal(rows.length, FAMILY_MEMBERS.length);
103
+ assert.ok(rows.every((r) => r.manifestState === NOT_INSTALLED));
104
+ });
105
+ });
106
+
107
+ // ── surveyProject ────────────────────────────────────────────────────────────────
108
+
109
+ describe('surveyProject', () => {
110
+ const projectDeps = ({ files }) => ({
111
+ exists: (p) => Object.prototype.hasOwnProperty.call(files, p) || Object.keys(files).some((k) => k === p),
112
+ readFile: (p) => {
113
+ if (!Object.prototype.hasOwnProperty.call(files, p)) throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
114
+ return files[p];
115
+ },
116
+ });
117
+
118
+ it('reports deployed + stamps + docs/ai + hidden fence', () => {
119
+ const dir = '/proj';
120
+ const files = {
121
+ [join(dir, 'docs/ai/.workflow-version')]: '1.3.0\n',
122
+ [join(dir, 'docs/ai/.memory-version')]: '1.1.1\n',
123
+ [join(dir, 'docs', 'ai')]: '',
124
+ [join(dir, '.git', 'info', 'exclude')]: `# user rule\n${START_MARKER}\n/AGENTS.md\n`,
125
+ };
126
+ const r = surveyProject(dir, projectDeps({ files }));
127
+ assert.equal(r.deployed, true);
128
+ assert.equal(r.docsAiPresent, true);
129
+ assert.equal(r.hiddenFence, true);
130
+ assert.deepEqual(
131
+ r.stamps.map((s) => [s.name, s.version]),
132
+ [['agent-workflow-kit', '1.3.0'], ['agent-workflow-memory', '1.1.1']],
133
+ );
134
+ });
135
+
136
+ it('reports not-deployed when there is no docs/ai and no stamp', () => {
137
+ const r = surveyProject('/empty', projectDeps({ files: {} }));
138
+ assert.equal(r.deployed, false);
139
+ assert.equal(r.hiddenFence, false);
140
+ assert.ok(r.stamps.every((s) => s.version === null));
141
+ });
142
+ });
143
+
144
+ // ── drift-guard: FAMILY_MEMBERS ⟷ the 5 in-repo capability.json (the AD-008 lockstep pattern) ──────
145
+
146
+ describe('FAMILY_MEMBERS drift-guard', () => {
147
+ const readManifest = (memberName) =>
148
+ JSON.parse(readFileSync(resolve(REPO_ROOT, memberName, 'capability.json'), 'utf8'));
149
+
150
+ // deduped roles[].cmd, in first-seen order (mirrors detect-backends' wrapperCmds derivation).
151
+ const dedupedCmds = (manifest) => {
152
+ const seen = new Set();
153
+ const out = [];
154
+ for (const role of Object.values(manifest.roles ?? {})) {
155
+ if (role && typeof role.cmd === 'string' && !seen.has(role.cmd)) {
156
+ seen.add(role.cmd);
157
+ out.push(role.cmd);
158
+ }
159
+ }
160
+ return out;
161
+ };
162
+
163
+ it('has exactly the five family members and no release skills', () => {
164
+ assert.equal(FAMILY_MEMBERS.length, 5);
165
+ const names = FAMILY_MEMBERS.map((m) => m.name);
166
+ assert.ok(!names.includes('release-engineering'));
167
+ assert.ok(!names.includes('release-marketing'));
168
+ });
169
+
170
+ for (const member of FAMILY_MEMBERS) {
171
+ it(`${member.name}: name/kind/detect.installed/deployed/npm/wrapperCmds match the in-repo manifest`, () => {
172
+ const m = readManifest(member.name);
173
+ assert.equal(m.name, member.name);
174
+ assert.equal(m.kind, member.kind);
175
+
176
+ assert.equal(m.detect.installed.env, member.installed.env);
177
+ assert.equal(m.detect.installed.default, member.installed.default);
178
+ assert.equal(m.detect.installed.file, member.installed.file);
179
+
180
+ if (member.deployed) assert.equal(m.detect.deployed.file, member.deployed.file);
181
+ else assert.equal(m.detect?.deployed ?? null, null);
182
+
183
+ if (member.npm) assert.equal(m.install.npm, member.npm);
184
+ else assert.equal(m.install?.npm ?? null, null);
185
+
186
+ assert.deepEqual(dedupedCmds(m), member.wrapperCmds);
187
+ });
188
+ }
189
+ });