@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 +61 -0
- package/README.md +2 -2
- package/SKILL.md +17 -1
- package/bin/install.mjs +120 -9
- package/bin/install.test.mjs +133 -4
- package/bridges/antigravity-cli-bridge/SKILL.md +2 -2
- package/bridges/codex-cli-bridge/SKILL.md +2 -2
- package/capability.json +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
-
|
|
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);
|
package/bin/install.test.mjs
CHANGED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "1.
|
|
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",
|