@sabaiway/agent-workflow-kit 1.6.0 → 1.8.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 +61 -0
- package/README.md +14 -7
- package/SKILL.md +39 -2
- package/bin/install.mjs +135 -56
- package/bin/install.test.mjs +138 -5
- package/bridges/antigravity-cli-bridge/SKILL.md +178 -0
- package/bridges/antigravity-cli-bridge/bin/agy.sh +133 -0
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -0
- package/bridges/antigravity-cli-bridge/capability.json +22 -0
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +108 -0
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +93 -0
- package/bridges/antigravity-cli-bridge/references/review-prompt.md +51 -0
- package/bridges/antigravity-cli-bridge/setup/README.md +65 -0
- package/bridges/codex-cli-bridge/SKILL.md +148 -0
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +143 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +84 -0
- package/bridges/codex-cli-bridge/capability.json +22 -0
- package/bridges/codex-cli-bridge/references/driving-codex.md +97 -0
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +105 -0
- package/bridges/codex-cli-bridge/setup/README.md +78 -0
- package/capability.json +1 -1
- package/package.json +3 -2
- package/tools/detect-backends.mjs +36 -0
- package/tools/detect-backends.test.mjs +102 -0
- package/tools/fs-safe.mjs +129 -0
- package/tools/fs-safe.test.mjs +200 -0
- package/tools/setup-backends.mjs +468 -0
- package/tools/setup-backends.test.mjs +500 -0
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.0 — Stale-version DX: `@latest` everywhere + a no-network never-downgrade gate
|
|
8
|
+
|
|
9
|
+
A returning user ran the headline `npx @sabaiway/agent-workflow-kit init` and it quietly did nothing:
|
|
10
|
+
a bare `npx <pkg> init` (no `@latest`) reuses the npx cache and re-runs an **older cached build** of
|
|
11
|
+
the installer, which exits 0 and reports it "updated" — to the same stale version. This release makes
|
|
12
|
+
that mistake hard to miss while **the installer itself stays 100% network-free** — the only thing
|
|
13
|
+
that ever contacts npm is npx resolving `@latest`, exactly as it already does (the no-phone-home
|
|
14
|
+
principle is preserved; see AD-012):
|
|
15
|
+
|
|
16
|
+
- **`@latest` is the documented default everywhere.** Every prescribing surface (both READMEs, the
|
|
17
|
+
bridge `SKILL.md` files + their bundled mirrors, the installer `--help` / header) now shows
|
|
18
|
+
`npx @sabaiway/agent-workflow-kit@latest init`. A new drift guard
|
|
19
|
+
(`test/init-command-uses-latest.test.mjs`) fails the build if a bare form sneaks back in (historical
|
|
20
|
+
contexts — CHANGELOG / `releases/` / `migrations/` — are exempt).
|
|
21
|
+
- **Never-downgrade gate (no network).** `init` reads the installed skill's version from
|
|
22
|
+
`SKILL.md` **before** writing; if the installed kit is **newer** than the version you ran (the exact
|
|
23
|
+
stale-cache signature), it **refuses** (nonzero) and points at `@latest`, rather than silently
|
|
24
|
+
overwriting a newer install with old code. `--force` overrides. A legacy install with no version
|
|
25
|
+
stamp still upgrades cleanly.
|
|
26
|
+
- **No-op re-run hint.** When `init` refreshes the skill with the *same* version it already had, it
|
|
27
|
+
says so and points at `@latest` — the no-network signal that catches the reported scenario.
|
|
28
|
+
- **In-agent skill** (`SKILL.md`): surfaces a one-line version status (project `docs/ai/.workflow-version`
|
|
29
|
+
vs the lineage head) + routes (bootstrap / upgrade / current), spells out the **two independent
|
|
30
|
+
version axes** (project deployment vs kit freshness — the latter is the npx installer's job, never
|
|
31
|
+
this skill's), and tells you to **restart the session** after refreshing the kit so the new skill
|
|
32
|
+
files load.
|
|
33
|
+
|
|
34
|
+
New users are unaffected (an empty npx cache already fetches `latest`); this targets the returning-user
|
|
35
|
+
trap. No `docs/ai` structural change → the deployment-lineage head stays **`1.3.0`**; no migration.
|
|
36
|
+
|
|
37
|
+
## 1.7.0 — Link-only backend auto-setup; bridges bundled in the tarball
|
|
38
|
+
|
|
39
|
+
The optional execution-backend bridges (`codex-cli-bridge` → `codex`, `antigravity-cli-bridge` →
|
|
40
|
+
`agy`) can now be set up from the kit itself, via a new **opt-in, in-agent** mode —
|
|
41
|
+
**`/agent-workflow-kit setup [backend]`** (`tools/setup-backends.mjs`). It owns only the two
|
|
42
|
+
**deterministic, secret-free** steps and **guides** the rest (AD-011):
|
|
43
|
+
|
|
44
|
+
- **Bridges are bundled in the kit's npm tarball** under `bridges/<name>/` — a **byte-identical
|
|
45
|
+
mirror** of the repo-root bridges, pinned by `test/bridges-mirror.test.mjs` (the same drift-guard
|
|
46
|
+
pattern as the methodology mirror). So `setup` places a bridge from local files, with **no network
|
|
47
|
+
fetch**. `init` (npx) bundles them but still **does not place** them — that stays the opt-in
|
|
48
|
+
`setup` job (preserving the honest `init` ≠ deploy claim).
|
|
49
|
+
- **`setup` places/refreshes the bundled bridge skill**, but only into a dir that is **absent /
|
|
50
|
+
empty / proven-managed** (valid manifest, matching `name`+`kind`); a stub/foreign/invalid/
|
|
51
|
+
unsupported manifest, a marker fs-error, a non-empty unknown dir, or a symlinked dir → **STOP**,
|
|
52
|
+
never overwritten. Refresh re-runs on a managed dir so re-running `setup` delivers bundled fixes.
|
|
53
|
+
- **It links the wrappers** (`codex-exec` / `codex-review`; `agy-run`) onto `PATH` (`~/.local/bin`,
|
|
54
|
+
override with `--bindir`) via **managed symlinks** — replacing only a symlink already pointing at
|
|
55
|
+
our source. It **preflights every target first**, so a conflict on one wrapper makes **zero**
|
|
56
|
+
changes; a non-symlink or a foreign symlink → STOP. Wrapper presence is judged **per-bindir**, not
|
|
57
|
+
PATH-wide. `--dry-run` prints the plan and changes nothing.
|
|
58
|
+
- **The binary install + the interactive subscription login stay manual** — `setup` prints the exact
|
|
59
|
+
commands (the detector's axis-aware `guideFor`), never runs a subscription CLI, never commits. On
|
|
60
|
+
**Windows** it reports *unsupported — use WSL* and mutates nothing (the wrappers are POSIX `.sh`).
|
|
61
|
+
- Internal: the symlink-traversal-safe copy/link primitives are now shared in `tools/fs-safe.mjs`
|
|
62
|
+
(the npx installer consumes them and gained an `isDirectRun` guard so importing it runs nothing).
|
|
63
|
+
The per-package publish workflow now gates the kit on its **whole** test suite, not just the
|
|
64
|
+
shipped enforcement scripts.
|
|
65
|
+
|
|
66
|
+
No `docs/ai` structural change → the deployment-lineage head stays **`1.3.0`**; no migration.
|
|
67
|
+
|
|
7
68
|
## 1.6.0 — Methodology slot reconciliation; engine becomes the canonical methodology home
|
|
8
69
|
|
|
9
70
|
The workflow methodology now has a **single canonical home** in `agent-workflow-engine`
|
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
|
|
@@ -216,6 +216,7 @@ command is printed).
|
|
|
216
216
|
| `/agent-workflow-kit` | new / empty project | recon → **asks visible-or-hidden** + **conversational language** + **agent attribution** (default off) → deploys `AGENTS.md` + `docs/ai/` filled with real recon data → installs enforcement → **asks before committing** |
|
|
217
217
|
| `/agent-workflow-kit upgrade` | existing deployment | reads `docs/ai/.workflow-version`, shows the changelog diff, preserves your authored memory, applies migrations, re-stamps |
|
|
218
218
|
| `/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). |
|
|
219
|
+
| `/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. |
|
|
219
220
|
|
|
220
221
|
It **never auto-commits** and **never overwrites** an existing `AGENTS.md` without asking.
|
|
221
222
|
|
|
@@ -248,17 +249,20 @@ agent-workflow-kit — the composition root (installed via npx … init)
|
|
|
248
249
|
├─ delegates ─▶ memory substrate (healthy copy, else bundled fallback)
|
|
249
250
|
├─ injects ─▶ workflow methodology (engine = future supplier; stub)
|
|
250
251
|
├─ deploys ─▶ AGENTS.md + docs/ai/ + Node scripts + pre-commit hook
|
|
251
|
-
|
|
252
|
+
├─ detects ─▶ optional backends (codex / agy, read-only)
|
|
253
|
+
└─ sets up ─▶ a bridge (opt-in) (place skill + link wrappers)
|
|
252
254
|
```
|
|
253
255
|
|
|
254
256
|
- **Delegates** substrate deployment to **`@sabaiway/agent-workflow-memory`** when a healthy
|
|
255
257
|
standalone copy is present, else uses its **bundled fallback** — same `docs/ai/` either way.
|
|
256
258
|
- **Injects** the bounded workflow methodology into the deployed `AGENTS.md`. Its *future* home is
|
|
257
259
|
**`agent-workflow-engine`** — today an `available: false` stub, never one of the shipped backends.
|
|
258
|
-
- **Detects** the optional `codex` / `agy` **bridges** — agent skills
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
- **Detects & (opt-in) sets up** the optional `codex` / `agy` **bridges** — agent skills (not npm, not
|
|
261
|
+
installed by `init`). `/agent-workflow-kit backends` reports readiness **read-only**;
|
|
262
|
+
`/agent-workflow-kit setup` does the **link-only** part (place the bundled bridge skill + link its
|
|
263
|
+
wrappers), while the binary install + the subscription login stay manual. A bridge reads the deployed
|
|
264
|
+
memory only if it wins that tool's context-file priority, and the bridges call third-party services
|
|
265
|
+
(so "no telemetry" covers family code, not those).
|
|
262
266
|
|
|
263
267
|
> Full member-by-member map + the whole-family story: the
|
|
264
268
|
> **[family front door](https://github.com/sabaiway/agent-workflow#readme)** — this page stays the
|
|
@@ -295,7 +299,10 @@ agent-workflow-kit/
|
|
|
295
299
|
│ ├── delegation.mjs ← detect substrate · delegate-or-fall-back
|
|
296
300
|
│ ├── inject-methodology.mjs ← write the methodology slot
|
|
297
301
|
│ ├── detect-backends.mjs ← read-only backend detector
|
|
302
|
+
│ ├── setup-backends.mjs ← link-only backend setup
|
|
303
|
+
│ ├── fs-safe.mjs ← symlink-safe copy/link
|
|
298
304
|
│ └── release-scan.mjs ← attribution / release gate
|
|
305
|
+
├── bridges/ ← bundled bridge skill mirrors (codex / antigravity)
|
|
299
306
|
├── launchers/ ← Codex / Devin Desktop / Cursor entries
|
|
300
307
|
└── migrations/ ← per-version upgrade steps
|
|
301
308
|
```
|
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.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
|
@@ -83,6 +83,23 @@ Pick the mode from the user's invocation. Auto-detect an existing `docs/ai/` to
|
|
|
83
83
|
- **`/agent-workflow-kit`** (default) — bootstrap a new or empty project. If `docs/ai/` already exists, stop and ask whether they meant `upgrade`.
|
|
84
84
|
- **`/agent-workflow-kit upgrade`** — upgrade an existing deployment to the skill's current `version`.
|
|
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
|
+
- **`/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
|
+
|
|
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`).
|
|
86
103
|
|
|
87
104
|
### Mode: bootstrap
|
|
88
105
|
|
|
@@ -145,6 +162,26 @@ Read-only. Answers *"which optional execution-backends are set up vs missing, an
|
|
|
145
162
|
- **"credentials present"** means the credential-marker **file** exists — it is **not** a live login check. The detector never runs `codex login status` / `agy` (that would spawn a paid, slow, networked subscription CLI).
|
|
146
163
|
- The bridges' wrappers are **POSIX `.sh`** scripts. On Windows the detector still works, but the bridges themselves are **not promised to run** — say so rather than implying they will.
|
|
147
164
|
|
|
165
|
+
### Mode: setup
|
|
166
|
+
|
|
167
|
+
The **only writer** among the backend modes, and **opt-in / in-agent only** — it is **never** part of `init`. The npx installer deploys the *kit* and bundles the bridge skills in its tarball, but **does not place** them (that honesty claim is load-bearing — see `decisions.md` AD-009 / AD-011). `setup` owns exactly the two deterministic, secret-free steps and **guides** the rest. It **never commits and never runs a subscription CLI**.
|
|
168
|
+
|
|
169
|
+
Run `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs [<backend>] [--bindir <path>] [--dry-run]`:
|
|
170
|
+
|
|
171
|
+
- `<backend>` — `codex` | `agy` | `antigravity` | `codex-cli-bridge` | `antigravity-cli-bridge`; omit for **all**.
|
|
172
|
+
- `--bindir <path>` — where to link the wrappers (default `~/.local/bin`).
|
|
173
|
+
- `--dry-run` — print the per-backend plan and change **nothing** (run this first).
|
|
174
|
+
- `--help`, `-h` — usage.
|
|
175
|
+
|
|
176
|
+
For each backend it:
|
|
177
|
+
1. **Places / refreshes the bundled bridge skill** (from the kit's `bridges/<name>/` mirror) into its canonical dir — but only when that dir is **absent / empty / proven-managed** (valid manifest, matching `name`+`kind`). A `stub` / `foreign` / `invalid` / `unsupported` dir, a marker fs-error, or a symlinked dir → **STOP**, never overwritten. Refresh re-runs on a proven-managed dir so re-running `setup` delivers bundled fixes.
|
|
178
|
+
2. **Links its wrappers** (`codex-exec` / `codex-review`; `agy-run`) onto `--bindir` via **managed symlinks** — replacing only a symlink that already points at our source. A non-symlink or a foreign symlink → **STOP**; it **preflights every target first**, so a conflict on one wrapper makes **zero** changes. If `--bindir` is not on `PATH`, it prints the one-line `export PATH=…` to add — it never edits a shell rc.
|
|
179
|
+
3. **Guides the manual, secret-bearing steps it will NOT automate** — the binary install (each bridge's `setup/README.md` §1) and the one-time interactive subscription login (`codex login` / `agy`) — printing the exact command for whichever axis is still missing (axis-aware: it can ask for both the CLI and the login at once).
|
|
180
|
+
|
|
181
|
+
**Windows:** the wrappers are POSIX `.sh`; on `win32` it reports *unsupported — use WSL* and mutates nothing.
|
|
182
|
+
|
|
183
|
+
**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).
|
|
184
|
+
|
|
148
185
|
---
|
|
149
186
|
|
|
150
187
|
## Gotchas
|
|
@@ -214,6 +251,6 @@ Deploy these into `AGENTS.md`; remove rows that don't apply to the stack.
|
|
|
214
251
|
- [`references/scripts/`](references/scripts/) — the Node enforcement scripts (caps + staleness + index-freshness gate, 3-tier archive, hook installer) and their unit tests.
|
|
215
252
|
- [`migrations/`](migrations/) — per-version upgrade steps; see `migrations/README.md`.
|
|
216
253
|
- [`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`.
|
|
217
|
-
- [`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` + `methodology-slot.md` (the bounded slot reconciliation — ensure-slot / inject-if-empty / cap; the fragment is a byte-identical mirror of the `agent-workflow-engine` canon, pinned by `methodology-mirror.test.mjs`), `detect-backends.mjs` (the read-only **backend detector** behind `/agent-workflow-kit backends`), and `release-scan.mjs` (the attribution-off release gate). See [`tools/manifest/schema.md`](tools/manifest/schema.md).
|
|
254
|
+
- [`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` + `methodology-slot.md` (the bounded slot reconciliation — ensure-slot / inject-if-empty / cap; the fragment is a byte-identical mirror of the `agent-workflow-engine` canon, pinned by `methodology-mirror.test.mjs`), `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), 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).
|
|
218
255
|
- [`capability.json`](capability.json) — the kit's own `agent-workflow` family manifest (`kind: composition-root`).
|
|
219
256
|
- [`CHANGELOG.md`](CHANGELOG.md) — version history of this kernel.
|
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,23 +14,30 @@
|
|
|
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
|
-
import { readFile, mkdir
|
|
25
|
+
import { readFile, mkdir } from 'node:fs/promises';
|
|
22
26
|
import { existsSync, lstatSync } from 'node:fs';
|
|
23
|
-
import { dirname,
|
|
24
|
-
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { dirname, resolve } from 'node:path';
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
29
|
import { homedir } from 'node:os';
|
|
26
30
|
import { spawnSync } from 'node:child_process';
|
|
31
|
+
import { copyTreeRefresh } from '../tools/fs-safe.mjs';
|
|
27
32
|
|
|
28
33
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
34
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
30
35
|
|
|
31
36
|
// The deployable skill = everything except the npm wrapper (package.json, bin/).
|
|
32
37
|
// capability.json (the family manifest) + tools/ (the family schema + validator the kit runs
|
|
33
|
-
// as the memory detector) must land in the installed skill dir too.
|
|
38
|
+
// as the memory detector) must land in the installed skill dir too. bridges/ carries the
|
|
39
|
+
// byte-identical execution-backend skill mirrors (codex/antigravity) so `setup` can place a bridge
|
|
40
|
+
// from the installed kit, with no network fetch (Plan B / AD-011).
|
|
34
41
|
const PAYLOAD = [
|
|
35
42
|
'SKILL.md',
|
|
36
43
|
'README.md',
|
|
@@ -40,6 +47,7 @@ const PAYLOAD = [
|
|
|
40
47
|
'launchers',
|
|
41
48
|
'migrations',
|
|
42
49
|
'tools',
|
|
50
|
+
'bridges',
|
|
43
51
|
];
|
|
44
52
|
|
|
45
53
|
const tildify = (path) => path.replace(homedir(), '~');
|
|
@@ -53,6 +61,58 @@ const readVersion = async () => {
|
|
|
53
61
|
}
|
|
54
62
|
};
|
|
55
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
|
+
|
|
56
116
|
// lstat without following symlinks; null when absent. existsSync FOLLOWS symlinks (so a
|
|
57
117
|
// *dangling* symlink reads as absent) — lstat is what lets the guard catch a dangling dest symlink.
|
|
58
118
|
const lstatNoFollow = (path) => {
|
|
@@ -64,42 +124,10 @@ const lstatNoFollow = (path) => {
|
|
|
64
124
|
}
|
|
65
125
|
};
|
|
66
126
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
72
|
-
throw new Error(`[agent-workflow-kit] refusing to write outside the target dir: ${dest}`);
|
|
73
|
-
}
|
|
74
|
-
if (lstatNoFollow(root)?.isSymbolicLink()) {
|
|
75
|
-
throw new Error(`[agent-workflow-kit] refusing to install into a symlinked target dir: ${root}`);
|
|
76
|
-
}
|
|
77
|
-
const walk = (acc, part) => {
|
|
78
|
-
const cur = join(acc, part);
|
|
79
|
-
if (lstatNoFollow(cur)?.isSymbolicLink()) {
|
|
80
|
-
throw new Error(`[agent-workflow-kit] refusing to write through a symlink at ${cur} (would escape ${root}).`);
|
|
81
|
-
}
|
|
82
|
-
return cur;
|
|
83
|
-
};
|
|
84
|
-
rel.split(sep).filter(Boolean).reduce(walk, root);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const copyRecursive = async (src, dest, root) => {
|
|
88
|
-
assertContainedRealPath(root, dest); // never write through a dest symlink (root/intermediate/leaf)
|
|
89
|
-
const stat = await lstat(src);
|
|
90
|
-
if (stat.isSymbolicLink()) {
|
|
91
|
-
if (existsSync(dest)) return; // additive: never delete/replace an existing entry
|
|
92
|
-
const linkTarget = await readlink(src);
|
|
93
|
-
await symlink(linkTarget, dest);
|
|
94
|
-
} else if (stat.isDirectory()) {
|
|
95
|
-
await mkdir(dest, { recursive: true });
|
|
96
|
-
const entries = await readdir(src);
|
|
97
|
-
await Promise.all(entries.map((entry) => copyRecursive(join(src, entry), join(dest, entry), root)));
|
|
98
|
-
} else {
|
|
99
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
100
|
-
await copyFile(src, dest);
|
|
101
|
-
}
|
|
102
|
-
};
|
|
127
|
+
// The symlink-traversal guard + the recursive refresh copy now live in tools/fs-safe.mjs (shared,
|
|
128
|
+
// dependency-injectable, unit-tested in isolation). install.mjs consumes copyTreeRefresh, which guards
|
|
129
|
+
// every dest via assertContainedRealPath internally. The local lstatNoFollow above stays for the
|
|
130
|
+
// pre-flight check on the target root itself (a nicer error than letting the copy throw).
|
|
103
131
|
|
|
104
132
|
const parseArgs = (argv) => {
|
|
105
133
|
const dirFlag = argv.indexOf('--dir');
|
|
@@ -108,6 +136,7 @@ const parseArgs = (argv) => {
|
|
|
108
136
|
version: argv.includes('--version') || argv.includes('-v'),
|
|
109
137
|
noLaunchers: argv.includes('--no-launchers'),
|
|
110
138
|
force: argv.includes('--force'),
|
|
139
|
+
allowDowngrade: argv.includes('--allow-downgrade'),
|
|
111
140
|
dir: dirFlag >= 0 ? argv[dirFlag + 1] : undefined,
|
|
112
141
|
};
|
|
113
142
|
};
|
|
@@ -122,15 +151,20 @@ const printHelp = (version) => {
|
|
|
122
151
|
console.log(`agent-workflow-kit ${version}
|
|
123
152
|
|
|
124
153
|
Usage:
|
|
125
|
-
npx @sabaiway/agent-workflow-kit init [--dir <path>] [--no-launchers] [--force]
|
|
126
|
-
npx @sabaiway/agent-workflow-kit --version
|
|
127
|
-
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.
|
|
128
160
|
|
|
129
161
|
Installs/refreshes the kit at ~/.claude/skills/agent-workflow-kit
|
|
130
162
|
(override with --dir <path> or AGENT_WORKFLOW_KIT_DIR), then wires any
|
|
131
163
|
Codex / Devin Desktop you have. --no-launchers skips that wiring; --force replaces a
|
|
132
164
|
pre-existing non-kit launcher file (backed up first). init is additive — it never
|
|
133
|
-
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).
|
|
134
168
|
|
|
135
169
|
After install, invoke the skill in your agent, inside a project:
|
|
136
170
|
first time in the project -> /agent-workflow-kit
|
|
@@ -150,7 +184,15 @@ const main = async () => {
|
|
|
150
184
|
|
|
151
185
|
// Critical payload must be present, or the install would silently ship a kit that can't run
|
|
152
186
|
// its own detector (tools/) or family contract (capability.json). Fail loudly, don't filter away.
|
|
153
|
-
const REQUIRED = [
|
|
187
|
+
const REQUIRED = [
|
|
188
|
+
'SKILL.md',
|
|
189
|
+
'capability.json',
|
|
190
|
+
'references',
|
|
191
|
+
'tools',
|
|
192
|
+
'migrations',
|
|
193
|
+
'bridges/codex-cli-bridge',
|
|
194
|
+
'bridges/antigravity-cli-bridge',
|
|
195
|
+
];
|
|
154
196
|
const missing = REQUIRED.filter((entry) => !existsSync(resolve(PKG_ROOT, entry)));
|
|
155
197
|
if (missing.length > 0) {
|
|
156
198
|
console.error(`[agent-workflow-kit] package payload incomplete — missing: ${missing.join(', ')} (corrupt install?)`);
|
|
@@ -163,14 +205,46 @@ const main = async () => {
|
|
|
163
205
|
console.error(`[agent-workflow-kit] target dir is a symlink — refusing to write through it: ${tildify(target)}`);
|
|
164
206
|
process.exit(1);
|
|
165
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
|
+
|
|
166
231
|
await mkdir(target, { recursive: true });
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
),
|
|
171
|
-
);
|
|
232
|
+
for (const entry of PAYLOAD.filter((e) => existsSync(resolve(PKG_ROOT, e)))) {
|
|
233
|
+
copyTreeRefresh(resolve(PKG_ROOT, entry), resolve(target, entry), target);
|
|
234
|
+
}
|
|
172
235
|
console.log(`[agent-workflow-kit] ${wasPresent ? 'updated the kit to' : 'installed'} v${version} -> ${tildify(target)}`);
|
|
173
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
|
+
|
|
174
248
|
// Wire non-Claude agents — best-effort; the launcher only touches tools you have.
|
|
175
249
|
const launcher = resolve(target, 'launchers/install-launchers.sh');
|
|
176
250
|
if (args.noLaunchers) {
|
|
@@ -198,7 +272,12 @@ This command only installs/updates the kit itself (in ${tildify(target)}).
|
|
|
198
272
|
To update the kit later, re-run: npx @sabaiway/agent-workflow-kit@latest init`);
|
|
199
273
|
};
|
|
200
274
|
|
|
201
|
-
main().
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
275
|
+
// Run main() only when executed directly (npx / node bin/install.mjs), never on import — so tests
|
|
276
|
+
// can import this module to assert it has no side effects. Same idiom as tools/detect-backends.mjs.
|
|
277
|
+
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
278
|
+
if (isDirectRun) {
|
|
279
|
+
main().catch((err) => {
|
|
280
|
+
console.error(err);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
});
|
|
283
|
+
}
|
package/bin/install.test.mjs
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
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';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
9
|
|
|
10
10
|
const INSTALLER = join(dirname(fileURLToPath(import.meta.url)), 'install.mjs');
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const KIT_ROOT = dirname(dirname(INSTALLER));
|
|
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;
|
|
14
27
|
|
|
15
28
|
describe('kit installer — payload + symlink-traversal hardening', () => {
|
|
16
29
|
let dir;
|
|
@@ -64,3 +77,123 @@ describe('kit installer — payload + symlink-traversal hardening', () => {
|
|
|
64
77
|
assert.deepEqual(await readdir(real), []);
|
|
65
78
|
});
|
|
66
79
|
});
|
|
80
|
+
|
|
81
|
+
describe('kit installer — module hygiene', () => {
|
|
82
|
+
it('importing install.mjs runs nothing (main() is guarded by isDirectRun)', () => {
|
|
83
|
+
// `node -e` has no argv[1], so isDirectRun is false → importing must not run main()
|
|
84
|
+
// (no FS writes, no exit). A child process keeps the assertion off the test runner's own state.
|
|
85
|
+
const url = JSON.stringify(pathToFileURL(INSTALLER).href);
|
|
86
|
+
const res = spawnSync(
|
|
87
|
+
process.execPath,
|
|
88
|
+
['--input-type=module', '-e', `import(${url}).then(() => console.log('IMPORT_OK'));`],
|
|
89
|
+
{ encoding: 'utf8' },
|
|
90
|
+
);
|
|
91
|
+
assert.equal(res.status, 0, res.stderr);
|
|
92
|
+
assert.match(res.stdout, /IMPORT_OK/);
|
|
93
|
+
assert.doesNotMatch(res.stdout, /installed v|updated the kit/);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('kit installer — stale-cache defenses (no network)', () => {
|
|
98
|
+
let dir;
|
|
99
|
+
beforeEach(async () => {
|
|
100
|
+
dir = await mkdtemp(join(tmpdir(), 'aw-kit-stale-'));
|
|
101
|
+
});
|
|
102
|
+
afterEach(async () => {
|
|
103
|
+
await rm(dir, { recursive: true, force: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('a no-op re-run (same version) flags the likely npx cache and points at @latest', async () => {
|
|
107
|
+
const target = join(dir, 'agent-workflow-kit');
|
|
108
|
+
assert.equal(runInstaller(target).status, 0); // first install
|
|
109
|
+
const again = runInstaller(target); // second run: installed == running
|
|
110
|
+
assert.equal(again.status, 0, again.stderr);
|
|
111
|
+
assert.match(again.stdout, /no version change/i);
|
|
112
|
+
assert.match(again.stdout, /cache/i);
|
|
113
|
+
assert.match(again.stdout, /@latest/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('refuses to downgrade when the installed kit is NEWER, and writes nothing', async () => {
|
|
117
|
+
const target = join(dir, 'agent-workflow-kit');
|
|
118
|
+
assert.equal(runInstaller(target).status, 0);
|
|
119
|
+
await setInstalledVersion(target, '99.0.0'); // pretend a newer kit is already installed
|
|
120
|
+
const res = runInstaller(target); // running version < installed → downgrade
|
|
121
|
+
assert.notEqual(res.status, 0);
|
|
122
|
+
assert.match(res.stderr, /downgrade/i);
|
|
123
|
+
assert.match(res.stderr, /@latest/);
|
|
124
|
+
assert.equal(await getInstalledVersion(target), '99.0.0', 'newer install must be left untouched');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('--allow-downgrade overrides the refusal and overwrites with the runner version', async () => {
|
|
128
|
+
const target = join(dir, 'agent-workflow-kit');
|
|
129
|
+
assert.equal(runInstaller(target).status, 0);
|
|
130
|
+
await setInstalledVersion(target, '99.0.0');
|
|
131
|
+
const res = runInstaller(target, ['--allow-downgrade']);
|
|
132
|
+
assert.equal(res.status, 0, res.stderr);
|
|
133
|
+
assert.equal(await getInstalledVersion(target), await pkgVersion());
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('--force alone does NOT override the downgrade gate (the override is its own flag)', async () => {
|
|
137
|
+
const target = join(dir, 'agent-workflow-kit');
|
|
138
|
+
assert.equal(runInstaller(target).status, 0);
|
|
139
|
+
await setInstalledVersion(target, '99.0.0');
|
|
140
|
+
const res = runInstaller(target, ['--force']); // launcher-clobber flag, not the gate override
|
|
141
|
+
assert.notEqual(res.status, 0, 'launcher --force must not silently clear the version gate');
|
|
142
|
+
assert.equal(await getInstalledVersion(target), '99.0.0', 'newer install must be left untouched');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('reads the version under `metadata:`, never a top-level / nested decoy `version:`', async () => {
|
|
146
|
+
// A crafted SKILL.md whose REAL metadata.version (0.0.1) is OLDER than the runner, but with decoy
|
|
147
|
+
// `version:` lines that are NEWER. A naive "first version: in frontmatter" read would see 99.0.0
|
|
148
|
+
// and wrongly refuse; the correct read sees 0.0.1 → a normal upgrade (exit 0).
|
|
149
|
+
const target = join(dir, 'agent-workflow-kit');
|
|
150
|
+
await mkdir(target, { recursive: true });
|
|
151
|
+
const decoy = [
|
|
152
|
+
'---',
|
|
153
|
+
"version: '99.0.0'", // top-level decoy (column 0)
|
|
154
|
+
'name: agent-workflow-kit',
|
|
155
|
+
'metadata:',
|
|
156
|
+
' nested:',
|
|
157
|
+
" version: '98.0.0'", // deeper-nested decoy
|
|
158
|
+
" version: '0.0.1'", // the authoritative direct child
|
|
159
|
+
'---',
|
|
160
|
+
'# decoy',
|
|
161
|
+
'',
|
|
162
|
+
].join('\n');
|
|
163
|
+
await writeFile(join(target, 'SKILL.md'), decoy);
|
|
164
|
+
const res = runInstaller(target);
|
|
165
|
+
assert.equal(res.status, 0, `expected a normal upgrade (read 0.0.1), got: ${res.stderr}`);
|
|
166
|
+
assert.match(res.stdout, /updated the kit to/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('fails closed (does not silently treat as legacy) when an existing SKILL.md cannot be read', async () => {
|
|
170
|
+
// SKILL.md present but unreadable (a directory → EISDIR on read). The gate must NOT be bypassed:
|
|
171
|
+
// we refuse rather than overwrite a kit whose version we could not determine (no silent failure).
|
|
172
|
+
const target = join(dir, 'agent-workflow-kit');
|
|
173
|
+
await mkdir(join(target, 'SKILL.md'), { recursive: true });
|
|
174
|
+
const res = runInstaller(target);
|
|
175
|
+
assert.notEqual(res.status, 0);
|
|
176
|
+
assert.match(res.stderr, /cannot read the installed SKILL\.md/i);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('a legacy install with no version stamp still upgrades (no false downgrade, no crash)', async () => {
|
|
180
|
+
const target = join(dir, 'agent-workflow-kit');
|
|
181
|
+
await mkdir(target, { recursive: true });
|
|
182
|
+
await writeFile(join(target, 'SKILL.md'), '---\nname: agent-workflow-kit\n---\n# legacy stub\n');
|
|
183
|
+
const res = runInstaller(target);
|
|
184
|
+
assert.equal(res.status, 0, res.stderr);
|
|
185
|
+
assert.match(res.stdout, /updated the kit to/);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('kit installer — published tarball bundles the bridges', () => {
|
|
190
|
+
it('npm pack ships bridges/<name>/ (the execution-backend skill mirrors)', () => {
|
|
191
|
+
// The real `files` whitelist decides what publishes — assert against `npm pack`, not the source
|
|
192
|
+
// tree, so a dropped `bridges/` entry in package.json fails here (not silently at install time).
|
|
193
|
+
const res = spawnSync('npm', ['pack', '--dry-run', '--json'], { cwd: KIT_ROOT, encoding: 'utf8' });
|
|
194
|
+
assert.equal(res.status, 0, res.stderr || res.error?.message);
|
|
195
|
+
const paths = JSON.parse(res.stdout)[0].files.map((f) => f.path);
|
|
196
|
+
assert.ok(paths.includes('bridges/codex-cli-bridge/SKILL.md'), 'codex bridge SKILL.md not packed');
|
|
197
|
+
assert.ok(paths.includes('bridges/antigravity-cli-bridge/bin/agy.sh'), 'antigravity agy.sh not packed');
|
|
198
|
+
});
|
|
199
|
+
});
|