@sabaiway/agent-workflow-kit 1.7.0 → 1.8.1

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,67 @@ 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.8.1 — Fix: `npx … init` ran nothing (the installer's own run-guard mis-fired under npx)
8
+
9
+ 1.8.0 set out to fix "`npx <pkg> init` quietly did nothing" — and shipped a *second*, unrelated
10
+ silent no-op in the same spot. The reported symptom: `npx @sabaiway/agent-workflow-kit@latest init`
11
+ installs the package, prints the npx "Ok to proceed?" line, and then **prints nothing and does
12
+ nothing** — none of 1.8.0's new DX messaging, no install, exit 0.
13
+
14
+ Root cause: the bottom-of-file run-guard that gates `main()` so importing the module has no side
15
+ effects:
16
+
17
+ ```js
18
+ const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
19
+ ```
20
+
21
+ npx never runs `bin/install.mjs` by its real path — it runs the `node_modules/.bin/agent-workflow-kit`
22
+ **symlink** to it. Node resolves `import.meta.url` to the real file but leaves `process.argv[1]` as the
23
+ symlink path, so the string compare is always false, `main()` never runs, and the process exits 0
24
+ without a word. (Running `node bin/install.mjs` directly — as the test suite did — has no symlink, which
25
+ is why every test passed while real `npx` was broken.)
26
+
27
+ - **Fix:** the guard now compares **real paths** (`realpathSync` on both sides), which collapses the
28
+ `.bin` symlink so direct and npx invocations both register as a direct run; it also holds under
29
+ `--preserve-symlinks`. Import-with-no-`argv[1]` and a missing file still fall through to `false`, so
30
+ importing the module continues to run nothing.
31
+ - **Regression test:** a new case invokes the installer **through a symlink** (the exact `.bin` shim
32
+ npx uses) and asserts it both prints and writes the payload — the previous suite never exercised a
33
+ symlinked invocation, so the bug slipped through.
34
+
35
+ Installer bugfix only — no `docs/ai` structural change, deployment-lineage head stays **`1.3.0`**, no
36
+ migration.
37
+
38
+ ## 1.8.0 — Stale-version DX: `@latest` everywhere + a no-network never-downgrade gate
39
+
40
+ A returning user ran the headline `npx @sabaiway/agent-workflow-kit init` and it quietly did nothing:
41
+ a bare `npx <pkg> init` (no `@latest`) reuses the npx cache and re-runs an **older cached build** of
42
+ the installer, which exits 0 and reports it "updated" — to the same stale version. This release makes
43
+ that mistake hard to miss while **the installer itself stays 100% network-free** — the only thing
44
+ that ever contacts npm is npx resolving `@latest`, exactly as it already does (the no-phone-home
45
+ principle is preserved; see AD-012):
46
+
47
+ - **`@latest` is the documented default everywhere.** Every prescribing surface (both READMEs, the
48
+ bridge `SKILL.md` files + their bundled mirrors, the installer `--help` / header) now shows
49
+ `npx @sabaiway/agent-workflow-kit@latest init`. A new drift guard
50
+ (`test/init-command-uses-latest.test.mjs`) fails the build if a bare form sneaks back in (historical
51
+ contexts — CHANGELOG / `releases/` / `migrations/` — are exempt).
52
+ - **Never-downgrade gate (no network).** `init` reads the installed skill's version from
53
+ `SKILL.md` **before** writing; if the installed kit is **newer** than the version you ran (the exact
54
+ stale-cache signature), it **refuses** (nonzero) and points at `@latest`, rather than silently
55
+ overwriting a newer install with old code. `--force` overrides. A legacy install with no version
56
+ stamp still upgrades cleanly.
57
+ - **No-op re-run hint.** When `init` refreshes the skill with the *same* version it already had, it
58
+ says so and points at `@latest` — the no-network signal that catches the reported scenario.
59
+ - **In-agent skill** (`SKILL.md`): surfaces a one-line version status (project `docs/ai/.workflow-version`
60
+ vs the lineage head) + routes (bootstrap / upgrade / current), spells out the **two independent
61
+ version axes** (project deployment vs kit freshness — the latter is the npx installer's job, never
62
+ this skill's), and tells you to **restart the session** after refreshing the kit so the new skill
63
+ files load.
64
+
65
+ New users are unaffected (an empty npx cache already fetches `latest`); this targets the returning-user
66
+ trap. No `docs/ai` structural change → the deployment-lineage head stays **`1.3.0`**; no migration.
67
+
7
68
  ## 1.7.0 — Link-only backend auto-setup; bridges bundled in the tarball
8
69
 
9
70
  The optional execution-backend bridges (`codex-cli-bridge` → `codex`, `antigravity-cli-bridge` →
package/README.md CHANGED
@@ -18,7 +18,7 @@ decisions. Works with Claude Code, Codex, Cursor, and any agent that reads `AGEN
18
18
  **One command to start:**
19
19
 
20
20
  ```bash
21
- npx @sabaiway/agent-workflow-kit init
21
+ npx @sabaiway/agent-workflow-kit@latest init
22
22
  ```
23
23
 
24
24
  <sub>This installs the **global skill** — deploying into a project is a separate step ([below](#-install)).</sub>
@@ -134,7 +134,7 @@ Hidden changes how the files are *tracked*, not where agents find them.
134
134
  ### 1. Install the global skill — once per machine
135
135
 
136
136
  ```bash
137
- npx @sabaiway/agent-workflow-kit init
137
+ npx @sabaiway/agent-workflow-kit@latest init
138
138
  ```
139
139
 
140
140
  `init` installs/refreshes the skill at `~/.claude/skills/agent-workflow-kit/` and wires launchers for
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.7.0'
6
+ version: '1.8.1'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -85,6 +85,22 @@ Pick the mode from the user's invocation. Auto-detect an existing `docs/ai/` to
85
85
  - **`/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.
86
86
  - **`/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.
87
87
 
88
+ ### Version status & the two axes — surface this on every invocation
89
+
90
+ Before acting, read `docs/ai/.workflow-version` (the project's stamp), state a one-line status, then route:
91
+
92
+ - **absent** → bootstrap (a fresh deployment).
93
+ - **stamp < `1.3.0`** (the deployment-lineage head) → `upgrade`.
94
+ - **stamp == `1.3.0`** → already current; only the stamp-independent methodology-slot reconcile may run.
95
+ - **stamp > head / unparseable** → STOP — never-downgrade gate (see *Mode: upgrade* step 2).
96
+
97
+ **Two independent version axes — never conflate them:**
98
+
99
+ 1. **Project deployment** — `docs/ai/.workflow-version` vs the lineage head (`1.3.0`). This is the **only** axis this skill compares.
100
+ 2. **Kit freshness** — this skill's own files vs the published npm package. That is the **npx installer's** job: `npx @sabaiway/agent-workflow-kit@latest init` (it refuses a stale-cache downgrade by comparing the version on disk — **no network**). This skill never checks npm, and the package version (e.g. `1.x`) is **not** the lineage head.
101
+
102
+ **Refreshed the kit but nothing changed?** The skill you are running is whatever was on disk when the session started. After `npx @sabaiway/agent-workflow-kit@latest init` updates `~/.claude/skills/agent-workflow-kit/`, **restart the session** so the agent reloads the new skill files (the slash command + this `SKILL.md`).
103
+
88
104
  ### Mode: bootstrap
89
105
 
90
106
  > Bundled sources below (templates, scripts) live in **this skill's own directory** — `${CLAUDE_SKILL_DIR}/` in Claude Code, or the folder containing this `SKILL.md` in Codex / other agents. Use that as the copy/read source; the working directory is the **target project**, not the skill.
package/bin/install.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // One-shot installer for @sabaiway/agent-workflow-kit.
3
3
  //
4
- // npx @sabaiway/agent-workflow-kit init
4
+ // npx @sabaiway/agent-workflow-kit@latest init
5
5
  //
6
6
  // Copies the kit into the canonical skill home (~/.claude/skills/agent-workflow-kit),
7
7
  // then runs the cross-agent launcher (auto-detects Codex / Devin Desktop — only touches tools
@@ -14,14 +14,18 @@
14
14
  // docs/ai deployment — see README "Use".
15
15
  //
16
16
  // No telemetry, no phone-home: adoption is the npm registry's public, passive per-version
17
- // download numbers (api.npmjs.org/downloads). Nothing here contacts a server.
17
+ // download numbers (api.npmjs.org/downloads). Nothing here contacts a server — including the
18
+ // stale-version defenses below, which compare the version already on disk (the installed SKILL.md)
19
+ // against this runner's own version, never the registry. That is why `@latest` (above) is the
20
+ // documented form: a bare `npx … init` can reuse an OLDER cached build of this installer, so a
21
+ // returning user must bypass the cache to actually upgrade. See decisions.md AD-012.
18
22
  //
19
23
  // Dependency-free, Node >= 18.
20
24
 
21
25
  import { readFile, mkdir } from 'node:fs/promises';
22
- import { existsSync, lstatSync } from 'node:fs';
26
+ import { existsSync, lstatSync, realpathSync } from 'node:fs';
23
27
  import { dirname, resolve } from 'node:path';
24
- import { fileURLToPath, pathToFileURL } from 'node:url';
28
+ import { fileURLToPath } from 'node:url';
25
29
  import { homedir } from 'node:os';
26
30
  import { spawnSync } from 'node:child_process';
27
31
  import { copyTreeRefresh } from '../tools/fs-safe.mjs';
@@ -57,6 +61,58 @@ const readVersion = async () => {
57
61
  }
58
62
  };
59
63
 
64
+ // Dependency-free semver: parse the leading `x.y.z` (prerelease/build ignored — kit versions are
65
+ // plain). compareSemver returns -1 | 0 | 1, or null when either side is unparseable (legacy installs
66
+ // predate any version stamp). No `let`: a small functional comparison (AGENTS.md §2.3).
67
+ const parseSemver = (str) => {
68
+ const m = typeof str === 'string' ? str.trim().match(/^(\d+)\.(\d+)\.(\d+)/) : null;
69
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
70
+ };
71
+
72
+ const compareSemver = (a, b) => {
73
+ const pa = parseSemver(a);
74
+ const pb = parseSemver(b);
75
+ if (!pa || !pb) return null;
76
+ const firstDiff = [0, 1, 2].map((i) => (pa[i] === pb[i] ? 0 : pa[i] < pb[i] ? -1 : 1)).find((c) => c !== 0);
77
+ return firstDiff ?? 0;
78
+ };
79
+
80
+ // Extract the version that is a DIRECT child of the top-level `metadata:` key — never a top-level
81
+ // or deeper-nested decoy `version:` (mirrors the manifest validator's rigor; the kit ships manifest
82
+ // fixtures that probe exactly those decoys). Pure string walk over the frontmatter block, no deps.
83
+ const metadataVersion = (frontmatter) => {
84
+ const lines = frontmatter.split(/\r?\n/);
85
+ const metaIdx = lines.findIndex((l) => /^metadata:[ \t]*$/.test(l));
86
+ if (metaIdx === -1) return null;
87
+ const after = lines.slice(metaIdx + 1);
88
+ const dedent = after.findIndex((l) => /^[^ \t]/.test(l)); // a column-0 line closes the metadata block
89
+ const block = dedent === -1 ? after : after.slice(0, dedent);
90
+ // Direct children share the first child's indent; a nested decoy is MORE indented, so a
91
+ // `<baseIndent>version:` prefix match excludes it. baseIndent is non-empty, so a top-level
92
+ // `version:` (column 0, and before `metadata:` anyway) is excluded too.
93
+ const baseIndent = block.length ? (block[0].match(/^[ \t]*/)?.[0] ?? '') : '';
94
+ const verLine = block.find((l) => l.startsWith(`${baseIndent}version:`));
95
+ return verLine?.match(/version:[ \t]*['"]?(\d+\.\d+\.\d+)['"]?/)?.[1] ?? null;
96
+ };
97
+
98
+ // The installed version is read from the target's SKILL.md frontmatter (`metadata.version`) — the
99
+ // manifest's canonical `detect.installed.file`, present even on legacy installs that predate
100
+ // capability.json. Returns the semver string, or null when ABSENT / has no parseable stamp (legacy
101
+ // → no gate). A SKILL.md that EXISTS but cannot be read is NOT swallowed as "legacy": we fail closed
102
+ // (throw) so the never-downgrade gate can never be silently bypassed (AGENTS.md: no silent failures).
103
+ const readInstalledVersion = async (target) => {
104
+ const skill = resolve(target, 'SKILL.md');
105
+ if (!existsSync(skill)) return null; // absent → new/legacy install, nothing to compare
106
+ const text = await readFile(skill, 'utf8').catch((err) => {
107
+ throw new Error(
108
+ `[agent-workflow-kit] cannot read the installed SKILL.md (${tildify(skill)}): ${err.message}. ` +
109
+ `Refusing to install over an unreadable kit — fix permissions/contents or remove it, then re-run.`,
110
+ );
111
+ });
112
+ const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
113
+ return fm ? metadataVersion(fm[1]) : null;
114
+ };
115
+
60
116
  // lstat without following symlinks; null when absent. existsSync FOLLOWS symlinks (so a
61
117
  // *dangling* symlink reads as absent) — lstat is what lets the guard catch a dangling dest symlink.
62
118
  const lstatNoFollow = (path) => {
@@ -80,6 +136,7 @@ const parseArgs = (argv) => {
80
136
  version: argv.includes('--version') || argv.includes('-v'),
81
137
  noLaunchers: argv.includes('--no-launchers'),
82
138
  force: argv.includes('--force'),
139
+ allowDowngrade: argv.includes('--allow-downgrade'),
83
140
  dir: dirFlag >= 0 ? argv[dirFlag + 1] : undefined,
84
141
  };
85
142
  };
@@ -94,15 +151,20 @@ const printHelp = (version) => {
94
151
  console.log(`agent-workflow-kit ${version}
95
152
 
96
153
  Usage:
97
- npx @sabaiway/agent-workflow-kit init [--dir <path>] [--no-launchers] [--force]
98
- npx @sabaiway/agent-workflow-kit --version
99
- npx @sabaiway/agent-workflow-kit --help
154
+ npx @sabaiway/agent-workflow-kit@latest init [--dir <path>] [--no-launchers] [--force] [--allow-downgrade]
155
+ npx @sabaiway/agent-workflow-kit@latest --version
156
+ npx @sabaiway/agent-workflow-kit@latest --help
157
+
158
+ Use the @latest form: a bare \`npx … init\` (no @latest) can reuse an OLDER cached
159
+ build of this installer, so a returning user must bypass the npx cache to upgrade.
100
160
 
101
161
  Installs/refreshes the kit at ~/.claude/skills/agent-workflow-kit
102
162
  (override with --dir <path> or AGENT_WORKFLOW_KIT_DIR), then wires any
103
163
  Codex / Devin Desktop you have. --no-launchers skips that wiring; --force replaces a
104
164
  pre-existing non-kit launcher file (backed up first). init is additive — it never
105
- deletes your settings.
165
+ deletes your settings. If the installed kit is newer than the version you ran, init
166
+ refuses (no network — it compares the version on disk) and points you at @latest;
167
+ --allow-downgrade overrides that refusal (distinct from --force, which is launcher-only).
106
168
 
107
169
  After install, invoke the skill in your agent, inside a project:
108
170
  first time in the project -> /agent-workflow-kit
@@ -143,12 +205,46 @@ const main = async () => {
143
205
  console.error(`[agent-workflow-kit] target dir is a symlink — refusing to write through it: ${tildify(target)}`);
144
206
  process.exit(1);
145
207
  }
208
+
209
+ // Stale-cache defenses (no network — version already on disk vs this runner). Read BEFORE any
210
+ // write so a refusal touches nothing. cmp is null on a legacy/unparseable install → no gate.
211
+ const installedVersion = wasPresent ? await readInstalledVersion(target) : null;
212
+ const cmp = installedVersion ? compareSemver(installedVersion, version) : null;
213
+
214
+ // Never-downgrade gate: a bare `npx … init` can run an OLDER cached build of this installer, which
215
+ // would overwrite a NEWER installed skill with old code. Refuse loudly (nonzero) unless the
216
+ // dedicated --allow-downgrade override is passed — surfacing the cache trap instead of silently
217
+ // regressing the install (AGENTS.md: no silent failures). The override is its OWN flag, NOT --force:
218
+ // --force means "replace a foreign launcher file" and is forwarded to the launcher; conflating them
219
+ // would let someone clearing the version gate also clobber launchers by accident.
220
+ if (cmp === 1 && !args.allowDowngrade) {
221
+ console.error(
222
+ `[agent-workflow-kit] refusing to downgrade: the installed kit is v${installedVersion}, but this ` +
223
+ `runner is the OLDER v${version}.\n` +
224
+ ` This is the classic npx cache serving a stale build. To get the newest kit, bypass the cache:\n` +
225
+ ` npx @sabaiway/agent-workflow-kit@latest init\n` +
226
+ ` (or pass --allow-downgrade to overwrite the newer install with v${version} anyway).`,
227
+ );
228
+ process.exit(1);
229
+ }
230
+
146
231
  await mkdir(target, { recursive: true });
147
232
  for (const entry of PAYLOAD.filter((e) => existsSync(resolve(PKG_ROOT, e)))) {
148
233
  copyTreeRefresh(resolve(PKG_ROOT, entry), resolve(target, entry), target);
149
234
  }
150
235
  console.log(`[agent-workflow-kit] ${wasPresent ? 'updated the kit to' : 'installed'} v${version} -> ${tildify(target)}`);
151
236
 
237
+ // No-op re-run: the install just refreshed the skill with the SAME version it already had. For a
238
+ // user who ran `init` expecting an upgrade, that almost always means npx reused a cached build —
239
+ // say so explicitly and point at @latest (the no-network signal that catches the reported scenario).
240
+ if (cmp === 0) {
241
+ console.log(
242
+ `[agent-workflow-kit] note: no version change — the kit was already v${version}. If you expected ` +
243
+ `an update, npx likely served a cached build; re-run bypassing the cache:\n` +
244
+ ` npx @sabaiway/agent-workflow-kit@latest init`,
245
+ );
246
+ }
247
+
152
248
  // Wire non-Claude agents — best-effort; the launcher only touches tools you have.
153
249
  const launcher = resolve(target, 'launchers/install-launchers.sh');
154
250
  if (args.noLaunchers) {
@@ -178,7 +274,22 @@ To update the kit later, re-run: npx @sabaiway/agent-workflow-kit@latest init`)
178
274
 
179
275
  // Run main() only when executed directly (npx / node bin/install.mjs), never on import — so tests
180
276
  // can import this module to assert it has no side effects. Same idiom as tools/detect-backends.mjs.
181
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
277
+ //
278
+ // Compare by REAL path, not by URL string: npx invokes the bin through a symlink in node_modules/.bin,
279
+ // so process.argv[1] is that symlink while import.meta.url is the resolved real file — a raw string
280
+ // compare reads them as different, main() never runs, and `npx … init` exits silently (the reported
281
+ // "nothing happens after install" bug). realpathSync collapses the symlink so both sides match; it also
282
+ // holds under --preserve-symlinks. A bare `node -e` (no argv[1]) and a missing file (realpath throws)
283
+ // both correctly fall through to false — importing the module still runs nothing.
284
+ const isDirectRun = (() => {
285
+ const invoked = process.argv[1];
286
+ if (!invoked) return false;
287
+ try {
288
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
289
+ } catch {
290
+ return false;
291
+ }
292
+ })();
182
293
  if (isDirectRun) {
183
294
  main().catch((err) => {
184
295
  console.error(err);
@@ -1,7 +1,7 @@
1
1
  import { describe, it, beforeEach, afterEach } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { mkdtemp, rm, mkdir, symlink, readdir } from 'node:fs/promises';
4
+ import { mkdtemp, rm, mkdir, symlink, readdir, readFile, writeFile } from 'node:fs/promises';
5
5
  import { existsSync } from 'node:fs';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { dirname, join } from 'node:path';
@@ -9,9 +9,21 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
9
9
 
10
10
  const INSTALLER = join(dirname(fileURLToPath(import.meta.url)), 'install.mjs');
11
11
  const KIT_ROOT = dirname(dirname(INSTALLER));
12
- // --no-launchers so the test never wires Codex/Devin on the host.
13
- const runInstaller = (target) =>
14
- spawnSync(process.execPath, [INSTALLER, '--dir', target, '--no-launchers'], { encoding: 'utf8' });
12
+ // --no-launchers so the test never wires Codex/Devin on the host. `extra` appends flags (e.g. --force).
13
+ const runInstaller = (target, extra = []) =>
14
+ spawnSync(process.execPath, [INSTALLER, '--dir', target, '--no-launchers', ...extra], { encoding: 'utf8' });
15
+
16
+ // Rewrite / read the installed skill's frontmatter version — used to simulate "a newer kit is already
17
+ // installed" (the stale-cache downgrade scenario). The installer reads exactly this field.
18
+ const setInstalledVersion = async (target, v) => {
19
+ const p = join(target, 'SKILL.md');
20
+ const text = await readFile(p, 'utf8');
21
+ await writeFile(p, text.replace(/version:\s*['"]?\d+\.\d+\.\d+['"]?/, `version: '${v}'`));
22
+ };
23
+ const getInstalledVersion = async (target) =>
24
+ (await readFile(join(target, 'SKILL.md'), 'utf8')).match(/version:\s*['"]?(\d+\.\d+\.\d+)['"]?/)?.[1] ?? null;
25
+ const pkgVersion = async () =>
26
+ JSON.parse(await readFile(join(KIT_ROOT, 'package.json'), 'utf8')).version;
15
27
 
16
28
  describe('kit installer — payload + symlink-traversal hardening', () => {
17
29
  let dir;
@@ -66,6 +78,31 @@ describe('kit installer — payload + symlink-traversal hardening', () => {
66
78
  });
67
79
  });
68
80
 
81
+ describe('kit installer — runs through the npx bin symlink', () => {
82
+ let dir;
83
+ beforeEach(async () => {
84
+ dir = await mkdtemp(join(tmpdir(), 'aw-kit-symlink-'));
85
+ });
86
+ afterEach(async () => {
87
+ await rm(dir, { recursive: true, force: true });
88
+ });
89
+
90
+ it('main() runs when invoked through a symlink (the node_modules/.bin shim npx uses)', async () => {
91
+ // npx never runs bin/install.mjs by its real path — it runs node_modules/.bin/agent-workflow-kit,
92
+ // a symlink to it. Node resolves import.meta.url to the REAL file but leaves argv[1] as the symlink,
93
+ // so a string compare of the two makes isDirectRun false and main() silently no-ops. Reproduce that
94
+ // exact invocation: run the installer via a symlink and assert it actually installed (visible output
95
+ // + payload on disk). A regression here means `npx … init` goes quiet again.
96
+ const shim = join(dir, 'agent-workflow-kit'); // stands in for node_modules/.bin/<name>
97
+ await symlink(INSTALLER, shim);
98
+ const target = join(dir, 'home', 'agent-workflow-kit');
99
+ const res = spawnSync(process.execPath, [shim, '--dir', target, '--no-launchers'], { encoding: 'utf8' });
100
+ assert.equal(res.status, 0, res.stderr);
101
+ assert.match(res.stdout, /installed v|updated the kit/);
102
+ assert.ok(existsSync(join(target, 'SKILL.md')), 'install through the symlink must write the payload');
103
+ });
104
+ });
105
+
69
106
  describe('kit installer — module hygiene', () => {
70
107
  it('importing install.mjs runs nothing (main() is guarded by isDirectRun)', () => {
71
108
  // `node -e` has no argv[1], so isDirectRun is false → importing must not run main()
@@ -82,6 +119,98 @@ describe('kit installer — module hygiene', () => {
82
119
  });
83
120
  });
84
121
 
122
+ describe('kit installer — stale-cache defenses (no network)', () => {
123
+ let dir;
124
+ beforeEach(async () => {
125
+ dir = await mkdtemp(join(tmpdir(), 'aw-kit-stale-'));
126
+ });
127
+ afterEach(async () => {
128
+ await rm(dir, { recursive: true, force: true });
129
+ });
130
+
131
+ it('a no-op re-run (same version) flags the likely npx cache and points at @latest', async () => {
132
+ const target = join(dir, 'agent-workflow-kit');
133
+ assert.equal(runInstaller(target).status, 0); // first install
134
+ const again = runInstaller(target); // second run: installed == running
135
+ assert.equal(again.status, 0, again.stderr);
136
+ assert.match(again.stdout, /no version change/i);
137
+ assert.match(again.stdout, /cache/i);
138
+ assert.match(again.stdout, /@latest/);
139
+ });
140
+
141
+ it('refuses to downgrade when the installed kit is NEWER, and writes nothing', async () => {
142
+ const target = join(dir, 'agent-workflow-kit');
143
+ assert.equal(runInstaller(target).status, 0);
144
+ await setInstalledVersion(target, '99.0.0'); // pretend a newer kit is already installed
145
+ const res = runInstaller(target); // running version < installed → downgrade
146
+ assert.notEqual(res.status, 0);
147
+ assert.match(res.stderr, /downgrade/i);
148
+ assert.match(res.stderr, /@latest/);
149
+ assert.equal(await getInstalledVersion(target), '99.0.0', 'newer install must be left untouched');
150
+ });
151
+
152
+ it('--allow-downgrade overrides the refusal and overwrites with the runner version', async () => {
153
+ const target = join(dir, 'agent-workflow-kit');
154
+ assert.equal(runInstaller(target).status, 0);
155
+ await setInstalledVersion(target, '99.0.0');
156
+ const res = runInstaller(target, ['--allow-downgrade']);
157
+ assert.equal(res.status, 0, res.stderr);
158
+ assert.equal(await getInstalledVersion(target), await pkgVersion());
159
+ });
160
+
161
+ it('--force alone does NOT override the downgrade gate (the override is its own flag)', async () => {
162
+ const target = join(dir, 'agent-workflow-kit');
163
+ assert.equal(runInstaller(target).status, 0);
164
+ await setInstalledVersion(target, '99.0.0');
165
+ const res = runInstaller(target, ['--force']); // launcher-clobber flag, not the gate override
166
+ assert.notEqual(res.status, 0, 'launcher --force must not silently clear the version gate');
167
+ assert.equal(await getInstalledVersion(target), '99.0.0', 'newer install must be left untouched');
168
+ });
169
+
170
+ it('reads the version under `metadata:`, never a top-level / nested decoy `version:`', async () => {
171
+ // A crafted SKILL.md whose REAL metadata.version (0.0.1) is OLDER than the runner, but with decoy
172
+ // `version:` lines that are NEWER. A naive "first version: in frontmatter" read would see 99.0.0
173
+ // and wrongly refuse; the correct read sees 0.0.1 → a normal upgrade (exit 0).
174
+ const target = join(dir, 'agent-workflow-kit');
175
+ await mkdir(target, { recursive: true });
176
+ const decoy = [
177
+ '---',
178
+ "version: '99.0.0'", // top-level decoy (column 0)
179
+ 'name: agent-workflow-kit',
180
+ 'metadata:',
181
+ ' nested:',
182
+ " version: '98.0.0'", // deeper-nested decoy
183
+ " version: '0.0.1'", // the authoritative direct child
184
+ '---',
185
+ '# decoy',
186
+ '',
187
+ ].join('\n');
188
+ await writeFile(join(target, 'SKILL.md'), decoy);
189
+ const res = runInstaller(target);
190
+ assert.equal(res.status, 0, `expected a normal upgrade (read 0.0.1), got: ${res.stderr}`);
191
+ assert.match(res.stdout, /updated the kit to/);
192
+ });
193
+
194
+ it('fails closed (does not silently treat as legacy) when an existing SKILL.md cannot be read', async () => {
195
+ // SKILL.md present but unreadable (a directory → EISDIR on read). The gate must NOT be bypassed:
196
+ // we refuse rather than overwrite a kit whose version we could not determine (no silent failure).
197
+ const target = join(dir, 'agent-workflow-kit');
198
+ await mkdir(join(target, 'SKILL.md'), { recursive: true });
199
+ const res = runInstaller(target);
200
+ assert.notEqual(res.status, 0);
201
+ assert.match(res.stderr, /cannot read the installed SKILL\.md/i);
202
+ });
203
+
204
+ it('a legacy install with no version stamp still upgrades (no false downgrade, no crash)', async () => {
205
+ const target = join(dir, 'agent-workflow-kit');
206
+ await mkdir(target, { recursive: true });
207
+ await writeFile(join(target, 'SKILL.md'), '---\nname: agent-workflow-kit\n---\n# legacy stub\n');
208
+ const res = runInstaller(target);
209
+ assert.equal(res.status, 0, res.stderr);
210
+ assert.match(res.stdout, /updated the kit to/);
211
+ });
212
+ });
213
+
85
214
  describe('kit installer — published tarball bundles the bridges', () => {
86
215
  it('npm pack ships bridges/<name>/ (the execution-backend skill mirrors)', () => {
87
216
  // The real `files` whitelist decides what publishes — assert against `npm pack`, not the source
@@ -150,9 +150,9 @@ checklist, prompt templates, output handling). Essentials:
150
150
  - **`agent-workflow-memory`** (family **context provider**) — if the current project has **no**
151
151
  `AGENTS.md` + `docs/ai/`, `agy` has no per-workspace context to read. The memory substrate creates
152
152
  it. Soft-recommend it (only when the user wants the memory workflow):
153
- `npx @sabaiway/agent-workflow-memory init`, then `/agent-workflow-memory` in the project — or
153
+ `npx @sabaiway/agent-workflow-memory@latest init`, then `/agent-workflow-memory` in the project — or
154
154
  bootstrap the whole family via the **`agent-workflow-kit`** orchestrator
155
- (`npx @sabaiway/agent-workflow-kit init`), which delegates substrate deployment to memory. Never a
155
+ (`npx @sabaiway/agent-workflow-kit@latest init`), which delegates substrate deployment to memory. Never a
156
156
  prerequisite for using `agy`.
157
157
  - **`codex-cli-bridge`** (sibling backend, OpenAI Codex) — recommend **by actual presence**: if
158
158
  `~/.claude/skills/codex-cli-bridge/` exists you have a **second delegated engine** (Codex for
@@ -129,8 +129,8 @@ The wrappers work in any git repo where `codex` is installed and authenticated.
129
129
  - **`agent-workflow-memory`** (family **context provider**) — if the target project has **no**
130
130
  `AGENTS.md` + `docs/ai/`, codex has no root context to read (and the wrappers' preflight will
131
131
  STOP). The memory substrate is what creates that context. Soft-recommend it (only when the user
132
- wants the memory workflow): `npx @sabaiway/agent-workflow-memory init`, or bootstrap the whole
133
- family via the **`agent-workflow-kit`** orchestrator (`npx @sabaiway/agent-workflow-kit init`),
132
+ wants the memory workflow): `npx @sabaiway/agent-workflow-memory@latest init`, or bootstrap the whole
133
+ family via the **`agent-workflow-kit`** orchestrator (`npx @sabaiway/agent-workflow-kit@latest init`),
134
134
  which delegates substrate deployment to memory and injects the workflow methodology. Never a
135
135
  prerequisite.
136
136
 
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.7.0",
6
+ "version": "1.8.1",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
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",