@sabaiway/agent-workflow-kit 5.10.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 (74) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +2 -2
  3. package/SKILL.md +1 -1
  4. package/capability.json +1 -1
  5. package/package.json +1 -1
  6. package/references/hooks/gate-approve.mjs +13 -2
  7. package/references/hooks/state-block-guard.mjs +14 -2
  8. package/references/modes/commit-guard.md +11 -8
  9. package/references/modes/core-evidence.md +1 -1
  10. package/references/modes/dispatch.md +32 -10
  11. package/references/modes/worktrees.md +47 -3
  12. package/references/scripts/archive-changelog.mjs +14 -3
  13. package/references/scripts/archive-decisions.mjs +14 -3
  14. package/references/scripts/archive-issues.mjs +14 -3
  15. package/references/scripts/check-docs-size.mjs +14 -3
  16. package/references/scripts/migrate-gates.mjs +13 -2
  17. package/tools/ack-write.mjs +3 -3
  18. package/tools/advisor-matrix.mjs +165 -0
  19. package/tools/autonomy-doctor.mjs +2 -3
  20. package/tools/bridge-settings.mjs +2 -3
  21. package/tools/cheap-agents.mjs +3 -3
  22. package/tools/commands.mjs +4 -5
  23. package/tools/commit-guard.mjs +77 -20
  24. package/tools/core-evidence.mjs +12 -3
  25. package/tools/coverage-check.mjs +2 -3
  26. package/tools/delegation.mjs +2 -3
  27. package/tools/detect-backends.mjs +2 -3
  28. package/tools/dispatch-advisor.mjs +323 -0
  29. package/tools/dispatch.mjs +174 -109
  30. package/tools/doc-parity.mjs +69 -16
  31. package/tools/family-registry.mjs +3 -3
  32. package/tools/flow-adoption-mint.mjs +70 -0
  33. package/tools/flow-append.mjs +309 -0
  34. package/tools/flow-chain-state.mjs +91 -0
  35. package/tools/flow-check-cores.mjs +35 -6
  36. package/tools/flow-check-rungs.mjs +20 -2
  37. package/tools/flow-check.mjs +22 -8
  38. package/tools/flow-delta-proof.mjs +307 -0
  39. package/tools/flow-record.mjs +1 -1
  40. package/tools/flow-store-read.mjs +3 -3
  41. package/tools/flow-store.mjs +35 -812
  42. package/tools/flow-subset-budget.mjs +81 -0
  43. package/tools/flow-writer.mjs +3 -3
  44. package/tools/gate-hook.mjs +3 -3
  45. package/tools/gates-init.mjs +3 -3
  46. package/tools/grounding.mjs +2 -3
  47. package/tools/hide-footprint.mjs +2 -3
  48. package/tools/inject-methodology.mjs +2 -3
  49. package/tools/lens-region.mjs +2 -3
  50. package/tools/manifest/validate.mjs +2 -3
  51. package/tools/migrate-adr-store.mjs +3 -3
  52. package/tools/observation-builder.mjs +123 -0
  53. package/tools/path-inventory.mjs +2 -3
  54. package/tools/procedures.mjs +3 -3
  55. package/tools/receipt-deadline.mjs +2 -3
  56. package/tools/recipes.mjs +2 -3
  57. package/tools/recommendations.mjs +3 -3
  58. package/tools/release-scan.mjs +2 -3
  59. package/tools/repo-search.mjs +2 -3
  60. package/tools/review-state.mjs +3 -3
  61. package/tools/run-gates.mjs +2 -3
  62. package/tools/sandbox-masks.mjs +3 -3
  63. package/tools/satellite-locator.mjs +179 -0
  64. package/tools/set-autonomy.mjs +2 -3
  65. package/tools/set-flow.mjs +3 -3
  66. package/tools/set-recipe.mjs +2 -3
  67. package/tools/setup-backends.mjs +3 -3
  68. package/tools/store-append.mjs +2 -2
  69. package/tools/uninstall.mjs +2 -3
  70. package/tools/velocity-profile.mjs +3 -3
  71. package/tools/worktree-handoff-return.mjs +369 -0
  72. package/tools/worktree-prompt.mjs +190 -0
  73. package/tools/worktrees-record.mjs +171 -0
  74. package/tools/worktrees.mjs +311 -300
@@ -0,0 +1,179 @@
1
+ // satellite-locator.mjs — slug → satellite worktree, and the proof that the handoff found there IS
2
+ // that satellite's identity (delegation Plan 3, Phase 2).
3
+ //
4
+ // Extracted out of worktrees.mjs so BOTH modes can ask the question: `worktrees prompt` composes a
5
+ // satellite's cold-start prompt today, and the dispatch-side handoff-return rung — a later phase,
6
+ // not yet wired — will read what came back through this same leaf. That rung is a `dispatch` verb,
7
+ // so this leaf is what will keep the 3200-line worktrees tool out of the dispatch CLI's closure.
8
+ //
9
+ // READ-ONLY by construction: it writes nothing, spawns nothing, and — deliberately — CONTENT-READS
10
+ // nothing of its own. The git runner and the fs seams are INJECTED, so a caller decides what may
11
+ // run, and the content read arrives as `fs.readFileNoFollow(abs)` returning the family's structured
12
+ // outcome { bytes } | { absent } | { unsafe } | { error: code }. That is not fastidiousness: the ONE
13
+ // no-follow read door lives in worktrees.mjs, its single body is pinned there by a tripwire, and a
14
+ // second body here would be exactly the duplication that pin exists to prevent. `fs` is the shape
15
+ // worktrees.mjs builds: { lstat, readdir, realpath, readFileNoFollow }.
16
+ //
17
+ // Node built-ins plus two pure leaves. No side effects on import; no CLI. Dependency-free, Node >= 22.
18
+
19
+ import { join } from 'node:path';
20
+ import { PLANS_REL } from './plan-files.mjs';
21
+ import { stop, handoffBasename, parseProvisionRecord, displayValue } from './worktrees-record.mjs';
22
+
23
+ // Every refusal below is the worktrees STOP, unchanged — a caller that reached this leaf directly
24
+ // (the dispatch side does) must be able to recognize it without importing worktrees.mjs.
25
+ export { WORKTREES_STOP } from './worktrees-record.mjs';
26
+
27
+ export const DEFAULT_BRANCH_PREFIX = 'aw/';
28
+
29
+ export const parseWorktreeList = (text) => {
30
+ const entries = [];
31
+ let fields = [];
32
+ const finishEntry = () => {
33
+ if (fields.length === 0) return;
34
+ const entry = { path: null, head: null, branch: null, detached: false, prunable: false, bare: false };
35
+ for (const field of fields) {
36
+ if (field.startsWith('worktree ')) entry.path = field.slice('worktree '.length);
37
+ else if (field.startsWith('HEAD ')) entry.head = field.slice('HEAD '.length);
38
+ else if (field.startsWith('branch ')) entry.branch = field.slice('branch '.length);
39
+ else if (field === 'detached') entry.detached = true;
40
+ else if (field === 'bare') entry.bare = true;
41
+ else if (field === 'prunable' || field.startsWith('prunable ')) entry.prunable = true;
42
+ }
43
+ if (entry.path !== null) entries.push(entry);
44
+ fields = [];
45
+ };
46
+ for (const field of String(text).split('\0')) {
47
+ if (field === '') finishEntry();
48
+ else fields.push(field);
49
+ }
50
+ finishEntry();
51
+ return entries;
52
+ };
53
+
54
+ export const listWorktrees = (git, cwd) => {
55
+ const r = git(['worktree', 'list', '--porcelain', '-z'], cwd);
56
+ if (r.status !== 0) throw stop(`git worktree list failed: ${r.stderr.trim() || r.stdout.trim()}`);
57
+ return parseWorktreeList(r.stdout);
58
+ };
59
+
60
+ export const classifyNodeNoFollow = (path, fs) => {
61
+ const node = (() => {
62
+ try {
63
+ return { stat: fs.lstat(path) };
64
+ } catch (error) {
65
+ return error?.code === 'ENOENT'
66
+ ? { stat: null }
67
+ : { error: error?.code ?? 'fs error' };
68
+ }
69
+ })();
70
+ if (node.error) return { kind: 'error', error: node.error };
71
+ if (node.stat === null) return { kind: 'absent' };
72
+ if (!node.stat.isSymbolicLink()) {
73
+ if (node.stat.isDirectory()) return { kind: 'plain-directory', stat: node.stat };
74
+ if (node.stat.isFile()) return { kind: 'regular-file', stat: node.stat };
75
+ return { kind: 'special', stat: node.stat };
76
+ }
77
+ const realPath = (() => {
78
+ try {
79
+ return { path: fs.realpath(path) };
80
+ } catch (error) {
81
+ return { error: error?.code ?? 'fs error' };
82
+ }
83
+ })();
84
+ if (realPath.error) return { kind: 'symlink-unresolvable', error: realPath.error };
85
+ const target = (() => {
86
+ try {
87
+ return { stat: fs.lstat(realPath.path) };
88
+ } catch (error) {
89
+ return { error: error?.code ?? 'fs error' };
90
+ }
91
+ })();
92
+ if (target.error) return { kind: 'symlink-unresolvable', error: target.error };
93
+ if (target.stat.isDirectory()) return { kind: 'symlink-to-directory', realPath: realPath.path, stat: node.stat };
94
+ if (target.stat.isFile()) return { kind: 'symlink-to-file', realPath: realPath.path, stat: node.stat };
95
+ return { kind: 'symlink-to-special', realPath: realPath.path, stat: node.stat };
96
+ };
97
+
98
+ // Whole-chain no-follow: the worktree root, docs, and docs/plans must be plain directories;
99
+ // handoff candidates count ONLY as regular files. states: ok | absent | unreadable.
100
+ // ANY stat failure (not just readdir) renders honestly — list must never crash on a bad node.
101
+ export const scanPlansDir = ({ wtRoot, fs }) => {
102
+ if (classifyNodeNoFollow(wtRoot, fs).kind !== 'plain-directory') return { state: 'unreadable' };
103
+ const docs = classifyNodeNoFollow(join(wtRoot, 'docs'), fs);
104
+ if (docs.kind === 'absent') return { state: 'absent' };
105
+ if (docs.kind !== 'plain-directory') return { state: 'unreadable' };
106
+ const plans = classifyNodeNoFollow(join(wtRoot, PLANS_REL), fs);
107
+ if (plans.kind === 'absent') return { state: 'absent' };
108
+ if (plans.kind !== 'plain-directory') return { state: 'unreadable' };
109
+ let names;
110
+ try {
111
+ names = fs.readdir(join(wtRoot, PLANS_REL));
112
+ } catch {
113
+ return { state: 'unreadable' };
114
+ }
115
+ const handoffs = [];
116
+ const nonRegular = [];
117
+ for (const n of names) {
118
+ if (!/^handoff-.+\.md$/.test(n)) continue;
119
+ const cand = classifyNodeNoFollow(join(wtRoot, PLANS_REL, n), fs);
120
+ if (cand.kind !== 'regular-file') nonRegular.push(n);
121
+ else handoffs.push(n);
122
+ }
123
+ return { state: 'ok', handoffs, nonRegular };
124
+ };
125
+
126
+ export const branchNameOf = (entry) => entry.branch?.replace(/^refs\/heads\//, '') ?? null;
127
+
128
+ export const findSatelliteEntry = ({ root, slug, branch, git, fs }) => {
129
+ const entries = listWorktrees(git, root).slice(1);
130
+ const exactHandoff = [];
131
+ for (const entry of entries) {
132
+ if (entry.prunable) continue;
133
+ const scan = scanPlansDir({ wtRoot: entry.path, fs });
134
+ if (scan.state === 'ok' && scan.handoffs.includes(handoffBasename(slug))) exactHandoff.push(entry);
135
+ }
136
+ if (exactHandoff.length > 1) {
137
+ throw stop(`multiple worktrees carry ${handoffBasename(slug)} — cleanup the duplicate identity before continuing`);
138
+ }
139
+ if (branch !== null) {
140
+ const byBranch = entries.filter((entry) => entry.branch === `refs/heads/${branch}`);
141
+ if (byBranch.length > 1) throw stop(`multiple worktrees claim branch ${branch}`);
142
+ if (byBranch.length === 1) return byBranch[0];
143
+ }
144
+ if (exactHandoff.length === 1) return exactHandoff[0];
145
+ const fallback = entries.filter((entry) => entry.branch === `refs/heads/${DEFAULT_BRANCH_PREFIX}${slug}`);
146
+ if (fallback.length === 1) return fallback[0];
147
+ throw stop(`no registered satellite worktree for ${slug}`);
148
+ };
149
+
150
+ export const readSatelliteIdentity = ({ entry, slug, expectedBranch, fs, abandon = false }) => {
151
+ const name = handoffBasename(slug);
152
+ const scan = scanPlansDir({ wtRoot: entry.path, fs });
153
+ if (scan.state === 'ok' && scan.nonRegular.includes(name)) {
154
+ throw stop(`handoff identity mismatch: ${name} is not a regular file`);
155
+ }
156
+ if (scan.state !== 'ok' || !scan.handoffs.includes(name)) {
157
+ if (abandon) throw stop(`${name} is absent — force deletion is forbidden without the handoff identity`);
158
+ throw stop(`handoff identity mismatch: expected ${name} in the satellite`);
159
+ }
160
+ // Every value below reaches a terminal, and every one of them is foreign: the names come from a
161
+ // directory listing, the record fields from a hand-editable file. They render ESCAPED — the guard
162
+ // that refuses them for the prompt must not be undone by the message that reports them.
163
+ if (scan.handoffs.length !== 1) {
164
+ throw stop(`handoff identity mismatch: expected exactly ${name}, found [${scan.handoffs.map(displayValue).join(', ')}]`);
165
+ }
166
+ const leaf = fs.readFileNoFollow(join(entry.path, PLANS_REL, name));
167
+ if (!leaf.bytes) throw stop(`handoff identity mismatch: ${name} is not readable as a regular file`);
168
+ const record = parseProvisionRecord(String(leaf.bytes));
169
+ const liveBranch = branchNameOf(entry);
170
+ const wantedBranch = expectedBranch ?? liveBranch;
171
+ if (record.slug !== slug || record.branch !== wantedBranch || liveBranch !== wantedBranch) {
172
+ throw stop(
173
+ `handoff identity mismatch: expected slug ${slug} and branch ${displayValue(wantedBranch)}; ` +
174
+ `record has slug ${record.slug === null ? '(missing)' : displayValue(record.slug)} and branch ${record.branch === null ? '(missing)' : displayValue(record.branch)}, ` +
175
+ `live branch ${liveBranch === null ? '(detached)' : displayValue(liveBranch)}`,
176
+ );
177
+ }
178
+ return { record, path: join(entry.path, PLANS_REL, name), branch: wantedBranch };
179
+ };
@@ -22,7 +22,6 @@
22
22
  // Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
23
23
 
24
24
  import { readFileSync, lstatSync } from 'node:fs';
25
- import { pathToFileURL } from 'node:url';
26
25
  import {
27
26
  AUTONOMY_REL,
28
27
  fail,
@@ -35,6 +34,7 @@ import {
35
34
  AUTONOMY_README,
36
35
  } from './autonomy-config.mjs';
37
36
  import { writeAutonomy as writeAutonomyFs } from './autonomy-write.mjs';
37
+ import { isDirectRun } from './direct-run.mjs';
38
38
 
39
39
  // ── argument parsing (usage errors → exit 2) ────────────────────────────────────────
40
40
 
@@ -186,8 +186,7 @@ export const main = (argv, ctx = {}) => {
186
186
  }
187
187
  };
188
188
 
189
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
190
- if (isDirectRun) {
189
+ if (isDirectRun(import.meta.url)) {
191
190
  const r = main(process.argv.slice(2));
192
191
  if (r.stdout) console.log(r.stdout);
193
192
  if (r.stderr) console.error(r.stderr);
@@ -30,7 +30,7 @@
30
30
 
31
31
  import { readFileSync, lstatSync } from 'node:fs';
32
32
  import { join, dirname, resolve } from 'node:path';
33
- import { pathToFileURL, fileURLToPath } from 'node:url';
33
+ import { fileURLToPath } from 'node:url';
34
34
  import { spawnSync } from 'node:child_process';
35
35
  import {
36
36
  CONFIG_REL,
@@ -44,6 +44,7 @@ import {
44
44
  FLOW_CANDIDATE_CLASSES,
45
45
  FLOW_SCHEMA_1_FIXTURE,
46
46
  } from './orchestration-config.mjs';
47
+ import { isDirectRun } from './direct-run.mjs';
47
48
  import { writeConfig as writeConfigFs } from './orchestration-write.mjs';
48
49
  import { loadDeclaration } from './run-gates.mjs';
49
50
  import { compareSemver } from './semver-lite.mjs';
@@ -456,8 +457,7 @@ export const main = (argv, ctx = {}) => {
456
457
  }
457
458
  };
458
459
 
459
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
460
- if (isDirectRun) {
460
+ if (isDirectRun(import.meta.url)) {
461
461
  const r = main(process.argv.slice(2));
462
462
  if (r.stdout) console.log(r.stdout);
463
463
  if (r.stderr) console.error(r.stderr);
@@ -23,8 +23,8 @@
23
23
 
24
24
  import { readFileSync, lstatSync } from 'node:fs';
25
25
  import { homedir } from 'node:os';
26
- import { pathToFileURL } from 'node:url';
27
26
  import { detectBackends } from './detect-backends.mjs';
27
+ import { isDirectRun } from './direct-run.mjs';
28
28
  import { resolveActivityRecipe, composeActiveRecipeLine } from './recipes.mjs';
29
29
  import { loadAutonomy, resolveAutonomy } from './autonomy-config.mjs';
30
30
  import {
@@ -230,8 +230,7 @@ export const main = (argv, ctx = {}) => {
230
230
  }
231
231
  };
232
232
 
233
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
234
- if (isDirectRun) {
233
+ if (isDirectRun(import.meta.url)) {
235
234
  const r = main(process.argv.slice(2));
236
235
  if (r.stdout) console.log(r.stdout);
237
236
  if (r.stderr) console.error(r.stderr);
@@ -28,9 +28,10 @@ import {
28
28
  chmodSync, readFileSync, realpathSync,
29
29
  } from 'node:fs';
30
30
  import { join, resolve, relative, dirname, isAbsolute } from 'node:path';
31
- import { fileURLToPath, pathToFileURL } from 'node:url';
31
+ import { fileURLToPath } from 'node:url';
32
32
  import os from 'node:os';
33
33
  import { KNOWN_BACKENDS, detectBackend, detectBackends, resolveDir, guideFor, READY } from './detect-backends.mjs';
34
+ import { isDirectRun } from './direct-run.mjs';
34
35
  import { copyTreeRefresh, linkManaged, isReadonlyWriteBoundary } from './fs-safe.mjs';
35
36
  import {
36
37
  READONLY_RERUN_HINT, WRAPPER_MODE, scanBundleOwnedDrift, scanWrapperParity, parityVerdict,
@@ -821,5 +822,4 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
821
822
  return worst;
822
823
  };
823
824
 
824
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
825
- if (isDirectRun) process.exit(main(process.argv.slice(2)));
825
+ if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));
@@ -1,6 +1,6 @@
1
1
  // store-append.mjs — the PARAMETERIZED lock/CAS serialized-append leaf (delegation Plan 1, Phase 2,
2
- // D12). Extracted VERBATIM from flow-store.mjs, which is now its first caller; the delegation store
3
- // (dispatch-store.mjs) is the second. No CLI, no side effects on import.
2
+ // D12). Extracted VERBATIM from the flow store, whose write door flow-append.mjs is now its first
3
+ // caller; the delegation store (dispatch-store.mjs) is the second. No CLI, no side effects on import.
4
4
  //
5
5
  // Why a leaf rather than an import of the flow appender: `appendFlowRecordWithPreflight` hardwires
6
6
  // the AW_FLOW_STORE seam and validateFlowRecord, so a second store could only reuse it by pretending
@@ -24,8 +24,8 @@
24
24
 
25
25
  import { existsSync, statSync, lstatSync, readlinkSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
26
26
  import { join, resolve, dirname, basename, isAbsolute } from 'node:path';
27
- import { pathToFileURL } from 'node:url';
28
27
  import os from 'node:os';
28
+ import { isDirectRun } from './direct-run.mjs';
29
29
  import { surveyFamily, surveyProject, FAMILY_MEMBERS, classifyMember, OK } from './family-registry.mjs';
30
30
  import { removeTreeManaged, unlinkManaged, MANAGED_LINK_CONFLICT } from './fs-safe.mjs';
31
31
  import { deriveLinks } from './setup-backends.mjs';
@@ -606,8 +606,7 @@ const main = (argv) => {
606
606
  }
607
607
  };
608
608
 
609
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
610
- if (isDirectRun) {
609
+ if (isDirectRun(import.meta.url)) {
611
610
  try {
612
611
  main(process.argv.slice(2));
613
612
  } catch (err) {
@@ -1,7 +1,7 @@
1
1
  import { existsSync, lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { basename, dirname, join, relative, resolve } from 'node:path';
3
3
  import { homedir, tmpdir } from 'node:os';
4
- import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { fileURLToPath } from 'node:url';
5
5
  // The --autonomy render reads the per-project autonomy policy through the read-only autonomy core
6
6
  // (AD-044). This file is the family's one .claude/settings.json writer, so the policy render lives here;
7
7
  // it never imports the policy fs-writer (autonomy-write.mjs) — the render owns the settings file, not
@@ -10,6 +10,7 @@ import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, COMMAND_REDLINES } from '.
10
10
  // The bridge-wrappers tier's placement probe (AD-044 Plan 4, Decision 2): a tier entry derives ONLY
11
11
  // for a PLACED bridge wrapper — findOnPath is the same read-only PATH scan the backend detector uses.
12
12
  import { findOnPath } from './detect-backends.mjs';
13
+ import { isDirectRun } from './direct-run.mjs';
13
14
  import { compareSemver } from './semver-lite.mjs';
14
15
  // The declared-path resolution + segment containment the allowWrite degrade shares with the
15
16
  // advisor's worktrees-dir convergence lane — ONE leaf, so the two readings cannot drift.
@@ -1820,5 +1821,4 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
1820
1821
  }
1821
1822
  };
1822
1823
 
1823
- export const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
1824
- if (isDirectRun) process.exit(main(process.argv.slice(2)));
1824
+ if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));