@sabaiway/agent-workflow-kit 1.15.0 → 1.15.2
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 +43 -0
- package/SKILL.md +3 -2
- package/capability.json +1 -1
- package/package.json +5 -2
- package/references/contracts.md +2 -0
- package/bin/install.test.mjs +0 -347
- package/tools/delegation.test.mjs +0 -116
- package/tools/detect-backends.test.mjs +0 -444
- package/tools/engine-source.test.mjs +0 -280
- package/tools/family-registry.test.mjs +0 -279
- package/tools/fs-safe.test.mjs +0 -346
- package/tools/hide-footprint.integration.test.mjs +0 -168
- package/tools/hide-footprint.test.mjs +0 -463
- package/tools/inject-methodology.test.mjs +0 -689
- package/tools/known-footprint.test.mjs +0 -271
- package/tools/manifest/fixtures/bad-available/SKILL.md +0 -7
- package/tools/manifest/fixtures/bad-available/capability.json +0 -10
- package/tools/manifest/fixtures/detect-array/SKILL.md +0 -7
- package/tools/manifest/fixtures/detect-array/capability.json +0 -10
- package/tools/manifest/fixtures/malformed-json/capability.json +0 -1
- package/tools/manifest/fixtures/metadata-version/SKILL.md +0 -10
- package/tools/manifest/fixtures/metadata-version/capability.json +0 -9
- package/tools/manifest/fixtures/missing-key/SKILL.md +0 -7
- package/tools/manifest/fixtures/missing-key/capability.json +0 -8
- package/tools/manifest/fixtures/missing-source/SKILL.md +0 -7
- package/tools/manifest/fixtures/missing-source/capability.json +0 -11
- package/tools/manifest/fixtures/nested-version-decoy/SKILL.md +0 -10
- package/tools/manifest/fixtures/nested-version-decoy/capability.json +0 -9
- package/tools/manifest/fixtures/null-root/capability.json +0 -1
- package/tools/manifest/fixtures/provides-roles-mismatch/SKILL.md +0 -7
- package/tools/manifest/fixtures/provides-roles-mismatch/bin/run.sh +0 -2
- package/tools/manifest/fixtures/provides-roles-mismatch/capability.json +0 -11
- package/tools/manifest/fixtures/stub/capability.json +0 -10
- package/tools/manifest/fixtures/traversal-source/SKILL.md +0 -7
- package/tools/manifest/fixtures/traversal-source/capability.json +0 -11
- package/tools/manifest/fixtures/unknown-schema/capability.json +0 -9
- package/tools/manifest/fixtures/valid/SKILL.md +0 -10
- package/tools/manifest/fixtures/valid/bin/run.sh +0 -3
- package/tools/manifest/fixtures/valid/capability.json +0 -18
- package/tools/manifest/fixtures/version-mismatch/SKILL.md +0 -7
- package/tools/manifest/fixtures/version-mismatch/capability.json +0 -9
- package/tools/manifest/fixtures/win-absolute-source/SKILL.md +0 -7
- package/tools/manifest/fixtures/win-absolute-source/capability.json +0 -11
- package/tools/manifest/validate.test.mjs +0 -73
- package/tools/procedures.test.mjs +0 -303
- package/tools/recipes.test.mjs +0 -538
- package/tools/release-scan.test.mjs +0 -41
- package/tools/setup-backends.test.mjs +0 -500
- package/tools/uninstall.integration.test.mjs +0 -144
- package/tools/uninstall.test.mjs +0 -401
- package/tools/velocity-profile.test.mjs +0 -496
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ 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.15.2 — Strip the package's own tests + fixtures from the npm tarball (kit)
|
|
8
|
+
|
|
9
|
+
Packaging only — no API/behaviour change; removed the package's own colocated tests + fixtures from
|
|
10
|
+
the published tarball, deploy/mirror payload tests retained. The deployment-lineage head stays
|
|
11
|
+
**`1.3.0`** (no `docs/ai` structural change, no migration file). The kit **package** version is a
|
|
12
|
+
separate axis.
|
|
13
|
+
|
|
14
|
+
- **`files[]` scoped negation.** Appended `!bin/*.test.mjs`, `!tools/**/*.test.mjs`, and
|
|
15
|
+
`!tools/manifest/fixtures/**` to the package allowlist (npm ignores a root `.npmignore` when
|
|
16
|
+
`files[]` is present, so negation entries in `files[]` are the mechanism). Tarball **115 → 69
|
|
17
|
+
files**: 18 of the package's own colocated tests + 28 manifest fixtures no longer ship.
|
|
18
|
+
- **Deploy/mirror payload tests retained.** `references/scripts/*.test.mjs` (deployed into a
|
|
19
|
+
consumer repo's `scripts/` with the docs-rotation scripts) and
|
|
20
|
+
`bridges/antigravity-cli-bridge/bin/agy.test.mjs` (part of the byte-identical bridge mirror the
|
|
21
|
+
installed kit links from) still ship — a blanket `!**/*.test.mjs` would have silently broken
|
|
22
|
+
installs, so the negation is deliberately scoped. **Never broaden it to `!references/**` or
|
|
23
|
+
`!bridges/**`.**
|
|
24
|
+
- **New tarball guard.** `test/package-content.test.mjs` (dev-only; outside `files[]`, never ships)
|
|
25
|
+
pins the exact shape via `npm pack --dry-run --json`: no own-test/fixture leak, payload tests +
|
|
26
|
+
runtime files present, exact file count `=== 69`.
|
|
27
|
+
- Test files stay on disk; the gate + publish CI run them from the checkout, unchanged. This is a
|
|
28
|
+
tarball-only exclusion.
|
|
29
|
+
|
|
30
|
+
## 1.15.1 — Version-axis clarity, hidden-mode invariant, lineage-head drift guard (kit)
|
|
31
|
+
|
|
32
|
+
Patch: documentation + a regression guard; no behaviour change to shipped tooling, and the
|
|
33
|
+
deployment-lineage head stays **`1.3.0`** (no `docs/ai` structural change, no migration file). The
|
|
34
|
+
kit **package** version is a separate axis.
|
|
35
|
+
|
|
36
|
+
- **`upgrade` now names the version axis in its report.** Steps 4 (equal-head exit) and 8 (re-stamp)
|
|
37
|
+
state that a project's stamp tracks the **deployment-lineage head** (`1.3.0`) — a separate axis from
|
|
38
|
+
the kit **package** version on npm/GitHub — so an equal-head report is no longer mistaken for a
|
|
39
|
+
stale deployment when GitHub shows a higher package number. A packaging-only release bumps the
|
|
40
|
+
package without moving the head; the head advances only when the deployed `docs/ai` structure does.
|
|
41
|
+
- **Hidden-mode maintenance invariant made explicit (Visibility contract).** Hidden mode changes only
|
|
42
|
+
what *git* sees — never the agent's duty to read/maintain `docs/ai`. "Git-ignored / `git status`
|
|
43
|
+
clean" is **not** "optional to update"; those updates simply live on disk and never enter a commit.
|
|
44
|
+
- **New cross-package drift guard** (`test/lineage-head-drift.test.mjs`): asserts the kit's
|
|
45
|
+
`EXPECTED_WORKFLOW_VERSION` equals the canonical `LINEAGE_HEAD` in `agent-workflow-memory`, so a
|
|
46
|
+
future head bump can't silently desync the two duplicated literals (which would make
|
|
47
|
+
`velocity --apply` reject a correctly-upgraded project). Runs in the gate; lives outside the package
|
|
48
|
+
`files` whitelist, so it is never shipped.
|
|
49
|
+
|
|
7
50
|
## 1.15.0 — Velocity-profile onboarding (kit)
|
|
8
51
|
|
|
9
52
|
An opt-in **`/agent-workflow-kit velocity`** mode seeds a fixed, audited **read-only** Claude Code
|
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.15.
|
|
6
|
+
version: '1.15.2'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
|
@@ -219,12 +219,13 @@ Fill strategy:
|
|
|
219
219
|
**Orchestration config ensure — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Ensure `docs/ai/orchestration.json` exists: **create it if missing**, **preserve it byte-for-byte if it already exists** (a user may have edited it — never clobber it). In the **delegated** path memory does this from its own template (memory upgrade step 2); in the **fallback** path the kit seeds it from `${CLAUDE_SKILL_DIR}/references/templates/orchestration.json`. Like the pointer slots + the footprint reconcile, this lets an equal-head (`1.3.0`) deployment gain the config seed **without a lineage-head bump or a migration file** (it is a `.json`, inherently outside the docs cap-validator). Report it in the step 4 / step 8 success report (config *seeded* vs *already present*).
|
|
220
220
|
4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the methodology-slot **and** hidden-mode footprint reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
|
|
221
221
|
- **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology and orchestration-recipes) whether it was *added*, was *already present* (nothing changed), or was *skipped because the entry point is over its line limit* (the cap-refusal soft-skip from step 3, with its reason); whether the `docs/ai/orchestration.json` config was *seeded* or was *already present* (a user edit is preserved); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (Gotcha: never leak kit internals).
|
|
222
|
+
- **Name the version axis so the package number isn't mistaken for a newer release.** When you state the deployment is up to date, say it is current at the **deployment-lineage head** (`1.3.0`) and add one plain line: this head versions the deployed `docs/ai` structure and is a **separate axis** from the kit **package** version published on npm/GitHub (the larger number users see on the repo/npm page). A packaging-only release can bump that package version **without** moving the head — the head advances only when the deployed `docs/ai` structure changes — so an equal-head report is **not** a stale deployment even though GitHub shows a higher package number. (This is the only place the two axes meet the user in `upgrade`; surfacing it here prevents the recurring "but GitHub shows a higher version" confusion.)
|
|
222
223
|
- **Print the one-line backend-status line** — the shared contract above (run `node ${CLAUDE_SKILL_DIR}/tools/detect-backends.mjs`; same format, invariants, and detector-unavailable skip-with-reason).
|
|
223
224
|
- **Then ask before committing — never auto-commit.** If step 3 added the slot (or anything else changed), report it and ask. If step 3 was a pure zero-diff no-op and nothing else changed, say **"already up to date"** and still print the read-only backend line.
|
|
224
225
|
5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
|
|
225
226
|
6. Apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those newer than the project's stamp. Migrations are **idempotent** — safe to re-run.
|
|
226
227
|
7. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. If `AGENTS.md` has no *Communication language* block (pre-1.1.0 deployment), **ask the user their conversational language** and insert the block — see `migrations/1.1.0-communication-language.md`. If it has no *Attribution* block (pre-1.2.0 deployment), **ask whether the agent may attribute work to itself / AI** and insert the block (defaulting to `off`) — see `migrations/1.2.0-agent-attribution.md`.
|
|
227
|
-
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`1.3.0`, not the package version). Report changes,
|
|
228
|
+
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`1.3.0`, not the package version). Report changes — and when you report the stamp, **name the axis**: the head versions the deployed `docs/ai` structure and is a separate axis from the kit **package** version on npm/GitHub (the larger number users see there), so the user doesn't read the bigger package number as a newer deployment (same framing as the step 4 equal-head exit). Then **print the one-line backend-status line** (the shared contract above — same detector, format, invariants, and detector-unavailable skip-with-reason). Then **ask before committing**.
|
|
228
229
|
|
|
229
230
|
### Mode: backends
|
|
230
231
|
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.2",
|
|
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",
|
|
@@ -50,7 +50,10 @@
|
|
|
50
50
|
"launchers/",
|
|
51
51
|
"migrations/",
|
|
52
52
|
"tools/",
|
|
53
|
-
"bridges/"
|
|
53
|
+
"bridges/",
|
|
54
|
+
"!bin/*.test.mjs",
|
|
55
|
+
"!tools/**/*.test.mjs",
|
|
56
|
+
"!tools/manifest/fixtures/**"
|
|
54
57
|
],
|
|
55
58
|
"engines": {
|
|
56
59
|
"node": ">=18"
|
package/references/contracts.md
CHANGED
|
@@ -19,6 +19,8 @@ diverge:
|
|
|
19
19
|
- **visible** — artifacts are committed. Wire the project's `package.json` scripts (`docs:check` / `docs:index` / `docs:index:check` / `docs:archive` / `docs:archive:check` / `docs:archive:issues` / `docs:archive:issues:check` / `prepare: node scripts/install-git-hooks.mjs`) and add a minimal `.gitignore` (`docs/plans/`, `.claude/settings.local.json`). This is the canonical model.
|
|
20
20
|
- **hidden** (in-tree) — same files on disk, but the repo "looks normal": the AI/agent footprint is git-ignored via **one managed block in the project-local `.git/info/exclude`** (resolved with `git rev-parse --git-path info/exclude` — never the machine-global `core.excludesFile`, which would affect every repo on the host; **AD-014** amends **AD-006**). The kit's `tools/hide-footprint.mjs` is the single writer: it covers `KIT_OWN_PATHS ∪ the present subset of KNOWN_FOOTPRINT` (the full footprint table below — the kit's own artifacts **and** every other AI/agent tool's files). Per path: a **tracked** file → **ASK** (an exclude does nothing for it; only `git rm --cached` un-tracks it — the tool prints that command and never runs it); an untracked path already covered by a **tracked `.gitignore`** → dropped (redundant); a **present** file whose name is generic enough to be ambiguous (`falsePositiveRisk`) → **ASK**; everything else → **hidden**. `asks` are excluded from the block unless explicitly opted in. **Verify** treats a path as hidden only when it is **untracked AND ignored by our project-local block** (or a tracked `.gitignore`) — being ignored by the global excludes does **not** count. Re-running re-derives the block wholesale (sorted/deduped) → a clean re-run is a **zero-diff** no-op. On an existing global-excludes deployment the tool **detects + reports the residual legacy machine-global block and keeps it by default** (a harmless double-ignore; the local block wins precedence); removal is the explicit `--remove-global` (it prints the removed lines as a restorable backup), which **the agent only runs after asking** — another of the user's hidden repos on the same host may rely on the same root-anchored global lines. **Do not edit `package.json`** — that is a tracked change and would leak; the pre-commit hook (always untracked in `.git/hooks/`) calls the scripts via `node scripts/<x>.mjs` directly. Windows is supported (text edit, no symlinks; CRLF preserved).
|
|
21
21
|
|
|
22
|
+
**Visibility changes what git sees — never how the agent works (the load-bearing invariant).** Both modes carry the **same** working memory and run the **same** session protocol: `docs/ai` and the deployed workflow artifacts (`AGENTS.md`, the added `scripts/`) are read at session start and maintained on task completion **identically** whether visible or hidden. (Hidden mode git-ignores a *wider* footprint — other AI/agent tools' files too — but that is only a **visibility** surface; the working memory the agent actually reads and maintains is `docs/ai` plus those artifacts.) Hidden mode only makes the repo *look* normal to git; it is **never** a license to skip, defer, or down-prioritise a doc/state update. The single difference is the destination: in hidden mode those updates live on disk and never enter a commit (the footprint is excluded), so a commit there carries only the project's own tracked files while `docs/ai` is still kept current locally — **git-ignored ≠ optional-to-maintain**. An agent that reads "git-ignored / `git status` clean" and infers the working memory is optional has the model backwards. (This is why bootstrap/upgrade never wire hidden-mode docs into a commit, yet the session protocol still mandates maintaining them.)
|
|
23
|
+
|
|
22
24
|
**Known AI/agent footprint** (the `KNOWN_FOOTPRINT` registry in `tools/known-footprint.mjs`; this table is its human mirror, kept in sync by review — D11):
|
|
23
25
|
|
|
24
26
|
| Pattern | Owner | Kind | Commit-risk name? | Note |
|
package/bin/install.test.mjs
DELETED
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { mkdtemp, rm, mkdir, symlink, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { existsSync } from 'node:fs';
|
|
6
|
-
import { tmpdir } from 'node:os';
|
|
7
|
-
import { dirname, join } from 'node:path';
|
|
8
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
-
|
|
10
|
-
import { engineInstallArgv, installEngine, ENGINE_PACKAGE } from './install.mjs';
|
|
11
|
-
|
|
12
|
-
const INSTALLER = join(dirname(fileURLToPath(import.meta.url)), 'install.mjs');
|
|
13
|
-
const KIT_ROOT = dirname(dirname(INSTALLER));
|
|
14
|
-
// --no-launchers so the test never wires Codex/Devin on the host; --no-engine so a full install never
|
|
15
|
-
// spawns a real `npx … agent-workflow-engine init` (network + a write to the real engine dir). The
|
|
16
|
-
// dedicated engine-step tests cover that path in-process / via a deliberately-broken PATH. `extra`
|
|
17
|
-
// appends flags (e.g. --force, --allow-downgrade).
|
|
18
|
-
const runInstaller = (target, extra = []) =>
|
|
19
|
-
spawnSync(process.execPath, [INSTALLER, '--dir', target, '--no-launchers', '--no-engine', ...extra], { encoding: 'utf8' });
|
|
20
|
-
|
|
21
|
-
// Rewrite / read the installed skill's frontmatter version — used to simulate "a newer kit is already
|
|
22
|
-
// installed" (the stale-cache downgrade scenario). The installer reads exactly this field.
|
|
23
|
-
const setInstalledVersion = async (target, v) => {
|
|
24
|
-
const p = join(target, 'SKILL.md');
|
|
25
|
-
const text = await readFile(p, 'utf8');
|
|
26
|
-
await writeFile(p, text.replace(/version:\s*['"]?\d+\.\d+\.\d+['"]?/, `version: '${v}'`));
|
|
27
|
-
};
|
|
28
|
-
const getInstalledVersion = async (target) =>
|
|
29
|
-
(await readFile(join(target, 'SKILL.md'), 'utf8')).match(/version:\s*['"]?(\d+\.\d+\.\d+)['"]?/)?.[1] ?? null;
|
|
30
|
-
const pkgVersion = async () =>
|
|
31
|
-
JSON.parse(await readFile(join(KIT_ROOT, 'package.json'), 'utf8')).version;
|
|
32
|
-
|
|
33
|
-
describe('kit installer — payload + symlink-traversal hardening', () => {
|
|
34
|
-
let dir;
|
|
35
|
-
beforeEach(async () => {
|
|
36
|
-
dir = await mkdtemp(join(tmpdir(), 'aw-kit-install-'));
|
|
37
|
-
});
|
|
38
|
-
afterEach(async () => {
|
|
39
|
-
await rm(dir, { recursive: true, force: true });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('installs the critical payload (capability.json + tools/ + references/) and not the npm wrapper', () => {
|
|
43
|
-
const target = join(dir, 'agent-workflow-kit');
|
|
44
|
-
const res = runInstaller(target);
|
|
45
|
-
assert.equal(res.status, 0, res.stderr);
|
|
46
|
-
for (const f of ['SKILL.md', 'capability.json', 'tools/manifest/validate.mjs', 'tools/delegation.mjs', 'references']) {
|
|
47
|
-
assert.ok(existsSync(join(target, f)), `missing installed entry: ${f}`);
|
|
48
|
-
}
|
|
49
|
-
assert.equal(existsSync(join(target, 'bin/install.mjs')), false);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('removes retired mirror files an older install left behind (single source of truth on upgrade)', async () => {
|
|
53
|
-
const target = join(dir, 'agent-workflow-kit');
|
|
54
|
-
// Seed a pre-3D install carrying the now-retired bundled mirror files.
|
|
55
|
-
await mkdir(join(target, 'references'), { recursive: true });
|
|
56
|
-
await mkdir(join(target, 'tools'), { recursive: true });
|
|
57
|
-
await writeFile(join(target, 'references', 'planning.md'), 'stale mirror\n');
|
|
58
|
-
await writeFile(join(target, 'tools', 'methodology-slot.md'), 'stale mirror\n');
|
|
59
|
-
const res = runInstaller(target);
|
|
60
|
-
assert.equal(res.status, 0, res.stderr);
|
|
61
|
-
assert.equal(existsSync(join(target, 'references', 'planning.md')), false, 'retired references/planning.md removed');
|
|
62
|
-
assert.equal(existsSync(join(target, 'tools', 'methodology-slot.md')), false, 'retired tools/methodology-slot.md removed');
|
|
63
|
-
assert.ok(existsSync(join(target, 'SKILL.md')), 'the real payload is still installed');
|
|
64
|
-
assert.ok(existsSync(join(target, 'references', 'contracts.md')), 'non-retired references/ content survives');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('refuses to write through a symlinked INTERMEDIATE dest component (no leak)', async () => {
|
|
68
|
-
const target = join(dir, 'target');
|
|
69
|
-
const evil = join(dir, 'evil');
|
|
70
|
-
await mkdir(target, { recursive: true });
|
|
71
|
-
await mkdir(evil, { recursive: true });
|
|
72
|
-
await symlink(evil, join(target, 'references'));
|
|
73
|
-
const res = runInstaller(target);
|
|
74
|
-
assert.notEqual(res.status, 0);
|
|
75
|
-
assert.match(res.stderr, /symlink/i);
|
|
76
|
-
assert.deepEqual(await readdir(evil), []);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('refuses a DANGLING destination symlink', async () => {
|
|
80
|
-
const target = join(dir, 'target');
|
|
81
|
-
await mkdir(target, { recursive: true });
|
|
82
|
-
await symlink(join(dir, 'nowhere'), join(target, 'references'));
|
|
83
|
-
const res = runInstaller(target);
|
|
84
|
-
assert.notEqual(res.status, 0);
|
|
85
|
-
assert.match(res.stderr, /symlink/i);
|
|
86
|
-
assert.equal(existsSync(join(dir, 'nowhere')), false);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('refuses a symlinked TARGET root (no leak)', async () => {
|
|
90
|
-
const real = join(dir, 'real');
|
|
91
|
-
const root = join(dir, 'root');
|
|
92
|
-
await mkdir(real, { recursive: true });
|
|
93
|
-
await symlink(real, root);
|
|
94
|
-
const res = runInstaller(root);
|
|
95
|
-
assert.notEqual(res.status, 0);
|
|
96
|
-
assert.match(res.stderr, /symlink/i);
|
|
97
|
-
assert.deepEqual(await readdir(real), []);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe('kit installer — runs through the npx bin symlink', () => {
|
|
102
|
-
let dir;
|
|
103
|
-
beforeEach(async () => {
|
|
104
|
-
dir = await mkdtemp(join(tmpdir(), 'aw-kit-symlink-'));
|
|
105
|
-
});
|
|
106
|
-
afterEach(async () => {
|
|
107
|
-
await rm(dir, { recursive: true, force: true });
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('main() runs when invoked through a symlink (the node_modules/.bin shim npx uses)', async () => {
|
|
111
|
-
// npx never runs bin/install.mjs by its real path — it runs node_modules/.bin/agent-workflow-kit,
|
|
112
|
-
// a symlink to it. Node resolves import.meta.url to the REAL file but leaves argv[1] as the symlink,
|
|
113
|
-
// so a string compare of the two makes isDirectRun false and main() silently no-ops. Reproduce that
|
|
114
|
-
// exact invocation: run the installer via a symlink and assert it actually installed (visible output
|
|
115
|
-
// + payload on disk). A regression here means `npx … init` goes quiet again.
|
|
116
|
-
const shim = join(dir, 'agent-workflow-kit'); // stands in for node_modules/.bin/<name>
|
|
117
|
-
await symlink(INSTALLER, shim);
|
|
118
|
-
const target = join(dir, 'home', 'agent-workflow-kit');
|
|
119
|
-
const res = spawnSync(process.execPath, [shim, '--dir', target, '--no-launchers', '--no-engine'], { encoding: 'utf8' });
|
|
120
|
-
assert.equal(res.status, 0, res.stderr);
|
|
121
|
-
assert.match(res.stdout, /installed v|updated the kit/);
|
|
122
|
-
assert.ok(existsSync(join(target, 'SKILL.md')), 'install through the symlink must write the payload');
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe('kit installer — module hygiene', () => {
|
|
127
|
-
it('importing install.mjs runs nothing (main() is guarded by isDirectRun)', () => {
|
|
128
|
-
// `node -e` has no argv[1], so isDirectRun is false → importing must not run main()
|
|
129
|
-
// (no FS writes, no exit). A child process keeps the assertion off the test runner's own state.
|
|
130
|
-
const url = JSON.stringify(pathToFileURL(INSTALLER).href);
|
|
131
|
-
const res = spawnSync(
|
|
132
|
-
process.execPath,
|
|
133
|
-
['--input-type=module', '-e', `import(${url}).then(() => console.log('IMPORT_OK'));`],
|
|
134
|
-
{ encoding: 'utf8' },
|
|
135
|
-
);
|
|
136
|
-
assert.equal(res.status, 0, res.stderr);
|
|
137
|
-
assert.match(res.stdout, /IMPORT_OK/);
|
|
138
|
-
assert.doesNotMatch(res.stdout, /installed v|updated the kit/);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe('kit installer — stale-cache defenses (no network)', () => {
|
|
143
|
-
let dir;
|
|
144
|
-
beforeEach(async () => {
|
|
145
|
-
dir = await mkdtemp(join(tmpdir(), 'aw-kit-stale-'));
|
|
146
|
-
});
|
|
147
|
-
afterEach(async () => {
|
|
148
|
-
await rm(dir, { recursive: true, force: true });
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('a no-op re-run (same version) flags the likely npx cache and points at @latest', async () => {
|
|
152
|
-
const target = join(dir, 'agent-workflow-kit');
|
|
153
|
-
assert.equal(runInstaller(target).status, 0); // first install
|
|
154
|
-
const again = runInstaller(target); // second run: installed == running
|
|
155
|
-
assert.equal(again.status, 0, again.stderr);
|
|
156
|
-
assert.match(again.stdout, /no version change/i);
|
|
157
|
-
assert.match(again.stdout, /cache/i);
|
|
158
|
-
assert.match(again.stdout, /@latest/);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('refuses to downgrade when the installed kit is NEWER, and writes nothing', async () => {
|
|
162
|
-
const target = join(dir, 'agent-workflow-kit');
|
|
163
|
-
assert.equal(runInstaller(target).status, 0);
|
|
164
|
-
await setInstalledVersion(target, '99.0.0'); // pretend a newer kit is already installed
|
|
165
|
-
const res = runInstaller(target); // running version < installed → downgrade
|
|
166
|
-
assert.notEqual(res.status, 0);
|
|
167
|
-
assert.match(res.stderr, /downgrade/i);
|
|
168
|
-
assert.match(res.stderr, /@latest/);
|
|
169
|
-
assert.equal(await getInstalledVersion(target), '99.0.0', 'newer install must be left untouched');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('--allow-downgrade overrides the refusal and overwrites with the runner version', async () => {
|
|
173
|
-
const target = join(dir, 'agent-workflow-kit');
|
|
174
|
-
assert.equal(runInstaller(target).status, 0);
|
|
175
|
-
await setInstalledVersion(target, '99.0.0');
|
|
176
|
-
const res = runInstaller(target, ['--allow-downgrade']);
|
|
177
|
-
assert.equal(res.status, 0, res.stderr);
|
|
178
|
-
assert.equal(await getInstalledVersion(target), await pkgVersion());
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('--force alone does NOT override the downgrade gate (the override is its own flag)', async () => {
|
|
182
|
-
const target = join(dir, 'agent-workflow-kit');
|
|
183
|
-
assert.equal(runInstaller(target).status, 0);
|
|
184
|
-
await setInstalledVersion(target, '99.0.0');
|
|
185
|
-
const res = runInstaller(target, ['--force']); // launcher-clobber flag, not the gate override
|
|
186
|
-
assert.notEqual(res.status, 0, 'launcher --force must not silently clear the version gate');
|
|
187
|
-
assert.equal(await getInstalledVersion(target), '99.0.0', 'newer install must be left untouched');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('reads the version under `metadata:`, never a top-level / nested decoy `version:`', async () => {
|
|
191
|
-
// A crafted SKILL.md whose REAL metadata.version (0.0.1) is OLDER than the runner, but with decoy
|
|
192
|
-
// `version:` lines that are NEWER. A naive "first version: in frontmatter" read would see 99.0.0
|
|
193
|
-
// and wrongly refuse; the correct read sees 0.0.1 → a normal upgrade (exit 0).
|
|
194
|
-
const target = join(dir, 'agent-workflow-kit');
|
|
195
|
-
await mkdir(target, { recursive: true });
|
|
196
|
-
const decoy = [
|
|
197
|
-
'---',
|
|
198
|
-
"version: '99.0.0'", // top-level decoy (column 0)
|
|
199
|
-
'name: agent-workflow-kit',
|
|
200
|
-
'metadata:',
|
|
201
|
-
' nested:',
|
|
202
|
-
" version: '98.0.0'", // deeper-nested decoy
|
|
203
|
-
" version: '0.0.1'", // the authoritative direct child
|
|
204
|
-
'---',
|
|
205
|
-
'# decoy',
|
|
206
|
-
'',
|
|
207
|
-
].join('\n');
|
|
208
|
-
await writeFile(join(target, 'SKILL.md'), decoy);
|
|
209
|
-
const res = runInstaller(target);
|
|
210
|
-
assert.equal(res.status, 0, `expected a normal upgrade (read 0.0.1), got: ${res.stderr}`);
|
|
211
|
-
assert.match(res.stdout, /updated the kit to/);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('fails closed (does not silently treat as legacy) when an existing SKILL.md cannot be read', async () => {
|
|
215
|
-
// SKILL.md present but unreadable (a directory → EISDIR on read). The gate must NOT be bypassed:
|
|
216
|
-
// we refuse rather than overwrite a kit whose version we could not determine (no silent failure).
|
|
217
|
-
const target = join(dir, 'agent-workflow-kit');
|
|
218
|
-
await mkdir(join(target, 'SKILL.md'), { recursive: true });
|
|
219
|
-
const res = runInstaller(target);
|
|
220
|
-
assert.notEqual(res.status, 0);
|
|
221
|
-
assert.match(res.stderr, /cannot read the installed SKILL\.md/i);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('a legacy install with no version stamp still upgrades (no false downgrade, no crash)', async () => {
|
|
225
|
-
const target = join(dir, 'agent-workflow-kit');
|
|
226
|
-
await mkdir(target, { recursive: true });
|
|
227
|
-
await writeFile(join(target, 'SKILL.md'), '---\nname: agent-workflow-kit\n---\n# legacy stub\n');
|
|
228
|
-
const res = runInstaller(target);
|
|
229
|
-
assert.equal(res.status, 0, res.stderr);
|
|
230
|
-
assert.match(res.stdout, /updated the kit to/);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
describe('kit installer — mandatory engine install dispatch (Plan 3D, in-process, no network)', () => {
|
|
235
|
-
it('engineInstallArgv: `npx @…/engine@latest init` on POSIX, `npx.cmd` on win32, no shell:true', () => {
|
|
236
|
-
const posix = engineInstallArgv('linux');
|
|
237
|
-
assert.equal(posix.command, 'npx');
|
|
238
|
-
assert.deepEqual(posix.args, [`${ENGINE_PACKAGE}@latest`, 'init']);
|
|
239
|
-
assert.equal(posix.options.shell, undefined, 'must not spawn through a shell');
|
|
240
|
-
assert.equal(engineInstallArgv('darwin').command, 'npx');
|
|
241
|
-
assert.equal(engineInstallArgv('win32').command, 'npx.cmd');
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('installEngine: first attempt succeeds → ok, runner called once (no retry)', () => {
|
|
245
|
-
let calls = 0;
|
|
246
|
-
const res = installEngine('linux', () => {
|
|
247
|
-
calls += 1;
|
|
248
|
-
return { status: 0 };
|
|
249
|
-
});
|
|
250
|
-
assert.deepEqual(res, { ok: true });
|
|
251
|
-
assert.equal(calls, 1);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const NO_SLEEP = { sleep: () => {} }; // skip the real backoff so the suite never actually waits
|
|
255
|
-
|
|
256
|
-
it('installEngine: fail once then succeed → backoff + retried exactly once, ends ok (D1)', () => {
|
|
257
|
-
let calls = 0;
|
|
258
|
-
let slept = 0;
|
|
259
|
-
const res = installEngine('linux', () => {
|
|
260
|
-
calls += 1;
|
|
261
|
-
return { status: calls === 1 ? 1 : 0 };
|
|
262
|
-
}, { sleep: () => { slept += 1; } });
|
|
263
|
-
assert.deepEqual(res, { ok: true });
|
|
264
|
-
assert.equal(calls, 2);
|
|
265
|
-
assert.equal(slept, 1, 'backoff runs exactly once, before the single retry');
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('installEngine: fails twice → not ok (D1 hard-failure outcome), no third attempt', () => {
|
|
269
|
-
let calls = 0;
|
|
270
|
-
const res = installEngine('linux', () => {
|
|
271
|
-
calls += 1;
|
|
272
|
-
return { status: 1 };
|
|
273
|
-
}, NO_SLEEP);
|
|
274
|
-
assert.deepEqual(res, { ok: false });
|
|
275
|
-
assert.equal(calls, 2);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('installEngine: a spawn error (npx not found) counts as a failure', () => {
|
|
279
|
-
const res = installEngine('linux', () => ({ status: null, error: new Error('spawn npx ENOENT') }), NO_SLEEP);
|
|
280
|
-
assert.deepEqual(res, { ok: false });
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('installEngine: hands the runner the exact descriptor (command/args/options, no shell)', () => {
|
|
284
|
-
let received;
|
|
285
|
-
installEngine('win32', (d) => {
|
|
286
|
-
received = d;
|
|
287
|
-
return { status: 0 };
|
|
288
|
-
});
|
|
289
|
-
assert.equal(received.command, 'npx.cmd');
|
|
290
|
-
assert.deepEqual(received.args, [`${ENGINE_PACKAGE}@latest`, 'init']);
|
|
291
|
-
assert.equal(received.options.shell, undefined);
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
describe('kit installer — mandatory engine install (subprocess)', () => {
|
|
296
|
-
let dir;
|
|
297
|
-
beforeEach(async () => {
|
|
298
|
-
dir = await mkdtemp(join(tmpdir(), 'aw-kit-engine-'));
|
|
299
|
-
});
|
|
300
|
-
afterEach(async () => {
|
|
301
|
-
await rm(dir, { recursive: true, force: true });
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('--no-engine skips the engine install and prints the live-read STOP note (exit 0)', () => {
|
|
305
|
-
const target = join(dir, 'agent-workflow-kit'); // runInstaller appends --no-engine by default
|
|
306
|
-
const res = runInstaller(target);
|
|
307
|
-
assert.equal(res.status, 0, res.stderr);
|
|
308
|
-
assert.match(res.stdout, /--no-engine: skipped installing the methodology engine/);
|
|
309
|
-
assert.match(res.stdout, /@latest init/);
|
|
310
|
-
assert.match(res.stdout, /installed v|updated the kit/, 'the kit itself still installs');
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('D1: when `npx` cannot run (both attempts fail) → nonzero exit, recommendations, success block NOT printed first', async () => {
|
|
314
|
-
// Force both engine-install attempts to fail deterministically WITHOUT network: run a real install
|
|
315
|
-
// (no --no-engine) with an empty PATH so `npx` resolves to ENOENT. The kit copy + the version gate
|
|
316
|
-
// do not need PATH; only the engine spawn does. This exercises the D1 loud-error + nonzero exit.
|
|
317
|
-
const target = join(dir, 'agent-workflow-kit');
|
|
318
|
-
const emptyBin = join(dir, 'emptybin');
|
|
319
|
-
await mkdir(emptyBin, { recursive: true });
|
|
320
|
-
const res = spawnSync(process.execPath, [INSTALLER, '--dir', target, '--no-launchers'], {
|
|
321
|
-
encoding: 'utf8',
|
|
322
|
-
env: { ...process.env, PATH: emptyBin },
|
|
323
|
-
});
|
|
324
|
-
assert.notEqual(res.status, 0, 'an engine-install failure must exit nonzero');
|
|
325
|
-
assert.match(res.stderr, /FAILED to install the methodology engine/);
|
|
326
|
-
assert.match(res.stderr, /@latest init/, 'recommends the manual engine install');
|
|
327
|
-
assert.match(res.stderr, /--no-engine/, 'recommends the opt-out');
|
|
328
|
-
assert.doesNotMatch(res.stdout, /Next — open your agent/, 'must NOT claim success before the engine failed');
|
|
329
|
-
assert.ok(existsSync(join(target, 'SKILL.md')), 'the kit itself is still on disk (recovery is one step)');
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
describe('kit installer — published tarball bundles the bridges + the live-read tool', () => {
|
|
334
|
-
it('npm pack ships bridges/<name>/ and tools/engine-source.mjs', () => {
|
|
335
|
-
// The real `files` whitelist decides what publishes — assert against `npm pack`, not the source
|
|
336
|
-
// tree, so a dropped `bridges/` entry in package.json fails here (not silently at install time).
|
|
337
|
-
const res = spawnSync('npm', ['pack', '--dry-run', '--json'], { cwd: KIT_ROOT, encoding: 'utf8' });
|
|
338
|
-
assert.equal(res.status, 0, res.stderr || res.error?.message);
|
|
339
|
-
const paths = JSON.parse(res.stdout)[0].files.map((f) => f.path);
|
|
340
|
-
assert.ok(paths.includes('bridges/codex-cli-bridge/SKILL.md'), 'codex bridge SKILL.md not packed');
|
|
341
|
-
assert.ok(paths.includes('bridges/antigravity-cli-bridge/bin/agy.sh'), 'antigravity agy.sh not packed');
|
|
342
|
-
assert.ok(paths.includes('tools/engine-source.mjs'), 'the live-read resolver must ship in the tarball');
|
|
343
|
-
// The retired mirror must NOT ship — the whole point of Plan 3D is one source of truth.
|
|
344
|
-
assert.ok(!paths.includes('tools/methodology-slot.md'), 'retired mirror tools/methodology-slot.md must not be packed');
|
|
345
|
-
assert.ok(!paths.includes('references/planning.md'), 'retired mirror references/planning.md must not be packed');
|
|
346
|
-
});
|
|
347
|
-
});
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { detectMemory, handoffPlan, REQUIRED_MEMORY_ASSETS } from './delegation.mjs';
|
|
4
|
-
|
|
5
|
-
// Inject a fake validator + fs so the decision matrix is tested independent of real files/agents.
|
|
6
|
-
const fakeValidate = (over = {}) => () => ({
|
|
7
|
-
result: 'valid',
|
|
8
|
-
kind: 'memory-substrate',
|
|
9
|
-
name: 'agent-workflow-memory',
|
|
10
|
-
available: true,
|
|
11
|
-
errors: [],
|
|
12
|
-
...over,
|
|
13
|
-
});
|
|
14
|
-
const ASSET_TYPE = {
|
|
15
|
-
'references/templates': 'dir',
|
|
16
|
-
'references/contracts.md': 'file',
|
|
17
|
-
'references/scripts': 'dir',
|
|
18
|
-
'scripts/stamp-takeover.mjs': 'file',
|
|
19
|
-
migrations: 'dir',
|
|
20
|
-
'capability.json': 'file',
|
|
21
|
-
};
|
|
22
|
-
const typeFor = (p) => {
|
|
23
|
-
for (const [k, t] of Object.entries(ASSET_TYPE)) if (p.endsWith(k)) return t;
|
|
24
|
-
return 'file';
|
|
25
|
-
};
|
|
26
|
-
const allPresent = (p) => typeFor(p);
|
|
27
|
-
const missing = (absent) => (p) => (absent.some((a) => p.endsWith(a)) ? null : typeFor(p));
|
|
28
|
-
const wrongType = (paths) => (p) => {
|
|
29
|
-
for (const [k, t] of Object.entries(ASSET_TYPE)) {
|
|
30
|
-
if (p.endsWith(k)) return paths.includes(k) ? (t === 'dir' ? 'file' : 'dir') : t;
|
|
31
|
-
}
|
|
32
|
-
return 'file';
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
describe('detectMemory — decision matrix', () => {
|
|
36
|
-
it('valid + memory-substrate + right name + available + all assets → delegate', () => {
|
|
37
|
-
const d = detectMemory('/m', { validate: fakeValidate(), statType: allPresent });
|
|
38
|
-
assert.equal(d.delegate, true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('invalid manifest → fallback', () => {
|
|
42
|
-
const d = detectMemory('/m', { validate: fakeValidate({ result: 'invalid' }), statType: allPresent });
|
|
43
|
-
assert.equal(d.delegate, false);
|
|
44
|
-
assert.match(d.reason, /invalid/);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('unsupported schema → fallback (treated like invalid)', () => {
|
|
48
|
-
const d = detectMemory('/m', { validate: fakeValidate({ result: 'unsupported' }), statType: allPresent });
|
|
49
|
-
assert.equal(d.delegate, false);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('wrong kind → fallback', () => {
|
|
53
|
-
const d = detectMemory('/m', { validate: fakeValidate({ kind: 'composition-root' }), statType: allPresent });
|
|
54
|
-
assert.equal(d.delegate, false);
|
|
55
|
-
assert.match(d.reason, /not memory-substrate/);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('wrong name → fallback (even if kind + assets are right)', () => {
|
|
59
|
-
const d = detectMemory('/m', { validate: fakeValidate({ name: 'evil-substrate' }), statType: allPresent });
|
|
60
|
-
assert.equal(d.delegate, false);
|
|
61
|
-
assert.match(d.reason, /name/);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('available:false stub → fallback', () => {
|
|
65
|
-
const d = detectMemory('/m', { validate: fakeValidate({ available: false }), statType: allPresent });
|
|
66
|
-
assert.equal(d.delegate, false);
|
|
67
|
-
assert.match(d.reason, /stub/);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('partial install (missing stamp-takeover) → fallback', () => {
|
|
71
|
-
const d = detectMemory('/m', {
|
|
72
|
-
validate: fakeValidate(),
|
|
73
|
-
statType: missing(['scripts/stamp-takeover.mjs']),
|
|
74
|
-
});
|
|
75
|
-
assert.equal(d.delegate, false);
|
|
76
|
-
assert.match(d.reason, /stamp-takeover/);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('wrong-type asset (templates is a file, not a dir) → fallback', () => {
|
|
80
|
-
const d = detectMemory('/m', { validate: fakeValidate(), statType: wrongType(['references/templates']) });
|
|
81
|
-
assert.equal(d.delegate, false);
|
|
82
|
-
assert.match(d.reason, /references\/templates/);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('required assets use real (references/) paths', () => {
|
|
86
|
-
const paths = REQUIRED_MEMORY_ASSETS.map((a) => a.path);
|
|
87
|
-
assert.ok(paths.includes('references/templates'));
|
|
88
|
-
assert.ok(paths.includes('references/contracts.md'));
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('handoffPlan — stamp sets + single commit gate', () => {
|
|
93
|
-
it('delegate → both stamps present; memory never raises its own commit gate', () => {
|
|
94
|
-
const p = handoffPlan(true);
|
|
95
|
-
assert.deepEqual(p.stampsPresent, ['.memory-version', '.workflow-version']);
|
|
96
|
-
assert.equal(p.memoryRaisesCommitGate, false);
|
|
97
|
-
assert.equal(p.commitGate, 'kit-only-after-injection');
|
|
98
|
-
assert.ok(p.memoryWrites.includes('docs/ai/.memory-version'));
|
|
99
|
-
// Delegate is the ONLY branch with a real slot (memory ships it empty; the kit injects).
|
|
100
|
-
assert.ok(p.kitWrites.some((w) => w.includes('slot')), 'delegate kitWrites should name the methodology slot');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('fallback → only .workflow-version; kit writes everything; one kit gate', () => {
|
|
104
|
-
const p = handoffPlan(false);
|
|
105
|
-
assert.deepEqual(p.stampsPresent, ['.workflow-version']);
|
|
106
|
-
assert.deepEqual(p.memoryWrites, []);
|
|
107
|
-
assert.equal(p.memoryRaisesCommitGate, false);
|
|
108
|
-
assert.equal(p.commitGate, 'kit-only-after-injection');
|
|
109
|
-
// Fallback now ships the kit's own AGENTS.md with the EMPTY methodology slot, which the kit
|
|
110
|
-
// reconciles + fills — the same slot mechanism as the delegate path (Plan 2).
|
|
111
|
-
assert.ok(
|
|
112
|
-
p.kitWrites.some((w) => w.includes('slot')) && !p.kitWrites.some((w) => w.includes('inline')),
|
|
113
|
-
'fallback kitWrites should describe the methodology slot, not inline methodology',
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
});
|