@sabaiway/agent-workflow-kit 5.11.0 → 5.11.1

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 (59) hide show
  1. package/CHANGELOG.md +61 -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/doc-parity.mjs +2 -3
  23. package/tools/family-registry.mjs +3 -3
  24. package/tools/flow-adoption-mint.mjs +70 -0
  25. package/tools/flow-append.mjs +309 -0
  26. package/tools/flow-chain-state.mjs +91 -0
  27. package/tools/flow-check.mjs +2 -3
  28. package/tools/flow-delta-proof.mjs +307 -0
  29. package/tools/flow-record.mjs +1 -1
  30. package/tools/flow-store-read.mjs +3 -3
  31. package/tools/flow-store.mjs +35 -812
  32. package/tools/flow-subset-budget.mjs +81 -0
  33. package/tools/flow-writer.mjs +3 -3
  34. package/tools/gate-hook.mjs +3 -3
  35. package/tools/gates-init.mjs +3 -3
  36. package/tools/grounding.mjs +2 -3
  37. package/tools/hide-footprint.mjs +2 -3
  38. package/tools/inject-methodology.mjs +2 -3
  39. package/tools/lens-region.mjs +2 -3
  40. package/tools/manifest/validate.mjs +2 -3
  41. package/tools/migrate-adr-store.mjs +3 -3
  42. package/tools/path-inventory.mjs +2 -3
  43. package/tools/procedures.mjs +3 -3
  44. package/tools/receipt-deadline.mjs +2 -3
  45. package/tools/recipes.mjs +2 -3
  46. package/tools/recommendations.mjs +3 -3
  47. package/tools/release-scan.mjs +2 -3
  48. package/tools/repo-search.mjs +2 -3
  49. package/tools/review-state.mjs +3 -3
  50. package/tools/run-gates.mjs +2 -3
  51. package/tools/sandbox-masks.mjs +3 -3
  52. package/tools/set-autonomy.mjs +2 -3
  53. package/tools/set-flow.mjs +3 -3
  54. package/tools/set-recipe.mjs +2 -3
  55. package/tools/setup-backends.mjs +3 -3
  56. package/tools/store-append.mjs +2 -2
  57. package/tools/uninstall.mjs +2 -3
  58. package/tools/velocity-profile.mjs +3 -3
  59. package/tools/worktrees.mjs +3 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,67 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 5.11.1 — the flow store becomes a facade over five leaves, and the direct-run guard fix finally reaches you (AD-102)
8
+
9
+ **A tool invoked through a symlink used to run nothing and exit 0 — and five of the affected tools are
10
+ declared gates.** That fix has been sitting in-repo, unpublished, since the delegation series' own
11
+ measurement came back FAIL and the fix was correctly held back from a release it should not have
12
+ justified (AD-101). It ships here, on a regular train, because it is a bug fix and every deployed
13
+ host still carries the broken form. Beside it, the flow store stops being one 827-line module.
14
+
15
+ - **The direct-run entry guard now decides by REAL PATH, in all 66 frozen sites.** The lexical form
16
+ compared `import.meta.url` against `process.argv[1]`, which is false when the entry point is a
17
+ symlink — so the tool did nothing, said nothing, and exited 0. Every one of the 39 kit tools now
18
+ decides through the shipped `isDirectRun(import.meta.url)` leaf; the seven mirrored
19
+ `references/scripts` and `references/hooks` payload scripts, which cannot reach that leaf from a
20
+ deployed project, inline the same fail-closed realpath guard. `velocity-profile.mjs` also drops a
21
+ vestigial exported predicate with no importer. The new `test/direct-run-sweep.test.mjs` pins list
22
+ completeness, in-process import of all 39, the absence of the lexical byte pattern, and a SPAWNED
23
+ symlink smoke — observed red 3/3 before the fix, so the silent-pass class is proven dead rather than
24
+ believed dead.
25
+ - **`tools/flow-store.mjs` keeps its path, its 29 export names and every one of its 22 import sites,
26
+ and becomes a 50-line re-export facade.** The flow-specific write side moved verbatim into five
27
+ leaves with one responsibility each and one-way edges: `flow-chain-state.mjs` (91, the pure chain
28
+ walk and the prior-terminal reference validator) · `flow-subset-budget.mjs` (81, the pure
29
+ Decision-7/8 counting-context budget) · `flow-append.mjs` (309, the store's ONE write door, keeping
30
+ the locked subset-attempt factory beside the lane it is the only sanctioned entrance to) ·
31
+ `flow-adoption-mint.mjs` (70) · `flow-delta-proof.mjs` (307, the bookkeeping-delta custody proof).
32
+ Nothing about the public surface moved, so nothing you import changes.
33
+ - **The split is proven, not asserted.** A line-multiset conservation check compares the pre-split
34
+ module against the union of the five leaves and expects exactly two lines to have gained a copy —
35
+ and it was run twice more, against the unsplit module alone and with one leaf omitted, so that a
36
+ comparator which cannot report loss could not pass for one that does. The three owning suites are
37
+ BYTE-IDENTICAL and reproduce 169/169; the ten named suites show an empty `git diff --name-only`.
38
+ `test/flow-store-layout.test.mjs` (not shipped) now pins the frozen surface, that every facade name
39
+ is the SAME binding as its leaf's export, that the facade carries no logic at all, the size caps,
40
+ and the one-way import direction including its negative edges.
41
+ - **`test/package-content.test.mjs` moves its payload pin from 222 to 227 files** and names the five
42
+ leaves: the facade imports all five, so a leaf falling out of the published tarball would break
43
+ every append at load, and the pin is what says so.
44
+
45
+ Recorded size effect, reason `tranche 2: flow-store split`:
46
+
47
+ ```text
48
+ agent-workflow-kit/test/package-content.test.mjs: lines 583 -> 600 (raise)
49
+ agent-workflow-kit/tools/flow-store.mjs: lines 827 -> none
50
+ agent-workflow-kit: aggregate lines 124960 -> 125315 (raise)
51
+ ```
52
+
53
+ Recorded size effect of the direct-run sweep that ships with it (reason: a standalone deployed script
54
+ cannot reach the kit's shared direct-run leaf, so it inlines the realpath guard — one lexical line
55
+ becomes an 11-line fail-closed IIFE; the 39 converted tool guards themselves only shrink):
56
+
57
+ ```text
58
+ agent-workflow-kit/references/hooks/gate-approve.mjs: lines 559 -> 570 (raise)
59
+ agent-workflow-kit/references/hooks/state-block-guard.mjs: lines 425 -> 437 (raise)
60
+ agent-workflow-kit/references/scripts/archive-changelog.mjs: lines 546 -> 557 (raise)
61
+ agent-workflow-kit/references/scripts/archive-decisions.mjs: lines 1199 -> 1210 (raise)
62
+ agent-workflow-kit/references/scripts/archive-issues.mjs: lines 415 -> 426 (raise)
63
+ agent-workflow-kit/references/scripts/check-docs-size.mjs: lines 580 -> 591 (raise)
64
+ agent-workflow-kit/references/scripts/migrate-gates.mjs: lines 722 -> 733 (raise)
65
+ agent-workflow-kit: aggregate lines 124726 -> 124805 -> 124925 -> 124960 (raise)
66
+ ```
67
+
7
68
  ## 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
69
 
9
70
  **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.1'
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.1",
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.1",
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));
@@ -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));
@@ -0,0 +1,70 @@
1
+ // flow-adoption-mint.mjs — the adoption mint (#58): the frontmatter planId reader and mintAdoption,
2
+ // which binds a plan's chain identity to {frontmatter planId, plan content digest} and lands the
3
+ // chain's FIRST record. Split out of flow-store.mjs unchanged (baseline-practices tranche 2); the
4
+ // facade re-exports both names.
5
+ //
6
+ // The plan file is READ, never written. Imports run ONE way: this leaf mints through the store's
7
+ // ONE append door (flow-append.mjs) and never reaches the flow-store.mjs facade or its sibling mint
8
+ // leaf — the one-line sha256Hex below is a deliberate copy rather than a sideways import.
9
+
10
+ import { createHash } from 'node:crypto';
11
+ import { readFileSync } from 'node:fs';
12
+ import { resolve } from 'node:path';
13
+ import { FLOW_SCHEMA_VERSION, CHAIN_KIND, canonicalFlowDigest } from './flow-record.mjs';
14
+ import { resolveBase, computeTreeFingerprint } from './core-evidence.mjs';
15
+ import { flowStoreStop, resolveFlowStorePath, readFlowStore, deriveFlowOwner } from './flow-store-read.mjs';
16
+ import { appendFlowRecord } from './flow-append.mjs';
17
+
18
+ const stop = flowStoreStop;
19
+ const sha256Hex = (bytes) => createHash('sha256').update(bytes).digest('hex');
20
+
21
+ const PLAN_ID_FRONTMATTER_HINT = 'planId: <your-stable-plan-id>';
22
+
23
+ // Identity binds only a CLOSED leading frontmatter block — an unterminated block never yields an
24
+ // id; CRLF is normalized per line so line endings never fork chain identity.
25
+ export const readPlanFrontmatterId = (text) => {
26
+ const lines = text.split('\n').map((line) => line.replace(/\r$/, ''));
27
+ if (lines[0]?.trim() !== '---') return null;
28
+ const close = lines.findIndex((line, i) => i > 0 && line.trim() === '---');
29
+ if (close === -1) return null;
30
+ for (const line of lines.slice(1, close)) {
31
+ const m = /^planId:[ \t]*(\S+)[ \t]*$/.exec(line);
32
+ if (m) return m[1];
33
+ }
34
+ return null;
35
+ };
36
+
37
+ export const mintAdoption = ({ cwd = process.cwd(), env = process.env, deps = {}, planPath, planLabel, cycle = 1, commitEpoch = 0, timestamp = new Date().toISOString() } = {}) => {
38
+ const owner = deriveFlowOwner(cwd);
39
+ if (owner == null) throw stop('not inside a git work tree — the adoption mint derives the owning worktree and the tree fingerprint from git (fail closed)');
40
+ let planBytes;
41
+ try {
42
+ planBytes = readFileSync(resolve(cwd, planPath));
43
+ } catch (err) {
44
+ throw stop(`cannot read the plan file ${planPath} (${(err && err.code) || (err && err.message) || err}) — the adoption mint READS an existing plan file (fail closed)`);
45
+ }
46
+ const planId = readPlanFrontmatterId(planBytes.toString('utf8'));
47
+ if (planId == null) {
48
+ throw stop(`the plan file ${planPath} carries no frontmatter planId — plan filenames are never chain identity. Add this line inside a leading "---" frontmatter block:\n${PLAN_ID_FRONTMATTER_HINT}\nand re-run; the plan file is never written by this mint (fail closed)`);
49
+ }
50
+ const planDigest = sha256Hex(planBytes);
51
+ // A pre-append read purely for the NAMED refusal: the locked append would refuse a second
52
+ // adoption anyway, but only this comparison can surface whether the plan content still matches.
53
+ const resolved = resolveFlowStorePath(cwd, env);
54
+ const adopted = resolved == null ? undefined : readFlowStore(resolved).records
55
+ .find((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption' && r.planId === planId);
56
+ if (adopted !== undefined) {
57
+ throw stop(adopted.planDigest === planDigest
58
+ ? `plan "${planId}" is already adopted (content digest unchanged — a rename never resets chain identity); adoption is only ever the chain's first record`
59
+ : `plan "${planId}" is already adopted and the plan file content no longer matches its adoption record (recorded ${adopted.planDigest.slice(0, 12)}…, current ${planDigest.slice(0, 12)}…) — re-adopting edited plan content is refused; the digest mismatch is surfaced, never silent`);
60
+ }
61
+ const fingerprint = computeTreeFingerprint(cwd);
62
+ if (fingerprint == null) throw stop('cannot compute the tree fingerprint — the adoption record binds {base, fingerprint} (fail closed)');
63
+ const record = {
64
+ schema: FLOW_SCHEMA_VERSION, kind: CHAIN_KIND, purpose: 'adoption', planId, cycle, round: 0,
65
+ commitEpoch, owner, base: resolveBase(cwd), timestamp, stepId: null, fingerprint,
66
+ planLabel: planLabel ?? planId, createdAt: timestamp, planDigest,
67
+ };
68
+ const { writtenPath } = appendFlowRecord({ cwd, record, env, deps });
69
+ return { writtenPath, record, digest: canonicalFlowDigest(record) };
70
+ };