@sabaiway/agent-workflow-kit 3.5.0 → 3.6.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,38 @@ 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
+ ## 3.6.0 — the dependency-free install posture: a proof, never a default (AD-067)
8
+
9
+ On a provably dependency-free project the provision record and the default-lane report no longer
10
+ print a generic `npm install` hint: both state
11
+ `no install needed — the project declares no dependencies` (recorded node_modules mode
12
+ `no-dependencies`), and a `doc-parity` binding pins the posture string to the mode doc.
13
+
14
+ The verdict is a PROOF granted only on evidence the tool actually read — the WORKTREE'S OWN LIVE
15
+ checkout: what an install run there would actually read, exactly HEAD at provision time and the
16
+ satellite's own committed state on `--resume`, never MAIN's mutable working tree (which can
17
+ diverge from what the satellite actually holds; real-git tests pin both refresh directions).
18
+ Everything the tool cannot vouch for leaves the posture UNKNOWN with the honest advice kept — the
19
+ fail-safe direction, because a false "nothing to install" is worse than a redundant hint:
20
+
21
+ - a `workspaces` field of ANY shape, outright — a workspace install materializes member links and
22
+ `.bin` shims even with zero dependencies, so a workspace tree is never provably install-free;
23
+ - an external workspace manifest beside the root (`pnpm-workspace.yaml`/`.yml`, `lerna.json`);
24
+ - a malformed manifest, dependency field, or `scripts` shape (three-valued verdicts — malformed is
25
+ `unknown`, never "none"; a lifecycle key with a non-string value fails closed);
26
+ - an install-lifecycle script — dependency-free is NOT install-free: the CLOSED, test-pinned set
27
+ is the npm lifecycle (including the deprecated `prepublish`) plus `pnpm:devPreinstall`; a
28
+ native-addon manifest (`binding.gyp`) is a mandatory install too.
29
+
30
+ The posture COMPOSES with the shipped record contract, and LIVE STATE WINS: a node already at the
31
+ worktree — a directory, or a symlink left by an earlier provision, even dangling — records
32
+ `present` before any MAIN-state early return, the AD-065 symlink unlink-first arm is untouched for
33
+ every non-proven project, and `--install` remains an explicit request that is always answered with
34
+ the isolated-install command while the record still states the posture.
35
+
36
+ Third safe slice of the deferred parallel-track work (AD-063); node_modules ownership and
37
+ resume-verify semantics stay separate redesigns.
38
+
7
39
  ## 3.5.0 — the provision record orients a fresh satellite session (AD-065)
8
40
 
9
41
  The worktrees provision record was an identity stub; a fresh satellite session could not derive
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: '3.5.0'
6
+ version: '3.6.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "3.5.0",
6
+ "version": "3.6.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
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",
@@ -70,7 +70,7 @@ PLUS the three facts a fresh satellite session cannot derive from its own checko
70
70
  isolated-install command when the package manager is unambiguous, the honest install-by-hand
71
71
  advice when it is not, and — when the provisioned `node_modules` is a SYMLINK into main — the
72
72
  unlink-first form, because a plain install through the symlink writes into MAIN and is never
73
- presented as isolated. `--install` remains an EXPLICIT request and is always answered with the
73
+ presented as isolated. When the WORKTREE'S OWN LIVE CHECKOUT is provably dependency-free (its `package.json` declares no dependencies, no `workspaces` field of any shape, no install-lifecycle script, no native-addon manifest, no external workspace manifest beside it — the evidence is what an install run in the satellite would actually read: exactly HEAD at provision time, the satellite's own committed state on `--resume`, never MAIN's mutable working tree) the record and the default-lane report both state `no install needed — the project declares no dependencies` and print no install command. A workspace tree is NEVER provably install-free — a workspace install materializes member links and `.bin` shims even with zero dependencies — and anything else the tool cannot enumerate (an absent or unparseable `package.json`, a malformed dependency or scripts field, an install-lifecycle script — dependency-free is not install-free) leaves the posture UNKNOWN and keeps the existing advice: a false "nothing to install" is worse than a redundant hint. `--install` remains an EXPLICIT request and is always answered with the
74
74
  isolated-install command.
75
75
 
76
76
  **Honesty:** there is NO preview step on the writers — over-warned by design. The tool never
@@ -34,7 +34,7 @@ import {
34
34
  } from './recommendations.mjs';
35
35
  import { SKIPPED_READONLY } from './setup-backends.mjs';
36
36
  import { LATENT_ARM_NOTICE } from './review-state.mjs';
37
- import { QUEUE_SHARED_RULE, LANDING_FROM_MAIN } from './worktrees.mjs';
37
+ import { QUEUE_SHARED_RULE, LANDING_FROM_MAIN, NO_DEPENDENCIES_POSTURE } from './worktrees.mjs';
38
38
 
39
39
  const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
40
40
 
@@ -99,10 +99,12 @@ export const BINDINGS = Object.freeze([
99
99
  // actually emits — a reworded doc dropping the notice fails this pin plus the gate.
100
100
  valueBinding('latent-arm-notice', LATENT_ARM_NOTICE, LATENT_ARM_NOTICE, [REVIEW_STATE_DOC]),
101
101
  // The provision-record orientation contract (same "the tool knows and does not say" class): the
102
- // shared-queue rule and the landing-from-main fact were prose-only bars a doc could silently
103
- // drop, so both are pinned to the live strings the record actually carries.
102
+ // shared-queue rule, the landing-from-main fact, and the no-dependencies install posture were
103
+ // prose-only bars a doc could silently drop, so all are pinned to the live strings the record
104
+ // actually carries.
104
105
  valueBinding('queue-shared-rule', QUEUE_SHARED_RULE, QUEUE_SHARED_RULE, [WORKTREES_DOC]),
105
106
  valueBinding('landing-from-main', LANDING_FROM_MAIN, LANDING_FROM_MAIN, [WORKTREES_DOC]),
107
+ valueBinding('no-dependencies-posture', NO_DEPENDENCIES_POSTURE, NO_DEPENDENCIES_POSTURE, [WORKTREES_DOC]),
106
108
  ].map((b) => Object.freeze(b)));
107
109
 
108
110
  // ── the pure checker (readText is injectable for hermetic tests) ────────────────────────
@@ -150,8 +152,8 @@ A CLOSED, exported registry binds each live code constant — the autonomy-docto
150
152
  table, the status tokens, the trusted-dir allowlist), the recommendations/upgrade presentation
151
153
  contract (section header, empty line, verdict templates), the acks-store path, the setup refresh
152
154
  degrade token, the review-state clean-tree latent-arm notice, and the worktrees provision-record
153
- orientation contract (shared-queue rule, landing-from-main) — to the exact token its
154
- references/modes/*.md contract must carry, and
155
+ orientation contract (shared-queue rule, landing-from-main, no-dependencies install posture) — to
156
+ the exact token its references/modes/*.md contract must carry, and
155
157
  asserts the CURRENT value renders into every bound file. A drifted doc, an unreadable bound file,
156
158
  or an absent token FAILS CLOSED.
157
159
 
@@ -933,6 +933,10 @@ export const QUEUE_BASENAME = 'queue.md';
933
933
  export const QUEUE_SHARED_RULE =
934
934
  'the series index is SHARED and lives ONLY in main: read it at the absolute path above, and never copy it into this worktree, because docs/plans is git-ignored and machine-local, so a copy silently diverges from what main and every other worktree are writing. This worktree never WRITES that file: reaching outside it is an fs_outside_repo action the autonomy policy denies by default. Put new findings in THIS handoff record instead — it is the channel that survives the landing, and main appends them to the index from here';
935
935
  export const LANDING_FROM_MAIN = 'landing runs FROM MAIN, never from this worktree';
936
+ export const NO_DEPENDENCIES_POSTURE = 'no install needed — the project declares no dependencies';
937
+ // The recorded node_modules mode for that same verdict: provision neither advised nor created a
938
+ // node_modules for this worktree.
939
+ export const NODE_MODULES_NONE = 'no-dependencies';
936
940
 
937
941
  // The record is LINE-oriented and is parsed back for IDENTITY, so a value carrying a control byte
938
942
  // is refused rather than written: a newline spills a second line the parser reads as a real field
@@ -1250,21 +1254,110 @@ const resolveInstallAdvice = ({ root, wtRoot, fs }) => {
1250
1254
  return { command, instruction: command };
1251
1255
  };
1252
1256
 
1257
+ // Exported and frozen so the closed sets are test-pinned in BOTH directions (a member can neither
1258
+ // be dropped nor smuggled in without failing the pin).
1259
+ export const DEPENDENCY_FIELDS = Object.freeze(['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']);
1260
+ // Workspace declarations this tool cannot read. Their presence makes the dependency inventory
1261
+ // unknowable from package.json alone, so it is never proof.
1262
+ export const EXTERNAL_WORKSPACE_MANIFESTS = Object.freeze(['pnpm-workspace.yaml', 'pnpm-workspace.yml', 'lerna.json']);
1263
+
1264
+ const readPackageJson = (fs, path) => {
1265
+ const file = readFileNoFollow(fs, path);
1266
+ if (!file.bytes) return null;
1267
+ try {
1268
+ const parsed = JSON.parse(String(file.bytes));
1269
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
1270
+ } catch {
1271
+ return null;
1272
+ }
1273
+ };
1274
+
1275
+ // A dependency field is read THREE ways, never two: 'has' (a non-empty plain object), 'none' (the
1276
+ // field is absent, or an empty plain object), and 'unknown' for every other shape — a string, an
1277
+ // array, null. A malformed manifest is evidence of NOTHING, so it must never read as "none".
1278
+ const dependencyFieldsVerdict = (pkg) => {
1279
+ let verdict = 'none';
1280
+ for (const field of DEPENDENCY_FIELDS) {
1281
+ if (!Object.hasOwn(pkg, field)) continue;
1282
+ const value = pkg[field];
1283
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return 'unknown';
1284
+ if (Object.keys(value).length > 0) verdict = 'has';
1285
+ }
1286
+ return verdict;
1287
+ };
1288
+
1289
+ // A project with NO declared dependencies can still REQUIRE an install: a package-manager install
1290
+ // runs the WHOLE install-lifecycle set — the npm set (including the deprecated `prepublish`, which
1291
+ // historically fired on install) plus pnpm's `pnpm:devPreinstall` — so a manifest declaring any of
1292
+ // them, ROOT or member, needs an install even with no dependencies. dependency-free is NOT
1293
+ // install-free. The list is CLOSED and stated: a hook from a manager this tool has never heard of is
1294
+ // the honest residual of a closed-world check, the same shape as EXTERNAL_WORKSPACE_MANIFESTS.
1295
+ export const INSTALL_LIFECYCLE_SCRIPTS = Object.freeze(['preinstall', 'install', 'postinstall', 'prepare', 'preprepare', 'postprepare', 'prepublish', 'pnpm:devPreinstall']);
1296
+ // Fail-closed like the dependency check: a `scripts` field of the wrong SHAPE, or a lifecycle key
1297
+ // present with a NON-STRING value, is 'unknown' (never "no hook"). An empty-string value is a no-op,
1298
+ // not a trigger; 'has' only when a lifecycle key holds a non-empty command string.
1299
+ const installHookVerdict = (pkg) => {
1300
+ if (!Object.hasOwn(pkg, 'scripts')) return 'none';
1301
+ const scripts = pkg.scripts;
1302
+ if (scripts === null || typeof scripts !== 'object' || Array.isArray(scripts)) return 'unknown';
1303
+ let verdict = 'none';
1304
+ for (const name of INSTALL_LIFECYCLE_SCRIPTS) {
1305
+ if (!Object.hasOwn(scripts, name)) continue;
1306
+ if (typeof scripts[name] !== 'string') return 'unknown';
1307
+ if (scripts[name].length > 0) verdict = 'has';
1308
+ }
1309
+ return verdict;
1310
+ };
1311
+ // A native-addon manifest triggers an implicit `node-gyp rebuild` on install even with an empty
1312
+ // scripts block, so its presence — root or member — is a mandatory install, never proof of none.
1313
+ const declaresNativeBuild = (fs, dir) => lstatNoFollow(fs.lstat, join(dir, 'binding.gyp')) !== null;
1314
+
1315
+ // PROVABLY dependency-free, or nothing — read from the WORKTREE'S OWN LIVE CHECKOUT, never from
1316
+ // MAIN's mutable working tree: the evidence is what an install run in THIS worktree would actually
1317
+ // read. At provision time that is exactly HEAD; on --resume it follows the session's own edits, in
1318
+ // both directions (gained dependencies revoke the proof, shed ones grant it) — the same live lane
1319
+ // as the node_modules symlink probe. A dirty main manifest must neither grant nor revoke a verdict
1320
+ // about content it does not describe. A `workspaces` field of ANY shape
1321
+ // is UNKNOWN outright — a workspace install materializes member links and `.bin` shims even with
1322
+ // zero dependencies, so a workspace tree is never provably install-free. Everything else the tool
1323
+ // cannot enumerate — an absent/unparseable package.json, a malformed dependency or scripts field,
1324
+ // an install-lifecycle script, a native-addon manifest (binding.gyp), an external workspace
1325
+ // manifest — leaves the posture UNKNOWN, and unknown keeps the existing install advice: a false
1326
+ // "nothing to install" is worse than a redundant hint.
1327
+ const declaresNoDependencies = ({ wtRoot, fs }) => {
1328
+ const pkg = readPackageJson(fs, join(wtRoot, 'package.json'));
1329
+ if (pkg === null || dependencyFieldsVerdict(pkg) !== 'none' || installHookVerdict(pkg) !== 'none') return false;
1330
+ if (Object.hasOwn(pkg, 'workspaces')) return false;
1331
+ if (declaresNativeBuild(fs, wtRoot)) return false;
1332
+ // A workspace set can be declared OUTSIDE package.json too. The list is CLOSED and stated: an
1333
+ // exotic third manifest this tool has never heard of would still read as proof, which is the
1334
+ // honest residual of a closed-world check (the same shape as INSTALL_LIFECYCLE_SCRIPTS).
1335
+ for (const manifest of EXTERNAL_WORKSPACE_MANIFESTS) {
1336
+ if (lstatNoFollow(fs.lstat, join(wtRoot, manifest)) !== null) return false;
1337
+ }
1338
+ return true;
1339
+ };
1340
+
1253
1341
  // What the RECORD states about installing — the resolved posture for THIS worktree (the runnable
1254
1342
  // isolated-install command, or the honest by-hand advice), never a lane-dependent hint. Probed on
1255
- // the LIVE worktree after the node_modules step: a plain `cd && install` through a SYMLINKED
1256
- // node_modules writes into MAIN never presented as isolated; the symlink case records the
1257
- // unlink-first form instead (the same contract the --install lane already prints).
1258
- const resolveInstallPosture = ({ root, wtRoot, fs }) => {
1259
- const advice = resolveInstallAdvice({ root, wtRoot, fs });
1343
+ // the LIVE worktree after the node_modules step, and LIVE STATE WINS: a symlinked node_modules
1344
+ // records the unlink-first form even on a dependency-free checkout (`--resume` can meet a symlink
1345
+ // an earlier provision left an install through it writes into MAIN, and the posture must never
1346
+ // hide that). Only then may a PROVEN dependency-free checkout short-circuit: a verdict of
1347
+ // "nothing to install" must not ride an install instruction.
1348
+ const resolveInstallPosture = ({ root, wtRoot, dependencyFree, fs }) => {
1260
1349
  const nmPath = join(wtRoot, 'node_modules');
1261
1350
  const nm = lstatNoFollow(fs.lstat, nmPath);
1262
- if (nm === null || !nm.isSymbolicLink()) return advice.instruction;
1263
- const separator = advice.command === null ? ' — ' : ' && ';
1264
- return `the provisioned node_modules is a symlink into MAIN (an install through it writes into MAIN) for isolation remove it first: rm ${shellQuoteArg(nmPath)}${separator}${advice.instruction}`;
1351
+ if (nm !== null && nm.isSymbolicLink()) {
1352
+ const advice = resolveInstallAdvice({ root, wtRoot, fs });
1353
+ const separator = advice.command === null ? '' : ' && ';
1354
+ return `the provisioned node_modules is a symlink into MAIN (an install through it writes into MAIN) — for isolation remove it first: rm ${shellQuoteArg(nmPath)}${separator}${advice.instruction}`;
1355
+ }
1356
+ if (dependencyFree) return NO_DEPENDENCIES_POSTURE;
1357
+ return resolveInstallAdvice({ root, wtRoot, fs }).instruction;
1265
1358
  };
1266
1359
 
1267
- const provisionNodeModules = ({ root, rootReal, wtRoot, installFlag, git, fs, report }) => {
1360
+ const provisionNodeModules = ({ root, rootReal, wtRoot, installFlag, dependencyFree, git, fs, report }) => {
1268
1361
  const install = resolveInstallAdvice({ root, wtRoot, fs });
1269
1362
  if (installFlag) {
1270
1363
  const dst = join(wtRoot, 'node_modules');
@@ -1280,6 +1373,22 @@ const provisionNodeModules = ({ root, rootReal, wtRoot, installFlag, git, fs, re
1280
1373
  : ` node_modules: install it yourself (zero spawn): ${install.instruction}`);
1281
1374
  return 'install-printed';
1282
1375
  }
1376
+ // LIVE STATE WINS the whole default lane: a node already at the worktree — a directory, or a
1377
+ // symlink an earlier provision left, even dangling — is what the record states; reporting
1378
+ // MAIN's state (`absent`) beside an existing node would contradict record.install.
1379
+ const dst = join(wtRoot, 'node_modules');
1380
+ if (lstatNoFollow(fs.lstat, dst) !== null) {
1381
+ report.push(' node_modules: already present in the worktree');
1382
+ return 'present';
1383
+ }
1384
+ // Only then may a PROVEN dependency-free checkout short-circuit the rest. Composing the posture
1385
+ // into one of the arms below instead produced a self-contradicting line ("after your own
1386
+ // install there, re-run --resume, or: no install needed"). `--install` is untouched above —
1387
+ // that request was explicit.
1388
+ if (dependencyFree) {
1389
+ report.push(` node_modules: ${NO_DEPENDENCIES_POSTURE}`);
1390
+ return NODE_MODULES_NONE;
1391
+ }
1283
1392
  const mainNm = join(root, 'node_modules');
1284
1393
  const node = classifyNodeNoFollow(mainNm, fs);
1285
1394
  if (node.kind === 'absent') {
@@ -1313,11 +1422,6 @@ const provisionNodeModules = ({ root, rootReal, wtRoot, installFlag, git, fs, re
1313
1422
  report.push(` node_modules: main's resolves outside the repo — not symlinked; ${install.instruction}`);
1314
1423
  return 'outside-repo';
1315
1424
  }
1316
- const dst = join(wtRoot, 'node_modules');
1317
- if (lstatNoFollow(fs.lstat, dst) !== null) {
1318
- report.push(' node_modules: already present in the worktree');
1319
- return 'present';
1320
- }
1321
1425
  guardDst(fs, wtRoot, dst);
1322
1426
  try {
1323
1427
  fs.symlink(mainNm, dst);
@@ -1544,7 +1648,10 @@ const finishProvision = ({ root, rootReal, targetPath, slug, branch, flags, seed
1544
1648
 
1545
1649
  writeSeedPlan({ wtRoot: targetPath, srcAbs: seed.srcAbs, name: seed.name, fs, report });
1546
1650
  const includesRecorded = provisionIncludes({ rootReal, wtRoot: targetPath, includeSources, git, fs, report, copied });
1547
- const nodeModulesMode = provisionNodeModules({ root, rootReal, wtRoot: targetPath, installFlag: flags.install, git, fs, report });
1651
+ // Computed ONCE, from the satellite's own checkout, and threaded to both consumers — the report
1652
+ // lane and the record must state the SAME verdict.
1653
+ const dependencyFree = declaresNoDependencies({ wtRoot: targetPath, fs });
1654
+ const nodeModulesMode = provisionNodeModules({ root, rootReal, wtRoot: targetPath, installFlag: flags.install, dependencyFree, git, fs, report });
1548
1655
  const vscodeMode = provisionVscode({ root, wtRoot: targetPath, slug, git, fs, report });
1549
1656
 
1550
1657
  rebasePins({ root, wtRoot: targetPath, git, fs, report });
@@ -1559,7 +1666,7 @@ const finishProvision = ({ root, rootReal, targetPath, slug, branch, flags, seed
1559
1666
  includes: includesRecorded,
1560
1667
  nodeModules: nodeModulesMode,
1561
1668
  vscode: vscodeMode,
1562
- install: resolveInstallPosture({ root, wtRoot: targetPath, fs }),
1669
+ install: resolveInstallPosture({ root, wtRoot: targetPath, dependencyFree, fs }),
1563
1670
  ...orientationFields({ root, slug }),
1564
1671
  },
1565
1672
  fs,