@sabaiway/agent-workflow-kit 5.11.0 → 5.11.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/SKILL.md +1 -1
  3. package/capability.json +1 -1
  4. package/package.json +1 -1
  5. package/references/hooks/gate-approve.mjs +13 -2
  6. package/references/hooks/state-block-guard.mjs +14 -2
  7. package/references/scripts/archive-changelog.mjs +14 -3
  8. package/references/scripts/archive-decisions.mjs +14 -3
  9. package/references/scripts/archive-issues.mjs +14 -3
  10. package/references/scripts/check-docs-size.mjs +14 -3
  11. package/references/scripts/migrate-gates.mjs +13 -2
  12. package/tools/ack-write.mjs +3 -3
  13. package/tools/autonomy-doctor.mjs +2 -3
  14. package/tools/bridge-settings.mjs +2 -3
  15. package/tools/cheap-agents.mjs +3 -3
  16. package/tools/commands.mjs +2 -3
  17. package/tools/commit-guard.mjs +3 -3
  18. package/tools/core-evidence.mjs +2 -3
  19. package/tools/coverage-check.mjs +2 -3
  20. package/tools/delegation.mjs +2 -3
  21. package/tools/detect-backends.mjs +2 -3
  22. package/tools/dispatch-record.mjs +1 -1
  23. package/tools/doc-parity.mjs +2 -3
  24. package/tools/family-registry.mjs +3 -3
  25. package/tools/flow-adoption-mint.mjs +70 -0
  26. package/tools/flow-append.mjs +309 -0
  27. package/tools/flow-chain-state.mjs +91 -0
  28. package/tools/flow-check.mjs +2 -3
  29. package/tools/flow-delta-proof.mjs +307 -0
  30. package/tools/flow-finding-manifest.mjs +70 -0
  31. package/tools/flow-legality.mjs +248 -0
  32. package/tools/flow-record-identity.mjs +115 -0
  33. package/tools/flow-record-shape.mjs +283 -0
  34. package/tools/flow-record.mjs +49 -789
  35. package/tools/flow-store-read.mjs +3 -3
  36. package/tools/flow-store.mjs +35 -812
  37. package/tools/flow-subset-budget.mjs +81 -0
  38. package/tools/flow-vocabulary.mjs +96 -0
  39. package/tools/flow-writer.mjs +3 -3
  40. package/tools/gate-hook.mjs +3 -3
  41. package/tools/gates-init.mjs +3 -3
  42. package/tools/grounding.mjs +2 -3
  43. package/tools/hide-footprint.mjs +2 -3
  44. package/tools/inject-methodology.mjs +2 -3
  45. package/tools/lens-region.mjs +2 -3
  46. package/tools/manifest/validate.mjs +2 -3
  47. package/tools/migrate-adr-store.mjs +3 -3
  48. package/tools/path-inventory.mjs +2 -3
  49. package/tools/procedures.mjs +3 -3
  50. package/tools/receipt-deadline.mjs +2 -3
  51. package/tools/recipes.mjs +2 -3
  52. package/tools/recommendations.mjs +3 -3
  53. package/tools/release-scan.mjs +2 -3
  54. package/tools/repo-search.mjs +2 -3
  55. package/tools/review-state.mjs +3 -3
  56. package/tools/run-gates.mjs +2 -3
  57. package/tools/sandbox-masks.mjs +3 -3
  58. package/tools/set-autonomy.mjs +2 -3
  59. package/tools/set-flow.mjs +3 -3
  60. package/tools/set-recipe.mjs +2 -3
  61. package/tools/setup-backends.mjs +3 -3
  62. package/tools/store-append.mjs +2 -2
  63. package/tools/uninstall.mjs +2 -3
  64. package/tools/velocity-profile.mjs +3 -3
  65. package/tools/worktrees.mjs +3 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,117 @@ 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
+ ## 5.11.2 — the record vocabulary becomes a facade over five leaves (AD-103)
8
+
9
+ **Nothing you import changes, and that is the whole claim — now certified rather than asserted.**
10
+ `tools/flow-record.mjs` was the kit's most expensive module to read: 795 lines holding eleven of its
11
+ own section seams, from the closed record vocabulary through the field shapes, the identity family,
12
+ the two legality walks and the wrapper finding manifest. It keeps its path and its 29 export names,
13
+ and becomes a 55-line re-export facade over five single-responsibility leaves.
14
+
15
+ - **`tools/flow-record.mjs` keeps its path, its 29 export names and every one of its 30 import
16
+ sites.** The internals moved verbatim into `flow-vocabulary.mjs` (96, the lowest leaf: the closed
17
+ kinds, purposes, terminal lanes, seed assignment and transition table) · `flow-record-shape.mjs`
18
+ (283, the closed per-kind field shapes and `validateFlowRecord`) · `flow-record-identity.mjs` (115,
19
+ every answer to "what identifies this record or this set" — keys, tree identity, the canonical
20
+ digest, the owner-scoped projection) · `flow-legality.mjs` (248, the two raw-order legality walks)
21
+ · `flow-finding-manifest.mjs` (70). The edges run one way and the family is acyclic by
22
+ construction; no leaf reaches a write module, so "pure form" is a structural pin instead of a
23
+ header sentence.
24
+ - **Conservation is certified by ORDER as well as by count.** A comparator normalises both sides
25
+ identically and then checks, per leaf, that its line SEQUENCE equals the concatenation of its
26
+ declared source ranges in declared order — a reordering is caught, not just a loss — AND that the
27
+ multiset over the union of the five leaves equals the pre-split module. Over the 587 executable
28
+ lines: five sequences EQUAL, the difference EMPTY in both directions. It was then run three more
29
+ times — against the unsplit module alone, with one leaf omitted, and with one line duplicated — so
30
+ a comparator that could not report loss or excess could not pass for one that does.
31
+ - **The five shared grammars live in exactly one place.** `HEX64_RE`, `isHex64`, `isPlainObject`,
32
+ `isNonEmptyString` and `refuse` are exported off-surface from the lowest leaf and used by the
33
+ others. They are the record family's named grammars — `isHex64` IS the 64-hex digest grammar every
34
+ consumer takes by reference — and a per-leaf copy is the only way two of them could ever drift.
35
+ The layout suite pins that off-surface set exactly, so it cannot widen unnoticed.
36
+ - **The owning suite never moved.** `tools/flow-record.test.mjs` is BYTE-IDENTICAL and reproduces
37
+ 58/58; the fifteen named suites show an empty `git diff --name-only` in the worktree and under
38
+ `--cached`. `test/flow-record-layout.test.mjs` (not shipped) pins the frozen 29-name surface, the
39
+ facade-declared owner of every name, that the facade carries no logic at all, the size caps, and
40
+ the one-way edges including their negative direction — four of its five cases were red-proofed on
41
+ the unsplit tree before the split existed.
42
+ - **`test/package-content.test.mjs` moves its payload pin from 227 to 232 files** and names the five
43
+ leaves: the facade imports all five, so a leaf falling out of the published tarball would break
44
+ every flow read at load, and the pin is what says so.
45
+ - **One consumer line changed in the whole tree, and it is a comment.** `dispatch-record.mjs:15`
46
+ cited the fail-closed rule by line number (`flow-record.mjs:22-24`); the split falsified that
47
+ reference, so it now names the rule semantically.
48
+
49
+ Recorded size effect, reason `tranche 3: flow-record split`:
50
+
51
+ ```text
52
+ agent-workflow-kit/test/package-content.test.mjs: lines 600 → 617 (raise)
53
+ agent-workflow-kit/tools/flow-record.mjs: lines 795 → none
54
+ agent-workflow-kit: aggregate lines 125315 → 125669 (raise)
55
+ ```
56
+
57
+ ## 5.11.1 — the flow store becomes a facade over five leaves, and the direct-run guard fix finally reaches you (AD-102)
58
+
59
+ **A tool invoked through a symlink used to run nothing and exit 0 — and five of the affected tools are
60
+ declared gates.** That fix has been sitting in-repo, unpublished, since the delegation series' own
61
+ measurement came back FAIL and the fix was correctly held back from a release it should not have
62
+ justified (AD-101). It ships here, on a regular train, because it is a bug fix and every deployed
63
+ host still carries the broken form. Beside it, the flow store stops being one 827-line module.
64
+
65
+ - **The direct-run entry guard now decides by REAL PATH, in all 66 frozen sites.** The lexical form
66
+ compared `import.meta.url` against `process.argv[1]`, which is false when the entry point is a
67
+ symlink — so the tool did nothing, said nothing, and exited 0. Every one of the 39 kit tools now
68
+ decides through the shipped `isDirectRun(import.meta.url)` leaf; the seven mirrored
69
+ `references/scripts` and `references/hooks` payload scripts, which cannot reach that leaf from a
70
+ deployed project, inline the same fail-closed realpath guard. `velocity-profile.mjs` also drops a
71
+ vestigial exported predicate with no importer. The new `test/direct-run-sweep.test.mjs` pins list
72
+ completeness, in-process import of all 39, the absence of the lexical byte pattern, and a SPAWNED
73
+ symlink smoke — observed red 3/3 before the fix, so the silent-pass class is proven dead rather than
74
+ believed dead.
75
+ - **`tools/flow-store.mjs` keeps its path, its 29 export names and every one of its 22 import sites,
76
+ and becomes a 50-line re-export facade.** The flow-specific write side moved verbatim into five
77
+ leaves with one responsibility each and one-way edges: `flow-chain-state.mjs` (91, the pure chain
78
+ walk and the prior-terminal reference validator) · `flow-subset-budget.mjs` (81, the pure
79
+ Decision-7/8 counting-context budget) · `flow-append.mjs` (309, the store's ONE write door, keeping
80
+ the locked subset-attempt factory beside the lane it is the only sanctioned entrance to) ·
81
+ `flow-adoption-mint.mjs` (70) · `flow-delta-proof.mjs` (307, the bookkeeping-delta custody proof).
82
+ Nothing about the public surface moved, so nothing you import changes.
83
+ - **The split is proven, not asserted.** A line-multiset conservation check compares the pre-split
84
+ module against the union of the five leaves and expects exactly two lines to have gained a copy —
85
+ and it was run twice more, against the unsplit module alone and with one leaf omitted, so that a
86
+ comparator which cannot report loss could not pass for one that does. The three owning suites are
87
+ BYTE-IDENTICAL and reproduce 169/169; the ten named suites show an empty `git diff --name-only`.
88
+ `test/flow-store-layout.test.mjs` (not shipped) now pins the frozen surface, that every facade name
89
+ is the SAME binding as its leaf's export, that the facade carries no logic at all, the size caps,
90
+ and the one-way import direction including its negative edges.
91
+ - **`test/package-content.test.mjs` moves its payload pin from 222 to 227 files** and names the five
92
+ leaves: the facade imports all five, so a leaf falling out of the published tarball would break
93
+ every append at load, and the pin is what says so.
94
+
95
+ Recorded size effect, reason `tranche 2: flow-store split`:
96
+
97
+ ```text
98
+ agent-workflow-kit/test/package-content.test.mjs: lines 583 -> 600 (raise)
99
+ agent-workflow-kit/tools/flow-store.mjs: lines 827 -> none
100
+ agent-workflow-kit: aggregate lines 124960 -> 125315 (raise)
101
+ ```
102
+
103
+ Recorded size effect of the direct-run sweep that ships with it (reason: a standalone deployed script
104
+ cannot reach the kit's shared direct-run leaf, so it inlines the realpath guard — one lexical line
105
+ becomes an 11-line fail-closed IIFE; the 39 converted tool guards themselves only shrink):
106
+
107
+ ```text
108
+ agent-workflow-kit/references/hooks/gate-approve.mjs: lines 559 -> 570 (raise)
109
+ agent-workflow-kit/references/hooks/state-block-guard.mjs: lines 425 -> 437 (raise)
110
+ agent-workflow-kit/references/scripts/archive-changelog.mjs: lines 546 -> 557 (raise)
111
+ agent-workflow-kit/references/scripts/archive-decisions.mjs: lines 1199 -> 1210 (raise)
112
+ agent-workflow-kit/references/scripts/archive-issues.mjs: lines 415 -> 426 (raise)
113
+ agent-workflow-kit/references/scripts/check-docs-size.mjs: lines 580 -> 591 (raise)
114
+ agent-workflow-kit/references/scripts/migrate-gates.mjs: lines 722 -> 733 (raise)
115
+ agent-workflow-kit: aggregate lines 124726 -> 124805 -> 124925 -> 124960 (raise)
116
+ ```
117
+
7
118
  ## 5.11.0 — the fan-out half: which vehicle carries a sub-task, what a satellite is told, and what its handoff brings back (AD-100)
8
119
 
9
120
  **A delegated thread could be recorded end to end, and the two questions around it still had no
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: '5.11.0'
6
+ version: '5.11.2'
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": "5.11.0",
6
+ "version": "5.11.2",
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": "5.11.0",
3
+ "version": "5.11.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",
@@ -85,7 +85,7 @@
85
85
 
86
86
  import { readFileSync, realpathSync } from 'node:fs';
87
87
  import { join } from 'node:path';
88
- import { pathToFileURL } from 'node:url';
88
+ import { fileURLToPath } from 'node:url';
89
89
 
90
90
  export const HOOK_EVENT_NAME = 'PreToolUse';
91
91
  export const BASH_TOOL_NAME = 'Bash';
@@ -555,5 +555,16 @@ export const main = async () => {
555
555
  return EXIT_OK;
556
556
  };
557
557
 
558
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
558
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
559
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
560
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
561
+ const isDirectRun = (() => {
562
+ const invoked = process.argv[1];
563
+ if (!invoked) return false;
564
+ try {
565
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
566
+ } catch {
567
+ return false;
568
+ }
569
+ })();
559
570
  if (isDirectRun) main().then((code) => process.exit(code));
@@ -37,7 +37,8 @@
37
37
  // fields (`decision`, `continue`, `stopReason`, `hookSpecificOutput`) are deliberately never emitted:
38
38
  // blocking the stop would re-enter the model on a message already sent to the reader.
39
39
 
40
- import { pathToFileURL } from 'node:url';
40
+ import { realpathSync } from 'node:fs';
41
+ import { fileURLToPath } from 'node:url';
41
42
 
42
43
  export const HOOK_EVENT_NAME = 'Stop';
43
44
  const EXIT_OK = 0;
@@ -419,7 +420,18 @@ export const main = async ({
419
420
  return EXIT_OK;
420
421
  };
421
422
 
423
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
424
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
425
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
426
+ const isDirectRun = (() => {
427
+ const invoked = process.argv[1];
428
+ if (!invoked) return false;
429
+ try {
430
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
431
+ } catch {
432
+ return false;
433
+ }
434
+ })();
422
435
  // `process.exitCode`, never `process.exit()`: an immediate exit can truncate a pending stdout write
423
436
  // to a pipe, and that single warning is the entire product of this hook.
424
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
425
437
  if (isDirectRun) main().then((code) => { process.exitCode = code; });
@@ -32,9 +32,9 @@
32
32
  // --warm-days=N (default 30)
33
33
  // --today=YYYY-MM-DD (default today UTC) — useful for tests / reproducible runs
34
34
 
35
- import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from 'node:fs';
35
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, realpathSync } from 'node:fs';
36
36
  import { dirname, resolve, basename } from 'node:path';
37
- import { fileURLToPath, pathToFileURL } from 'node:url';
37
+ import { fileURLToPath } from 'node:url';
38
38
  import { tokenizeMarkdown, findParagraphBreak, fail } from './markdown-blocks.mjs';
39
39
 
40
40
  const __filename = fileURLToPath(import.meta.url);
@@ -542,5 +542,16 @@ export const runCli = (argv, deps = {}) => {
542
542
  }
543
543
  };
544
544
 
545
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
545
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
546
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
547
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
548
+ const isDirectRun = (() => {
549
+ const invoked = process.argv[1];
550
+ if (!invoked) return false;
551
+ try {
552
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
553
+ } catch {
554
+ return false;
555
+ }
556
+ })();
546
557
  if (isDirectRun) process.exitCode = runCli(process.argv.slice(2));
@@ -75,9 +75,9 @@
75
75
  //
76
76
  // Dependency-free, Node >= 22. Deployed into a consumer's scripts/ like its siblings.
77
77
 
78
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
78
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, realpathSync } from 'node:fs';
79
79
  import { dirname, resolve, join, posix } from 'node:path';
80
- import { fileURLToPath, pathToFileURL } from 'node:url';
80
+ import { fileURLToPath } from 'node:url';
81
81
  import { spawnSync } from 'node:child_process';
82
82
  import { createHash } from 'node:crypto';
83
83
  import { tmpdir } from 'node:os';
@@ -1195,5 +1195,16 @@ export const runCli = (argv, deps = {}) => {
1195
1195
  }
1196
1196
  };
1197
1197
 
1198
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
1198
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
1199
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
1200
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
1201
+ const isDirectRun = (() => {
1202
+ const invoked = process.argv[1];
1203
+ if (!invoked) return false;
1204
+ try {
1205
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
1206
+ } catch {
1207
+ return false;
1208
+ }
1209
+ })();
1199
1210
  if (isDirectRun) process.exitCode = runCli(process.argv.slice(2));
@@ -31,9 +31,9 @@
31
31
  // --cutoff-days=N (default 14)
32
32
  // --today=YYYY-MM-DD (default UTC today)
33
33
 
34
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
34
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync } from 'node:fs';
35
35
  import { dirname, resolve, basename } from 'node:path';
36
- import { fileURLToPath, pathToFileURL } from 'node:url';
36
+ import { fileURLToPath } from 'node:url';
37
37
  import { tokenizeMarkdown, fail } from './markdown-blocks.mjs';
38
38
 
39
39
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -411,5 +411,16 @@ export const runCli = (argv, deps = {}) => {
411
411
  }
412
412
  };
413
413
 
414
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
414
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
415
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
416
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
417
+ const isDirectRun = (() => {
418
+ const invoked = process.argv[1];
419
+ if (!invoked) return false;
420
+ try {
421
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
422
+ } catch {
423
+ return false;
424
+ }
425
+ })();
415
426
  if (isDirectRun) process.exitCode = runCli(process.argv.slice(2));
@@ -25,9 +25,9 @@
25
25
  // --quiet print only failures (and final summary)
26
26
 
27
27
  import { readFile, writeFile, readdir, stat, rename, rm } from 'node:fs/promises';
28
- import { existsSync, lstatSync } from 'node:fs';
28
+ import { existsSync, lstatSync, realpathSync } from 'node:fs';
29
29
  import { dirname, resolve, relative, join, basename, sep } from 'node:path';
30
- import { fileURLToPath, pathToFileURL } from 'node:url';
30
+ import { fileURLToPath } from 'node:url';
31
31
  import { randomBytes } from 'node:crypto';
32
32
 
33
33
  const __filename = fileURLToPath(import.meta.url);
@@ -571,7 +571,18 @@ export const runCli = async (argv, deps = {}) => {
571
571
  return result(errorCount > 0 && !flags.report ? 1 : 0);
572
572
  };
573
573
 
574
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
574
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
575
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
576
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
577
+ const isDirectRun = (() => {
578
+ const invoked = process.argv[1];
579
+ if (!invoked) return false;
580
+ try {
581
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
582
+ } catch {
583
+ return false;
584
+ }
585
+ })();
575
586
  if (isDirectRun) {
576
587
  const { code, stdout, stderr } = await runCli(process.argv.slice(2));
577
588
  if (stdout) process.stdout.write(stdout);
@@ -28,7 +28,7 @@
28
28
 
29
29
  import { existsSync, lstatSync, readFileSync, writeFileSync, renameSync, unlinkSync, realpathSync } from 'node:fs';
30
30
  import { join, resolve, isAbsolute } from 'node:path';
31
- import { pathToFileURL, fileURLToPath } from 'node:url';
31
+ import { fileURLToPath } from 'node:url';
32
32
  import { randomBytes } from 'node:crypto';
33
33
  import { spawnSync } from 'node:child_process';
34
34
 
@@ -718,5 +718,16 @@ export const main = (argv = process.argv.slice(2), io = {}) => {
718
718
  }
719
719
  };
720
720
 
721
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
721
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
722
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
723
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
724
+ const isDirectRun = (() => {
725
+ const invoked = process.argv[1];
726
+ if (!invoked) return false;
727
+ try {
728
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
729
+ } catch {
730
+ return false;
731
+ }
732
+ })();
722
733
  if (isDirectRun) process.exitCode = main();
@@ -27,7 +27,8 @@
27
27
 
28
28
  import { lstatSync, readFileSync } from 'node:fs';
29
29
  import { dirname, join, resolve } from 'node:path';
30
- import { fileURLToPath, pathToFileURL } from 'node:url';
30
+ import { fileURLToPath } from 'node:url';
31
+ import { isDirectRun } from './direct-run.mjs';
31
32
  import { ACKS_FILE, ACK_LANES } from './recommendations.mjs';
32
33
  import { assertDocsAiDeployment, writeDocsAiFileAtomic, lstatNoFollow } from './atomic-write.mjs';
33
34
  import { shellQuoteArg } from './review-state.mjs';
@@ -207,5 +208,4 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
207
208
  }
208
209
  };
209
210
 
210
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
211
- if (isDirectRun) process.exit(main(process.argv.slice(2)));
211
+ if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));
@@ -40,7 +40,7 @@
40
40
  import { spawnSync } from 'node:child_process';
41
41
  import { closeSync, lstatSync, openSync } from 'node:fs';
42
42
  import { join } from 'node:path';
43
- import { pathToFileURL } from 'node:url';
43
+ import { isDirectRun } from './direct-run.mjs';
44
44
  import { isExecutableFile, probeSandboxAvailability } from './velocity-profile.mjs';
45
45
  import { assertDocsAiDeployment } from './atomic-write.mjs';
46
46
 
@@ -484,5 +484,4 @@ export const main = (argv, deps = {}) => {
484
484
  return finish(applied.status, applied.finalPlan);
485
485
  };
486
486
 
487
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
488
- if (isDirectRun) process.exit(main(process.argv.slice(2)));
487
+ if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));
@@ -23,7 +23,7 @@
23
23
  //
24
24
  // Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
25
25
 
26
- import { pathToFileURL } from 'node:url';
26
+ import { isDirectRun } from './direct-run.mjs';
27
27
  import { settingValueValid } from './manifest/validate.mjs';
28
28
  import { writeHostConfigFileAtomic } from './atomic-write.mjs';
29
29
  import {
@@ -294,8 +294,7 @@ export const main = (argv = [], ctx = {}) => {
294
294
  }
295
295
  };
296
296
 
297
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
298
- if (isDirectRun) {
297
+ if (isDirectRun(import.meta.url)) {
299
298
  const r = main(process.argv.slice(2));
300
299
  if (r.stdout) console.log(r.stdout);
301
300
  if (r.stderr) console.error(r.stderr);
@@ -27,7 +27,8 @@
27
27
 
28
28
  import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
29
29
  import { join, resolve, dirname } from 'node:path';
30
- import { fileURLToPath, pathToFileURL } from 'node:url';
30
+ import { fileURLToPath } from 'node:url';
31
+ import { isDirectRun } from './direct-run.mjs';
31
32
  import { shellQuoteArg } from './repo-lex.mjs';
32
33
 
33
34
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -251,5 +252,4 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
251
252
  }
252
253
  };
253
254
 
254
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
255
- if (isDirectRun) process.exit(main(process.argv.slice(2)));
255
+ if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));
@@ -19,7 +19,7 @@
19
19
  // the `### Mode:` headers in SKILL.md, so the catalog cannot silently drift from the documented modes.
20
20
  // Pure, dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
21
21
 
22
- import { pathToFileURL } from 'node:url';
22
+ import { isDirectRun } from './direct-run.mjs';
23
23
 
24
24
  const SKILL_NAME = 'agent-workflow-kit';
25
25
  const BARE_INVOCATION = `/${SKILL_NAME}`;
@@ -380,7 +380,6 @@ const main = (argv) => {
380
380
  console.log(formatHelp());
381
381
  };
382
382
 
383
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
384
- if (isDirectRun) main(process.argv.slice(2));
383
+ if (isDirectRun(import.meta.url)) main(process.argv.slice(2));
385
384
 
386
385
  export { KINDS, SKILL_NAME, BARE_INVOCATION };
@@ -42,9 +42,10 @@
42
42
 
43
43
  import { readFileSync, lstatSync } from 'node:fs';
44
44
  import { resolve } from 'node:path';
45
- import { pathToFileURL, fileURLToPath } from 'node:url';
45
+ import { fileURLToPath } from 'node:url';
46
46
  import { spawnSync } from 'node:child_process';
47
47
  import { createHash } from 'node:crypto';
48
+ import { isDirectRun } from './direct-run.mjs';
48
49
  import { computeTreeFingerprint, buildState, decideCheck, quoteReportName, shellQuoteArg } from './review-state.mjs';
49
50
  import {
50
51
  resolveEvidencePath, readEvidence, authoritativeOfKind, canonicalKindSerialization,
@@ -381,8 +382,7 @@ export const main = (argv, ctx = {}) => {
381
382
  }
382
383
  };
383
384
 
384
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
385
- if (isDirectRun) {
385
+ if (isDirectRun(import.meta.url)) {
386
386
  const r = main(process.argv.slice(2));
387
387
  if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
388
388
  if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
@@ -36,11 +36,11 @@
36
36
 
37
37
  import { readFileSync, lstatSync, realpathSync, readlinkSync, openSync, readSync, closeSync } from 'node:fs';
38
38
  import { join, dirname, normalize, sep, basename } from 'node:path';
39
- import { pathToFileURL } from 'node:url';
40
39
  import { spawnSync } from 'node:child_process';
41
40
  import { createHash } from 'node:crypto';
42
41
  import { writeContainedFileAtomic } from './atomic-write.mjs';
43
42
  import { parsePositiveIntKnob, probeVerdict } from './changed-surface.mjs';
43
+ import { isDirectRun } from './direct-run.mjs';
44
44
  import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
45
45
  import { lexicalRepoRelative } from './repo-lex.mjs';
46
46
  // The coverage vocabulary leaf: run-gates RECORDS the token this validator checks, and run-gates
@@ -1230,8 +1230,7 @@ export const main = (argv, ctx = {}) => {
1230
1230
  }
1231
1231
  };
1232
1232
 
1233
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
1234
- if (isDirectRun) {
1233
+ if (isDirectRun(import.meta.url)) {
1235
1234
  const r = main(process.argv.slice(2));
1236
1235
  if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
1237
1236
  if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
@@ -23,9 +23,9 @@
23
23
  import { lstatSync, readFileSync, realpathSync } from 'node:fs';
24
24
  import { createHash } from 'node:crypto';
25
25
  import { join } from 'node:path';
26
- import { pathToFileURL } from 'node:url';
27
26
  import { spawnSync } from 'node:child_process';
28
27
  import { computeChangedSurface } from './changed-surface.mjs';
28
+ import { isDirectRun } from './direct-run.mjs';
29
29
  import { lcovCoveredMap, uncoveredChangedFromLcov } from './lcov.mjs';
30
30
  import {
31
31
  computeTreeFingerprint,
@@ -365,8 +365,7 @@ export const main = (argv, ctx = {}) => {
365
365
  }
366
366
  };
367
367
 
368
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
369
- if (isDirectRun) {
368
+ if (isDirectRun(import.meta.url)) {
370
369
  // The capability is CONSUMED here: snapshot it, then remove it from this process's environment
371
370
  // before anything spawns. Every `git` query and every bound-test probe below inherits
372
371
  // process.env, so leaving it in place would hand a live attestation context to each of them —
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { statSync } from 'node:fs';
12
12
  import { join, resolve } from 'node:path';
13
- import { pathToFileURL } from 'node:url';
13
+ import { isDirectRun } from './direct-run.mjs';
14
14
  import { validateManifest, VALID } from './manifest/validate.mjs';
15
15
 
16
16
  // The exact skill name a delegable memory candidate must declare (guards against a wrong-name
@@ -120,5 +120,4 @@ const main = (argv) => {
120
120
  console.log(`[delegation] commit gate: ${plan.commitGate} (memory raises its own gate: ${plan.memoryRaisesCommitGate})`);
121
121
  };
122
122
 
123
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
124
- if (isDirectRun) main(process.argv.slice(2));
123
+ if (isDirectRun(import.meta.url)) main(process.argv.slice(2));
@@ -21,8 +21,8 @@
21
21
 
22
22
  import { existsSync, statSync, accessSync, realpathSync, constants } from 'node:fs';
23
23
  import { join } from 'node:path';
24
- import { pathToFileURL } from 'node:url';
25
24
  import os from 'node:os';
25
+ import { isDirectRun } from './direct-run.mjs';
26
26
  import { validateManifest, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
27
27
 
28
28
  // Probe states. `unknown` (a wrapped fs error) NEVER counts as present in any readiness rule.
@@ -439,5 +439,4 @@ const main = (_argv, deps = {}) => {
439
439
  process.exit(0); // informational, like validate.mjs non-strict — never blocks anything
440
440
  };
441
441
 
442
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
443
- if (isDirectRun) main(process.argv.slice(2));
442
+ if (isDirectRun(import.meta.url)) main(process.argv.slice(2));
@@ -12,7 +12,7 @@
12
12
  //
13
13
  // The record family (D3, versioned + closed): pre-registration · dispatch · return · fold ·
14
14
  // observation · degrade. Fail-closed in BOTH directions — unknown schema, unknown kind, a missing
15
- // field, a malformed field and an unknown EXTRA field are all refusals (the flow-record.mjs:22-24
15
+ // field, a malformed field and an unknown EXTRA field are all refusals (the flow-record.mjs header's
16
16
  // discipline): the per-record canonical digest is the record's identity, so a stray key would fork
17
17
  // it.
18
18
  //
@@ -19,7 +19,6 @@
19
19
  // Read-only: never writes, never commits, never runs a subscription CLI, spawns nothing. Dependency-
20
20
  // free, Node >= 22. No side effects on import (the isDirectRun idiom).
21
21
 
22
- import { pathToFileURL } from 'node:url';
23
22
  import { EXIT as DOCTOR_EXIT, STATUS as DOCTOR_STATUS, TRUSTED_DIRS as DOCTOR_TRUSTED_DIRS } from './autonomy-doctor.mjs';
24
23
  import {
25
24
  RECOMMENDATIONS_SECTION_HEADER,
@@ -30,6 +29,7 @@ import {
30
29
  VERDICT_SKIPS_TEMPLATE,
31
30
  ACKS_FILE,
32
31
  } from './recommendations.mjs';
32
+ import { isDirectRun } from './direct-run.mjs';
33
33
  import { SKIPPED_READONLY } from './setup-backends.mjs';
34
34
  // The parity verdicts that read-only skip may report — a CLOSED set the same two mode docs enumerate.
35
35
  import { PARITY } from './refresh-parity.mjs';
@@ -338,8 +338,7 @@ export const main = (argv, ctx = {}) => {
338
338
  }
339
339
  };
340
340
 
341
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
342
- if (isDirectRun) {
341
+ if (isDirectRun(import.meta.url)) {
343
342
  const r = main(process.argv.slice(2));
344
343
  if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
345
344
  if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
@@ -18,9 +18,10 @@
18
18
 
19
19
  import { existsSync, statSync, readFileSync, lstatSync } from 'node:fs';
20
20
  import { join, resolve, dirname } from 'node:path';
21
- import { pathToFileURL, fileURLToPath } from 'node:url';
21
+ import { fileURLToPath } from 'node:url';
22
22
  import os from 'node:os';
23
23
  import { resolveDir, detectBackends, findOnPath } from './detect-backends.mjs';
24
+ import { isDirectRun } from './direct-run.mjs';
24
25
  // The ONE dependency-free semver (shared with bin/install.mjs) — the bridge freshness probe compares
25
26
  // the placed version against the kit-bundled mirror; null-on-unparseable maps to 'unknown' (INV-B).
26
27
  import { parseSemver, compareSemver } from './semver-lite.mjs';
@@ -773,5 +774,4 @@ const main = (argv) => {
773
774
  console.log(args.surface.mode === 'json' ? JSON.stringify(envelope, null, 2) : render(toViewModel(envelope), args.surface));
774
775
  };
775
776
 
776
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
777
- if (isDirectRun) main(process.argv.slice(2));
777
+ if (isDirectRun(import.meta.url)) main(process.argv.slice(2));