@sabaiway/agent-workflow-engine 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to the methodology engine. Versions are this **package's** n
4
4
  they are distinct from the **deployment-lineage** stamp written into a project's `docs/ai/`
5
5
  (which tracks the shared `agent-workflow` lineage, head `1.3.0`).
6
6
 
7
+ ## 1.1.0 — Live-read ready: never-downgrade gate + installer hardening
8
+
9
+ The kit now reads this canon **live from the installed engine** and has retired its bundled mirror
10
+ (see the kit's 1.11.0 / **AD-016**). This release hardens the engine's own installer to match — so an
11
+ engine placed by `npx … kit init` (or by hand) is safe to refresh.
12
+
13
+ ### Added
14
+ - **Never-downgrade gate** (cloned from the kit, AD-012): a bare `npx … init` that npx serves from an
15
+ **older cached build** can no longer overwrite a **newer** installed canon — `init` compares the
16
+ on-disk version (no network) and refuses loudly unless you pass `--allow-downgrade`. A same-version
17
+ re-run prints a cache hint and points at `@latest`. An existing but **unreadable** `SKILL.md` fails
18
+ closed (the gate is never silently bypassed).
19
+
20
+ ### Fixed
21
+ - Containment check now accepts a legitimately-contained child literally named `..foo` (it wrongly
22
+ rejected anything starting with `..` before); `tildify` collapses only a **leading** `$HOME`, never
23
+ a mid-path occurrence (**Issue-004**, fixed in lockstep with the memory installer).
24
+
25
+ ### Changed
26
+ - The installer is importable without side effects (the `isDirectRun` guard) and exports its
27
+ path/format helpers for in-process tests. The installer's own bare `npx … init` strings now use
28
+ `@latest`.
29
+
7
30
  ## 1.0.0
8
31
 
9
32
  First publish. The canonical home of the `agent-workflow` planning methodology is now an
package/README.md CHANGED
@@ -10,9 +10,10 @@ This is the **methodology engine** of the `agent-workflow` family — the canoni
10
10
  rest of the family builds on. It is **content**, not a runtime: it reads nothing, writes
11
11
  nothing, and mutates no project file. It deliberately **knows nobody else** in the family —
12
12
  it only *provides* the methodology; the family composition root (`agent-workflow-kit`) is the
13
- one that injects it. The kit currently consumes a **byte-identical, drift-guarded mirror** of
14
- this canon bundled inside the kit; the live `kit engine` read (and retiring that mirror)
15
- lands in the next slice.
13
+ one that injects it. The kit reads **this canon live from the installed engine** and injects the
14
+ bounded slot from it **one source of truth, no bundled mirror** (Plan 3D / AD-016).
15
+ `npx @sabaiway/agent-workflow-kit@latest init` installs the engine as a core part of the kit; if a
16
+ slot fill is needed but the engine is absent, the kit's reconcile **fails loudly**.
16
17
 
17
18
  ## Install
18
19
 
package/SKILL.md CHANGED
@@ -1,19 +1,20 @@
1
1
  ---
2
2
  name: agent-workflow-engine
3
- description: Canonical home of the agent-workflow planning methodology — the Plan→Phase→Step→Substep vocabulary, plan lifecycle, queue.md series index, mandatory Cleanup phase, and the bounded methodology slot fragment. A published, installable npm package (available:true) that *provides* the methodology text; it mutates nothing. The composition root (agent-workflow-kit) currently keeps byte-identical mirror copies of this text (drift-guarded) and injects the slot from its mirror; the live kit engine read + mirror retirement land in the next slice.
3
+ description: Canonical home of the agent-workflow planning methodology — the Plan→Phase→Step→Substep vocabulary, plan lifecycle, queue.md series index, mandatory Cleanup phase, and the bounded methodology slot fragment. A published, installable npm package (available:true) that *provides* the methodology text; it mutates nothing. The composition root (agent-workflow-kit) reads this canon LIVE from the installed engine and injects the bounded slot from it one source of truth, no bundled mirror; `npx @sabaiway/agent-workflow-kit@latest init` installs the engine.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '1.0.0'
6
+ version: '1.1.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-engine
10
10
 
11
11
  The **canonical source of truth** for the `agent-workflow` planning methodology. It is a
12
12
  **published, installable npm package** (`capability.json` → `available:true`): it holds the
13
- methodology text and **provides** it, but it ships no runtime and mutates no files. The kit
14
- currently keeps **byte-identical mirror copies** of these files so the live injection + fallback
15
- keep working with no new dependency; a drift-guard test pins the mirrors to this canon. The live
16
- `kit → engine` read (and retiring the mirror) lands in the next slice.
13
+ methodology text and **provides** it, but it ships no runtime and mutates no files. The kit reads
14
+ **this canon live from the installed engine** (resolved via the family `detect.installed` pattern)
15
+ and injects the bounded slot fragment from it **one source of truth, no bundled mirror**.
16
+ `npx @sabaiway/agent-workflow-kit@latest init` installs the engine as a core part of the kit; when a
17
+ slot fill is needed but the engine is absent, the kit's reconcile **fails loudly** (never silently).
17
18
 
18
19
  ## What it provides (`provides: ["plan"]`)
19
20
 
@@ -36,9 +37,8 @@ no outbound edges.
36
37
  ## Status & roadmap
37
38
 
38
39
  The engine is **no longer a stub**: it is a published, `available:true`, installable npm package
39
- (`@sabaiway/agent-workflow-engine`). The kit currently keeps **byte-identical mirror copies** of
40
- this canon (drift-guarded), so the live injection + fallback keep working with no new dependency.
41
- The live `kit engine` read (the kit reading this canon from the installed engine instead of its
42
- bundled mirror) and **retiring that mirror** land in the next slice. See the kit's
43
- `tools/inject-methodology.mjs` for the marker contract and the slot reconciliation the kit runs on
44
- bootstrap + upgrade.
40
+ (`@sabaiway/agent-workflow-engine`). The kit reads **this canon live from the installed engine** and
41
+ injects the methodology slot from it — the old bundled mirror is **retired** (Plan 3D / AD-016):
42
+ **one source of truth**. `npx @sabaiway/agent-workflow-kit@latest init` installs the engine as a core
43
+ part of the kit. See the kit's `tools/inject-methodology.mjs` (the marker contract + slot
44
+ reconciliation) and `tools/engine-source.mjs` (the live read + fail-loud resolver).
package/bin/install.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // One-shot installer for @sabaiway/agent-workflow-engine.
3
3
  //
4
- // npx @sabaiway/agent-workflow-engine init
4
+ // npx @sabaiway/agent-workflow-engine@latest init
5
5
  //
6
6
  // Copies the methodology canon into the canonical skill home
7
7
  // (~/.claude/skills/agent-workflow-engine, override with --dir or
@@ -17,7 +17,7 @@
17
17
  // No telemetry, no phone-home. Dependency-free, Node >= 18.
18
18
 
19
19
  import { readFile, mkdir, readdir, copyFile, lstat, readlink, symlink } from 'node:fs/promises';
20
- import { existsSync, lstatSync } from 'node:fs';
20
+ import { existsSync, lstatSync, realpathSync } from 'node:fs';
21
21
  import { dirname, join, resolve, relative, sep, isAbsolute } from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
  import { homedir } from 'node:os';
@@ -37,7 +37,11 @@ const PAYLOAD = [
37
37
  'references',
38
38
  ];
39
39
 
40
- const tildify = (path) => path.replace(homedir(), '~');
40
+ // Collapse only a LEADING homedir() to "~" — anchored at the string start (boundary-checked with
41
+ // `sep`), never a mid-path occurrence. A naive `path.replace(homedir(), '~')` would rewrite the
42
+ // first match anywhere in the path (Issue-004). Exported so the regression can pin it in-process.
43
+ export const tildify = (path) =>
44
+ path === homedir() ? '~' : path.startsWith(homedir() + sep) ? `~${path.slice(homedir().length)}` : path;
41
45
 
42
46
  const readVersion = async () => {
43
47
  try {
@@ -48,6 +52,54 @@ const readVersion = async () => {
48
52
  }
49
53
  };
50
54
 
55
+ // Never-downgrade gate helpers (D3 / AD-012) — cloned from the kit installer so a bare `npx … init`
56
+ // serving an OLDER cached build cannot overwrite a NEWER installed canon. Dependency-free semver:
57
+ // parse the leading x.y.z (prerelease/build ignored). compareSemver → -1 | 0 | 1, or null when
58
+ // either side is unparseable (a legacy install predating any stamp → no gate).
59
+ const parseSemver = (str) => {
60
+ const m = typeof str === 'string' ? str.trim().match(/^(\d+)\.(\d+)\.(\d+)/) : null;
61
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
62
+ };
63
+
64
+ const compareSemver = (a, b) => {
65
+ const pa = parseSemver(a);
66
+ const pb = parseSemver(b);
67
+ if (!pa || !pb) return null;
68
+ const firstDiff = [0, 1, 2].map((i) => (pa[i] === pb[i] ? 0 : pa[i] < pb[i] ? -1 : 1)).find((c) => c !== 0);
69
+ return firstDiff ?? 0;
70
+ };
71
+
72
+ // Extract the version that is a DIRECT child of the top-level `metadata:` key — never a top-level or
73
+ // deeper-nested decoy `version:` (mirrors the manifest validator + the kit installer). Pure walk.
74
+ const metadataVersion = (frontmatter) => {
75
+ const lines = frontmatter.split(/\r?\n/);
76
+ const metaIdx = lines.findIndex((l) => /^metadata:[ \t]*$/.test(l));
77
+ if (metaIdx === -1) return null;
78
+ const after = lines.slice(metaIdx + 1);
79
+ const dedent = after.findIndex((l) => /^[^ \t]/.test(l)); // a column-0 line closes the metadata block
80
+ const block = dedent === -1 ? after : after.slice(0, dedent);
81
+ const baseIndent = block.length ? (block[0].match(/^[ \t]*/)?.[0] ?? '') : '';
82
+ const verLine = block.find((l) => l.startsWith(`${baseIndent}version:`));
83
+ return verLine?.match(/version:[ \t]*['"]?(\d+\.\d+\.\d+)['"]?/)?.[1] ?? null;
84
+ };
85
+
86
+ // The installed version is read from the target's SKILL.md frontmatter metadata.version (the engine's
87
+ // detect.installed.file). Returns the semver string, or null when ABSENT / unstamped (legacy → no
88
+ // gate). A SKILL.md that EXISTS but cannot be read is NOT swallowed as "legacy": fail closed (throw)
89
+ // so the gate can never be silently bypassed (AGENTS.md: no silent failures).
90
+ const readInstalledVersion = async (target) => {
91
+ const skill = resolve(target, 'SKILL.md');
92
+ if (!existsSync(skill)) return null; // absent → new/legacy install, nothing to compare
93
+ const text = await readFile(skill, 'utf8').catch((err) => {
94
+ throw new Error(
95
+ `[agent-workflow-engine] cannot read the installed SKILL.md (${tildify(skill)}): ${err.message}. ` +
96
+ `Refusing to install over an unreadable canon — fix permissions/contents or remove it, then re-run.`,
97
+ );
98
+ });
99
+ const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
100
+ return fm ? metadataVersion(fm[1]) : null;
101
+ };
102
+
51
103
  // lstat without following symlinks; null when the path does not exist. Using lstat (not
52
104
  // existsSync, which FOLLOWS symlinks and so reports a *dangling* symlink as absent) is what
53
105
  // makes the guard below catch a dangling destination symlink too.
@@ -64,9 +116,12 @@ const lstatNoFollow = (path) => {
64
116
  // within `root`. Walks `root` plus each existing path component down to `dest`; if the
65
117
  // root, an intermediate dir, or the leaf is a symlink (including a dangling one), a copy
66
118
  // could escape the target — STOP rather than follow it. Also refuses a dest outside `root`.
67
- const assertContainedRealPath = (root, dest) => {
119
+ export const assertContainedRealPath = (root, dest) => {
68
120
  const rel = relative(root, dest);
69
- if (rel.startsWith('..') || isAbsolute(rel)) {
121
+ // A true escape is `..` exactly or a `..`-prefixed PATH SEGMENT (`../x`) — NOT any string starting
122
+ // with the two chars "..": a legitimately-contained child literally named `..foo` has rel `..foo`,
123
+ // which the old `rel.startsWith('..')` wrongly rejected (Issue-004).
124
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
70
125
  throw new Error(`[agent-workflow-engine] refusing to write outside the target dir: ${dest}`);
71
126
  }
72
127
  if (lstatNoFollow(root)?.isSymbolicLink()) {
@@ -106,6 +161,7 @@ const parseArgs = (argv) => {
106
161
  return {
107
162
  help: argv.includes('--help') || argv.includes('-h'),
108
163
  version: argv.includes('--version') || argv.includes('-v'),
164
+ allowDowngrade: argv.includes('--allow-downgrade'),
109
165
  dir: dirFlag >= 0 ? argv[dirFlag + 1] : undefined,
110
166
  };
111
167
  };
@@ -120,13 +176,15 @@ const printHelp = (version) => {
120
176
  console.log(`agent-workflow-engine ${version}
121
177
 
122
178
  Usage:
123
- npx @sabaiway/agent-workflow-engine init [--dir <path>]
179
+ npx @sabaiway/agent-workflow-engine@latest init [--dir <path>] [--allow-downgrade]
124
180
  npx @sabaiway/agent-workflow-engine --version
125
181
  npx @sabaiway/agent-workflow-engine --help
126
182
 
127
183
  Installs/refreshes the methodology canon at ~/.claude/skills/agent-workflow-engine
128
184
  (override with --dir <path> or AGENT_WORKFLOW_ENGINE_DIR). init is additive —
129
- it never deletes your files and never writes through a symlink.
185
+ it never deletes your files and never writes through a symlink. If the installed
186
+ canon is newer than the version you ran, init refuses (no network — it compares the
187
+ version on disk) and points you at @latest; --allow-downgrade overrides that refusal.
130
188
 
131
189
  The engine is the methodology canon the family kit builds on — it is content, not a
132
190
  project deploy and not a model-invoked skill, so there is nothing to run inside an agent.
@@ -159,6 +217,26 @@ const main = async () => {
159
217
  console.error(`[agent-workflow-engine] target dir is a symlink — refusing to write through it: ${tildify(target)}`);
160
218
  process.exit(1);
161
219
  }
220
+
221
+ // Stale-cache defenses (no network — version already on disk vs this runner). Read BEFORE any write
222
+ // so a refusal touches nothing. cmp is null on a legacy/unparseable install → no gate.
223
+ const installedVersion = wasPresent ? await readInstalledVersion(target) : null;
224
+ const cmp = installedVersion ? compareSemver(installedVersion, version) : null;
225
+
226
+ // Never-downgrade gate (D3 / AD-012): a bare `npx … init` can run an OLDER cached build, which
227
+ // would overwrite a NEWER installed canon with old text. Refuse loudly (nonzero) unless
228
+ // --allow-downgrade — surfacing the cache trap instead of silently regressing (no silent failures).
229
+ if (cmp === 1 && !args.allowDowngrade) {
230
+ console.error(
231
+ `[agent-workflow-engine] refusing to downgrade: the installed canon is v${installedVersion}, but ` +
232
+ `this runner is the OLDER v${version}.\n` +
233
+ ` This is the classic npx cache serving a stale build. To get the newest canon, bypass the cache:\n` +
234
+ ` npx @sabaiway/agent-workflow-engine@latest init\n` +
235
+ ` (or pass --allow-downgrade to overwrite the newer install with v${version} anyway).`,
236
+ );
237
+ process.exit(1);
238
+ }
239
+
162
240
  await mkdir(target, { recursive: true });
163
241
  await Promise.all(
164
242
  PAYLOAD.map((entry) => copyRecursive(resolve(PKG_ROOT, entry), resolve(target, entry), target)),
@@ -167,13 +245,38 @@ const main = async () => {
167
245
  `[agent-workflow-engine] ${wasPresent ? 'updated the canon to' : 'installed'} v${version} -> ${tildify(target)}`,
168
246
  );
169
247
 
248
+ // No-op re-run (same version) → almost always npx served a cached build; say so + point at @latest.
249
+ if (cmp === 0) {
250
+ console.log(
251
+ `[agent-workflow-engine] note: no version change — the canon was already v${version}. If you ` +
252
+ `expected an update, npx likely served a cached build; re-run bypassing the cache:\n` +
253
+ ` npx @sabaiway/agent-workflow-engine@latest init`,
254
+ );
255
+ }
256
+
170
257
  console.log(`
171
258
  This installed the methodology canon at ${tildify(target)}. The engine is content the
172
259
  family kit builds on — there is nothing to run inside an agent.
173
260
  To update it later, re-run: npx @sabaiway/agent-workflow-engine@latest init`);
174
261
  };
175
262
 
176
- main().catch((err) => {
177
- console.error(err);
178
- process.exit(1);
179
- });
263
+ // Run main() only when executed directly (npx / node bin/install.mjs), never on import — so tests can
264
+ // import this module to unit-test its exported helpers with no side effects. Compare by REAL path:
265
+ // npx invokes the bin through a node_modules/.bin symlink, so process.argv[1] is that symlink while
266
+ // import.meta.url is the resolved real file — a raw string compare reads them as different and main()
267
+ // never runs. realpathSync collapses the symlink so both sides match. (Same idiom as the kit.)
268
+ const isDirectRun = (() => {
269
+ const invoked = process.argv[1];
270
+ if (!invoked) return false;
271
+ try {
272
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
273
+ } catch {
274
+ return false;
275
+ }
276
+ })();
277
+ if (isDirectRun) {
278
+ main().catch((err) => {
279
+ console.error(err);
280
+ process.exit(1);
281
+ });
282
+ }
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-engine",
5
5
  "kind": "methodology-engine",
6
- "version": "1.0.0",
6
+ "version": "1.1.0",
7
7
  "available": true,
8
8
  "provides": ["plan"],
9
9
  "roles": {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-engine",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Canonical home of the agent-workflow planning methodology — the Plan→Phase→Step vocabulary, plan lifecycle, queue.md series index, and mandatory Cleanup phase, consumed by the kit (composition root). The methodology engine of the agent-workflow family.",
5
5
  "keywords": [
6
6
  "ai-agents",