@sabaiway/agent-workflow-kit 1.30.0 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -0
- package/README.md +5 -2
- package/SKILL.md +31 -551
- package/capability.json +1 -1
- package/launchers/windsurf-workflow.md +4 -2
- package/package.json +1 -1
- package/references/hooks/gate-approve.mjs +13 -0
- package/references/modes/agents.md +11 -0
- package/references/modes/backends.md +9 -0
- package/references/modes/bootstrap.md +49 -0
- package/references/modes/gates.md +17 -0
- package/references/modes/grounding.md +12 -0
- package/references/modes/help.md +7 -0
- package/references/modes/hook.md +24 -0
- package/references/modes/procedures.md +18 -0
- package/references/modes/recipes.md +18 -0
- package/references/modes/review-state.md +13 -0
- package/references/modes/set-recipe.md +24 -0
- package/references/modes/setup.md +30 -0
- package/references/modes/status.md +22 -0
- package/references/modes/uninstall.md +18 -0
- package/references/modes/upgrade.md +46 -0
- package/references/modes/velocity.md +33 -0
- package/references/shared/composition-handoff.md +39 -0
- package/references/shared/deploy-tail.md +61 -0
- package/references/shared/report-footer.md +116 -0
- package/tools/engine-source.mjs +2 -1
- package/tools/velocity-profile.mjs +253 -32
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join, relative } from 'node:path';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
|
|
5
5
|
// Velocity-profile core + writer: a fixed, audited read-only allowlist that an onboarding step seeds
|
|
6
6
|
// into `.claude/settings.json` so routine read-only commands stop idling on approval prompts.
|
|
@@ -40,6 +40,13 @@ export const UNIVERSAL_READONLY_ALLOWLIST = Object.freeze([
|
|
|
40
40
|
'Bash(git ls-files:*)',
|
|
41
41
|
'Bash(git check-ignore:*)',
|
|
42
42
|
'Bash(git branch --list:*)',
|
|
43
|
+
'Bash(git rev-parse:*)',
|
|
44
|
+
'Bash(git blame:*)',
|
|
45
|
+
'Bash(git shortlog:*)',
|
|
46
|
+
'Bash(git describe:*)',
|
|
47
|
+
'Bash(git tag --list:*)',
|
|
48
|
+
'Bash(git stash list:*)',
|
|
49
|
+
'Bash(git worktree list:*)',
|
|
43
50
|
'Bash(npm view:*)',
|
|
44
51
|
'Bash(npm ls:*)',
|
|
45
52
|
'Bash(npm outdated:*)',
|
|
@@ -51,6 +58,12 @@ export const UNIVERSAL_READONLY_ALLOWLIST = Object.freeze([
|
|
|
51
58
|
'Bash(readlink:*)',
|
|
52
59
|
'Bash(which:*)',
|
|
53
60
|
'Bash(grep:*)',
|
|
61
|
+
'Bash(diff:*)',
|
|
62
|
+
'Bash(stat:*)',
|
|
63
|
+
'Bash(du:*)',
|
|
64
|
+
'Bash(basename:*)',
|
|
65
|
+
'Bash(dirname:*)',
|
|
66
|
+
'Bash(realpath:*)',
|
|
54
67
|
]);
|
|
55
68
|
|
|
56
69
|
// Per-tool POSITIVE allowlists, as frozen arrays. NOTE: `Object.freeze` on a Set does NOT prevent
|
|
@@ -64,6 +77,18 @@ export const GIT_READONLY_SUBCOMMANDS = Object.freeze([
|
|
|
64
77
|
'ls-files',
|
|
65
78
|
'check-ignore',
|
|
66
79
|
'branch --list',
|
|
80
|
+
'rev-parse',
|
|
81
|
+
'blame',
|
|
82
|
+
'shortlog',
|
|
83
|
+
'describe',
|
|
84
|
+
// `git cat-file` is deliberately ABSENT (diff-council fold, AD-040): `--textconv`/`--filters`
|
|
85
|
+
// activate CONFIGURED external filters under an auto-approved command, and its read utility is
|
|
86
|
+
// marginal next to the kept `git show`.
|
|
87
|
+
// Fixed read-only forms ONLY — the bare `git tag` / `git stash` / `git worktree` forms mutate
|
|
88
|
+
// (probe-proven, AD-040), the same multi-token precedent as 'branch --list'.
|
|
89
|
+
'tag --list',
|
|
90
|
+
'stash list',
|
|
91
|
+
'worktree list',
|
|
67
92
|
]);
|
|
68
93
|
export const NPM_READONLY_SUBCOMMANDS = Object.freeze(['view', 'ls', 'outdated']);
|
|
69
94
|
export const SHELL_READONLY = Object.freeze([
|
|
@@ -75,7 +100,56 @@ export const SHELL_READONLY = Object.freeze([
|
|
|
75
100
|
'readlink',
|
|
76
101
|
'which',
|
|
77
102
|
'grep',
|
|
103
|
+
// AD-040 audit survivors. `file` deliberately FAILED the audit (`-C -m <magic>` compiles a magic
|
|
104
|
+
// FILE WRITE — probe-proven) and stays a hand-add candidate only. `diff -l/--paginate` execs `pr`
|
|
105
|
+
// by a build-time FIXED absolute path (probed NOT PATH-resolved) — not arbitrary exec, kept.
|
|
106
|
+
'diff',
|
|
107
|
+
'stat',
|
|
108
|
+
'du',
|
|
109
|
+
'basename',
|
|
110
|
+
'dirname',
|
|
111
|
+
'realpath',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
// ── the opt-in --kit-tools tier (AD-040) ────────────────────────────────────────────────
|
|
115
|
+
// A SEPARATE frozen tier, never an extension of UNIVERSAL_READONLY_ALLOWLIST: the core stays the
|
|
116
|
+
// hook-parity surface (test/gate-hook-core-parity.test.mjs), while tier entries are derived at
|
|
117
|
+
// seed time from the RUNNING tool's own location — resolved-absolute, so a moved or reinstalled
|
|
118
|
+
// skill leaves a stale rule that FAIL-SAFE prompts again (never a silent widening).
|
|
119
|
+
//
|
|
120
|
+
// Membership (9, frozen): the read-only kit tools plus run-gates.mjs — which is NOT read-only but
|
|
121
|
+
// project-exec (it runs the project's OWN declared gates.json commands), so it seeds as ONE exact
|
|
122
|
+
// byte-string pinned to this project root (`--cwd <resolved root>`): a wildcard would be BROADER
|
|
123
|
+
// than the AD-037 hook boundary (`--cwd <dir>` executes an arbitrary OTHER project's gates.json)
|
|
124
|
+
// and a cwd-defaulting entry would follow the shell's current directory into subdirs. NEVER the
|
|
125
|
+
// writers. review-state.mjs's read-only git spawns are in-scope read-only.
|
|
126
|
+
export const KIT_RUN_GATES_TOOL = 'tools/run-gates.mjs';
|
|
127
|
+
export const KIT_READONLY_TOOLS = Object.freeze([
|
|
128
|
+
'tools/recipes.mjs',
|
|
129
|
+
'tools/procedures.mjs',
|
|
130
|
+
'tools/family-registry.mjs',
|
|
131
|
+
'tools/detect-backends.mjs',
|
|
132
|
+
'tools/commands.mjs',
|
|
133
|
+
'tools/review-state.mjs',
|
|
134
|
+
KIT_RUN_GATES_TOOL,
|
|
135
|
+
'tools/manifest/validate.mjs',
|
|
136
|
+
'tools/release-scan.mjs',
|
|
137
|
+
]);
|
|
138
|
+
// Writer previews: ONLY writers whose ARG-FREE invocation is a documented dry-run ("Default is
|
|
139
|
+
// --dry-run" in their usage) seed an EXACT preview byte-string — every --apply/--write/--yes keeps
|
|
140
|
+
// its prompt. set-recipe is excluded (its preview takes variable --set ops); setup-backends and
|
|
141
|
+
// hide-footprint are excluded (their arg-free forms APPLY); uninstall is excluded (a guarded
|
|
142
|
+
// teardown documents --dry-run explicitly — its bare form is not its preview contract).
|
|
143
|
+
export const KIT_WRITER_PREVIEW_TOOLS = Object.freeze([
|
|
144
|
+
'tools/velocity-profile.mjs',
|
|
145
|
+
'tools/cheap-agents.mjs',
|
|
146
|
+
'tools/gate-hook.mjs',
|
|
78
147
|
]);
|
|
148
|
+
const KIT_WILDCARD_TOOLS = Object.freeze(KIT_READONLY_TOOLS.filter((rel) => rel !== KIT_RUN_GATES_TOOL));
|
|
149
|
+
// The kit root this tool runs from (tools/..) — the tier's seed-time path anchor.
|
|
150
|
+
const KIT_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
151
|
+
const KIT_TOOL_INVOKER = 'node';
|
|
152
|
+
const RUN_GATES_CWD_FLAG = '--cwd';
|
|
79
153
|
|
|
80
154
|
// Characters that make a pattern NOT a single read-only command, so the screen rejects them.
|
|
81
155
|
// Per current Claude Code permission semantics: the recognized command SEPARATORS are
|
|
@@ -125,6 +199,9 @@ export const VELOCITY_SYMLINK = 'VELOCITY_SYMLINK';
|
|
|
125
199
|
const VELOCITY_ERROR_NAME = 'VelocityProfileError';
|
|
126
200
|
const ERROR_PREFIX = '[agent-workflow-kit]';
|
|
127
201
|
const BASH_ALLOW_PATTERN = /^Bash\((.+):\*\)$/u;
|
|
202
|
+
// The EXACT (non-wildcard) allow form `Bash(cmd)` — tier-only (run-gates + writer previews); the
|
|
203
|
+
// git/npm/shell core classes stay wildcard-only so the pre-existing advisory behavior never shifts.
|
|
204
|
+
const BASH_EXACT_ALLOW_PATTERN = /^Bash\((.+)\)$/u;
|
|
128
205
|
const BASH_PERMISSION_PATTERN = /^Bash\(.+\)$/u;
|
|
129
206
|
const WHITESPACE_PATTERN = /\s+/u;
|
|
130
207
|
const GIT_COMMAND = 'git';
|
|
@@ -143,6 +220,7 @@ const JSON_NEWLINE_PATTERN = /\n/gu;
|
|
|
143
220
|
const FLAG_DRY_RUN = '--dry-run';
|
|
144
221
|
const FLAG_APPLY = '--apply';
|
|
145
222
|
const FLAG_ACCEPT_EDITS = '--accept-edits';
|
|
223
|
+
const FLAG_KIT_TOOLS = '--kit-tools';
|
|
146
224
|
const FLAG_CWD = '--cwd';
|
|
147
225
|
const FLAG_HELP = '--help';
|
|
148
226
|
const SHORT_FLAG_HELP = '-h';
|
|
@@ -155,12 +233,16 @@ const MUTATING_SCRIPT_HOOK_PATTERN = /^(pre|post)/iu;
|
|
|
155
233
|
// refusal is named, tested, and produces a clear message).
|
|
156
234
|
const MUTATING_ALLOW_COMMAND_PATTERN = /^(?:git\s+(?:commit|push)|npm\s+publish)(?:\s|$)/iu;
|
|
157
235
|
const RESIDUAL_NOTICE =
|
|
158
|
-
'residual: seeded read-only allow entries are a trust-posture convenience, NOT a sandbox; settings-level rules cannot inspect runtime redirection/command-substitution/--output writes; commit/push/publish are never allowlisted (a DIRECT invocation still ASKs, but the runtime residual is not closed here); the residual guard ships as the opt-in PreToolUse hook — Mode: hook (/agent-workflow-kit hook).';
|
|
236
|
+
'residual: seeded read-only allow entries are a trust-posture convenience, NOT a sandbox; settings-level rules cannot inspect runtime redirection/command-substitution/--output writes; commit/push/publish are never allowlisted (a DIRECT invocation still ASKs, but the runtime residual is not closed here); the residual guard ships as the opt-in PreToolUse hook — Mode: hook (/agent-workflow-kit hook). floor (never auto-approved, with or without the tier): every writer --apply/--write/--yes still prompts; clobber-protection STOPs still stop; the three release asks (commit/push/publish) stay maintainer-owned.';
|
|
159
237
|
|
|
160
|
-
const USAGE = `usage: velocity-profile [--dry-run | --apply] [--accept-edits] [--cwd <dir>] [--help]
|
|
238
|
+
const USAGE = `usage: velocity-profile [--dry-run | --apply] [--kit-tools] [--accept-edits] [--cwd <dir>] [--help]
|
|
161
239
|
|
|
162
240
|
Seeds the fixed read-only Claude Code allowlist into .claude/settings.json.
|
|
163
|
-
Default is --dry-run. --apply writes; --accept-edits only sets defaultMode when applying
|
|
241
|
+
Default is --dry-run. --apply writes; --accept-edits only sets defaultMode when applying.
|
|
242
|
+
--kit-tools additionally seeds the audited kit-tool tier: 8 read-only kit tools by resolved
|
|
243
|
+
absolute path (args wildcard), run-gates.mjs as ONE exact project-root-pinned byte-string
|
|
244
|
+
(project-exec - it runs YOUR declared gates.json), and the writers' exact arg-free dry-run
|
|
245
|
+
preview byte-strings. Never touches settings.local.json.`;
|
|
164
246
|
|
|
165
247
|
const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
166
248
|
|
|
@@ -245,6 +327,36 @@ const getBashAllowCommand = (pattern) => {
|
|
|
245
327
|
return match ? match[1] : undefined;
|
|
246
328
|
};
|
|
247
329
|
|
|
330
|
+
// Parse the exact (non-wildcard) form ONLY when the wildcard form does not match — `Bash(cmd:*)`
|
|
331
|
+
// would otherwise parse here as `cmd:*`.
|
|
332
|
+
const getBashExactCommand = (pattern) => {
|
|
333
|
+
if (typeof pattern !== 'string' || BASH_ALLOW_PATTERN.test(pattern)) return undefined;
|
|
334
|
+
const match = pattern.match(BASH_EXACT_ALLOW_PATTERN);
|
|
335
|
+
return match ? match[1] : undefined;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Characters that survive whitespace tokenization but break an UNQUOTED byte-exact path rule:
|
|
339
|
+
// shell quoting syntax and glob brackets (SHELL_METACHARACTERS owns the command-level separators/
|
|
340
|
+
// redirections/expansions — `*`/`?` globs included — but not these four).
|
|
341
|
+
const PATH_BREAKING_CHARACTERS = Object.freeze(["'", '"', '[', ']']);
|
|
342
|
+
|
|
343
|
+
// A path token that can be seeded UNQUOTED into a byte-exact allow rule: POSIX-absolute, no
|
|
344
|
+
// whitespace, no shell metacharacter, no quoting/glob syntax.
|
|
345
|
+
const isSeedablePathToken = (token) =>
|
|
346
|
+
typeof token === 'string' &&
|
|
347
|
+
token.startsWith('/') &&
|
|
348
|
+
!/\s/u.test(token) &&
|
|
349
|
+
!hasShellMetacharacter(token) &&
|
|
350
|
+
!PATH_BREAKING_CHARACTERS.some((ch) => token.includes(ch));
|
|
351
|
+
|
|
352
|
+
// A tier path token must be SEEDABLE (a relative or shell-syntax-carrying spelling is a dead rule
|
|
353
|
+
// the screen refuses to bless) and end on a known tier-tool tail. Any seedable absolute prefix is
|
|
354
|
+
// accepted — the user's own kit copy elsewhere is legitimate shape-wise; entries OUTSIDE the
|
|
355
|
+
// derived tier are still flagged by the pre-existing advisory, and membership in the SEEDED set
|
|
356
|
+
// stays enforced separately by validateProfile's audited-set check.
|
|
357
|
+
const isKitToolPathToken = (token, relPaths) =>
|
|
358
|
+
isSeedablePathToken(token) && relPaths.some((rel) => token.endsWith(`/${rel}`));
|
|
359
|
+
|
|
248
360
|
const isSingleShellToken = (cmd, tokens) => tokens.length === 1 && cmd === tokens[0];
|
|
249
361
|
|
|
250
362
|
const isScriptMap = (scripts) => Boolean(scripts) && typeof scripts === 'object' && !Array.isArray(scripts);
|
|
@@ -309,11 +421,23 @@ const assertClaudeDirSafe = (cwd, deps = {}) => {
|
|
|
309
421
|
return { claudeDirAbsent: false };
|
|
310
422
|
};
|
|
311
423
|
|
|
312
|
-
|
|
424
|
+
// A kit-tool-SHAPED entry (any `node …` allow entry, wildcard or exact) passes the shape screen on
|
|
425
|
+
// any absolute path — but only the entries the tier itself derives for THIS kit + THIS root are
|
|
426
|
+
// audited. Everything else `node`-shaped is arbitrary local JS / foreign project-exec and must
|
|
427
|
+
// stay flagged for hand review (diff-council fold, AD-040).
|
|
428
|
+
const isKitToolShapedCommand = (entry) => {
|
|
429
|
+
const cmd = getBashAllowCommand(entry) ?? getBashExactCommand(entry);
|
|
430
|
+
return cmd !== undefined && tokenizeCommand(cmd)[0] === KIT_TOOL_INVOKER;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const collectPreExistingNonReadonly = (sources, derivedTier = []) =>
|
|
313
434
|
sources.flatMap(({ source, data }) =>
|
|
314
435
|
getAllowEntries(data)
|
|
315
436
|
.filter((entry) => typeof entry === 'string' && BASH_PERMISSION_PATTERN.test(entry))
|
|
316
|
-
.filter(
|
|
437
|
+
.filter(
|
|
438
|
+
(entry) =>
|
|
439
|
+
!screenAllowlistEntry(entry) || (isKitToolShapedCommand(entry) && !derivedTier.includes(entry)),
|
|
440
|
+
)
|
|
317
441
|
.map((entry) => ({ source, entry })),
|
|
318
442
|
);
|
|
319
443
|
|
|
@@ -351,6 +475,21 @@ const formatAllowlist = (result) => [
|
|
|
351
475
|
`already present: ${result.alreadyPresent.length}`,
|
|
352
476
|
];
|
|
353
477
|
|
|
478
|
+
// The tier's honest posture, printed on every --kit-tools run: run-gates is project-exec (never
|
|
479
|
+
// "read-only"), previews stay dry-run-only, and the tier gets none of the hook's residual ask-net.
|
|
480
|
+
const KIT_TIER_NOTICE =
|
|
481
|
+
'kit-tools tier: paths are resolved absolute at seed time (fail-safe - a moved skill or stale path simply prompts again); run-gates.mjs is seeded as ONE exact byte-string pinned to this project root and is project-exec - it runs YOUR declared gates.json commands, never "read-only"; writer previews are exact dry-run byte-strings - every --apply/--write/--yes still prompts; tier entries get NO PreToolUse-hook residual coverage (settings-level posture only - see the velocity mode notes).';
|
|
482
|
+
|
|
483
|
+
const formatKitTier = (result) =>
|
|
484
|
+
result.kitTools
|
|
485
|
+
? [
|
|
486
|
+
`${result.wrote ? 'added' : 'would add'} kit-tools tier entries: ${result.tierToAdd.length}`,
|
|
487
|
+
...formatEntryList(result.tierToAdd),
|
|
488
|
+
`already present (tier): ${result.tierAlreadyPresent.length}`,
|
|
489
|
+
KIT_TIER_NOTICE,
|
|
490
|
+
]
|
|
491
|
+
: [];
|
|
492
|
+
|
|
354
493
|
const formatGateAdvisory = (gateCandidates) => [
|
|
355
494
|
'gate advisory: candidates you may add BY HAND to .claude/settings.json or settings.local.json; this tool will NOT add them',
|
|
356
495
|
...(gateCandidates.length
|
|
@@ -375,6 +514,7 @@ const formatVelocityProfileResult = (result) =>
|
|
|
375
514
|
[
|
|
376
515
|
result.dryRun ? 'agent-workflow velocity profile - DRY RUN (no changes)' : 'agent-workflow velocity profile - APPLY',
|
|
377
516
|
...formatAllowlist(result),
|
|
517
|
+
...formatKitTier(result),
|
|
378
518
|
formatDefaultMode(result),
|
|
379
519
|
...formatGateAdvisory(result.gateCandidates),
|
|
380
520
|
...formatPreExistingAdvisory(result.preExistingNonReadonly),
|
|
@@ -386,16 +526,69 @@ const formatVelocityProfileResult = (result) =>
|
|
|
386
526
|
const exitCodeFor = (err) => err?.exitCode ?? EXIT_PRECONDITION;
|
|
387
527
|
|
|
388
528
|
// `:*` is the trailing word-boundary wildcard: equivalent to a trailing ` *`, recognized only at the
|
|
389
|
-
// end of a pattern per Claude Code semantics.
|
|
529
|
+
// end of a pattern per Claude Code semantics. Two forms are recognized: the wildcard form (the
|
|
530
|
+
// git/npm/shell core + the kit-tool wildcard class) and the EXACT form (tier-only: the root-pinned
|
|
531
|
+
// run-gates byte-string + the writer-preview dry-run byte-strings).
|
|
390
532
|
export const screenAllowlistEntry = (pattern) => {
|
|
391
|
-
const
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
533
|
+
const wildcardCmd = getBashAllowCommand(pattern);
|
|
534
|
+
if (wildcardCmd !== undefined) {
|
|
535
|
+
if (hasShellMetacharacter(wildcardCmd)) return false;
|
|
536
|
+
const tokens = tokenizeCommand(wildcardCmd);
|
|
537
|
+
if (tokens[0] === GIT_COMMAND) return GIT_READONLY_SUBCOMMANDS.includes(getSubcommand(tokens));
|
|
538
|
+
if (tokens[0] === NPM_COMMAND) return NPM_READONLY_SUBCOMMANDS.includes(getSubcommand(tokens));
|
|
539
|
+
// Kit-tool wildcard class: `node <abs kit tool>` + args wildcard. run-gates is deliberately NOT
|
|
540
|
+
// here (Decision 3: only its exact root-pinned form below — a wildcard would cover `--cwd <any>`).
|
|
541
|
+
if (tokens[0] === KIT_TOOL_INVOKER) return tokens.length === 2 && isKitToolPathToken(tokens[1], KIT_WILDCARD_TOOLS);
|
|
542
|
+
return isSingleShellToken(wildcardCmd, tokens) && SHELL_READONLY.includes(wildcardCmd);
|
|
543
|
+
}
|
|
544
|
+
const exactCmd = getBashExactCommand(pattern);
|
|
545
|
+
if (exactCmd === undefined || hasShellMetacharacter(exactCmd)) return false;
|
|
546
|
+
const tokens = tokenizeCommand(exactCmd);
|
|
547
|
+
if (tokens[0] !== KIT_TOOL_INVOKER) return false;
|
|
548
|
+
// Writer preview: the arg-free dry-run byte-string of a preview-class writer.
|
|
549
|
+
if (tokens.length === 2) return isKitToolPathToken(tokens[1], KIT_WRITER_PREVIEW_TOOLS);
|
|
550
|
+
// run-gates: EXACTLY `node <abs run-gates> --cwd <abs root>` — any other form keeps prompting.
|
|
551
|
+
return (
|
|
552
|
+
tokens.length === 4 &&
|
|
553
|
+
isKitToolPathToken(tokens[1], [KIT_RUN_GATES_TOOL]) &&
|
|
554
|
+
tokens[2] === RUN_GATES_CWD_FLAG &&
|
|
555
|
+
isSeedablePathToken(tokens[3])
|
|
556
|
+
);
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Derive the opt-in kit-tools tier for a project: 8 wildcard entries (resolved-absolute script
|
|
561
|
+
* path + args wildcard), ONE exact run-gates entry pinned to the resolved project root, and the
|
|
562
|
+
* writer-preview exact dry-run byte-strings. Pure derivation — a stale path simply prompts again.
|
|
563
|
+
*/
|
|
564
|
+
// A seedable path must survive UNQUOTED inside a byte-exact allow rule: POSIX-absolute, no
|
|
565
|
+
// whitespace, no shell metacharacter, no quoting/glob syntax (isSeedablePathToken). Anything else
|
|
566
|
+
// is refused UP FRONT with a clear error (a Windows-shaped, space- or quote-carrying path would
|
|
567
|
+
// otherwise die downstream as a confusing VELOCITY_NON_READONLY, or seed dead/shell-reinterpreted
|
|
568
|
+
// byte strings) — the hand-add advisory is the fallback.
|
|
569
|
+
const assertSeedablePath = (label, absPath) => {
|
|
570
|
+
if (isSeedablePathToken(absPath)) return;
|
|
571
|
+
throw makeVelocityProfileError(
|
|
572
|
+
VELOCITY_INVALID_ARGUMENT,
|
|
573
|
+
`kit-tools tier: the ${label} "${absPath}" is not a POSIX absolute path free of spaces/metacharacters/quoting - byte-exact allow rules cannot be seeded for it; add hand-picked entries to your settings instead (hand-add)`,
|
|
574
|
+
{ entry: absPath },
|
|
575
|
+
);
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
export const deriveKitToolsAllowlist = ({ projectDir } = {}) => {
|
|
579
|
+
if (typeof projectDir !== 'string' || projectDir === '') {
|
|
580
|
+
throw makeVelocityProfileError(VELOCITY_INVALID_ARGUMENT, 'kit-tools derivation needs a project directory', {
|
|
581
|
+
entry: projectDir,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
const projectRoot = resolve(projectDir);
|
|
585
|
+
assertSeedablePath('resolved project root', projectRoot);
|
|
586
|
+
assertSeedablePath('resolved skill dir', join(KIT_ROOT, '.'));
|
|
587
|
+
return Object.freeze([
|
|
588
|
+
...KIT_WILDCARD_TOOLS.map((rel) => `Bash(${KIT_TOOL_INVOKER} ${join(KIT_ROOT, rel)}:*)`),
|
|
589
|
+
`Bash(${KIT_TOOL_INVOKER} ${join(KIT_ROOT, KIT_RUN_GATES_TOOL)} ${RUN_GATES_CWD_FLAG} ${projectRoot})`,
|
|
590
|
+
...KIT_WRITER_PREVIEW_TOOLS.map((rel) => `Bash(${KIT_TOOL_INVOKER} ${join(KIT_ROOT, rel)})`),
|
|
591
|
+
]);
|
|
399
592
|
};
|
|
400
593
|
|
|
401
594
|
export const discoverGateCandidates = (packageJson) => {
|
|
@@ -405,12 +598,14 @@ export const discoverGateCandidates = (packageJson) => {
|
|
|
405
598
|
};
|
|
406
599
|
|
|
407
600
|
/**
|
|
408
|
-
* Validate that what we are about to write is a subset of the audited
|
|
601
|
+
* Validate that what we are about to write is a subset of the SELECTED audited set: every entry
|
|
409
602
|
* must (a) NOT be a commit/push/publish allow entry, (b) pass the read-only screen, and (c) be a
|
|
410
|
-
* member of
|
|
411
|
-
*
|
|
603
|
+
* member of the audited set (a guard against the constants drifting). The argument-less call keeps
|
|
604
|
+
* its exact historical semantics — the full core validated against itself; a --kit-tools run
|
|
605
|
+
* additionally validates the derived tier against core + tier. Pure; owns no exit codes — a CLI
|
|
606
|
+
* maps the typed `.code` to a process exit.
|
|
412
607
|
*/
|
|
413
|
-
export const validateProfile = (allowEntries = UNIVERSAL_READONLY_ALLOWLIST) => {
|
|
608
|
+
export const validateProfile = (allowEntries = UNIVERSAL_READONLY_ALLOWLIST, auditedSet = UNIVERSAL_READONLY_ALLOWLIST) => {
|
|
414
609
|
if (!Array.isArray(allowEntries)) {
|
|
415
610
|
throw makeVelocityProfileError(VELOCITY_INVALID_ARGUMENT, 'allow entries must be an array', { entry: allowEntries });
|
|
416
611
|
}
|
|
@@ -422,7 +617,7 @@ export const validateProfile = (allowEntries = UNIVERSAL_READONLY_ALLOWLIST) =>
|
|
|
422
617
|
if (!screenAllowlistEntry(entry)) {
|
|
423
618
|
throw makeVelocityProfileError(VELOCITY_NON_READONLY, `not a read-only allow entry: ${entry}`, { entry });
|
|
424
619
|
}
|
|
425
|
-
if (!
|
|
620
|
+
if (!auditedSet.includes(entry)) {
|
|
426
621
|
throw makeVelocityProfileError(VELOCITY_OFFCORE, `allow entry is outside the audited core: ${entry}`, { entry });
|
|
427
622
|
}
|
|
428
623
|
}
|
|
@@ -480,10 +675,25 @@ export const preflightVelocityProfile = ({ cwd }, deps = {}) => {
|
|
|
480
675
|
);
|
|
481
676
|
}
|
|
482
677
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
678
|
+
// The advisory compares kit-tool-shaped entries against the tier derived for THIS kit + root.
|
|
679
|
+
// Defensive derive: on a machine whose paths cannot seed (space/metacharacter — the tier itself
|
|
680
|
+
// refuses loudly on --kit-tools), the flagless advisory falls back to flagging EVERY node-shaped
|
|
681
|
+
// entry — over-flagging is the safe direction, and flagless behavior never gains a new failure
|
|
682
|
+
// mode from local skill paths.
|
|
683
|
+
const derivedTier = (() => {
|
|
684
|
+
try {
|
|
685
|
+
return deriveKitToolsAllowlist({ projectDir });
|
|
686
|
+
} catch {
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
})();
|
|
690
|
+
const preExistingNonReadonly = collectPreExistingNonReadonly(
|
|
691
|
+
[
|
|
692
|
+
{ source: SETTINGS_FILE, data: projectSettings.data },
|
|
693
|
+
{ source: SETTINGS_LOCAL_FILE, data: localSettings.data },
|
|
694
|
+
],
|
|
695
|
+
derivedTier,
|
|
696
|
+
);
|
|
487
697
|
const gateCandidates = discoverGateCandidates(readPackageJson(projectDir, deps));
|
|
488
698
|
|
|
489
699
|
return {
|
|
@@ -500,21 +710,30 @@ export const preflightVelocityProfile = ({ cwd }, deps = {}) => {
|
|
|
500
710
|
};
|
|
501
711
|
};
|
|
502
712
|
|
|
503
|
-
export const planVelocityProfile = (preflight, { acceptEdits } = {}) => {
|
|
713
|
+
export const planVelocityProfile = (preflight, { acceptEdits, kitTools } = {}) => {
|
|
504
714
|
const projectAllow = getAllowEntries(preflight.projectSettings?.data);
|
|
505
715
|
const toAdd = UNIVERSAL_READONLY_ALLOWLIST.filter((entry) => !projectAllow.includes(entry));
|
|
506
716
|
const alreadyPresent = UNIVERSAL_READONLY_ALLOWLIST.filter((entry) => projectAllow.includes(entry));
|
|
507
|
-
|
|
717
|
+
const tier = kitTools === true ? deriveKitToolsAllowlist({ projectDir: preflight.cwd }) : [];
|
|
718
|
+
const tierToAdd = tier.filter((entry) => !projectAllow.includes(entry));
|
|
719
|
+
const tierAlreadyPresent = tier.filter((entry) => projectAllow.includes(entry));
|
|
720
|
+
return { toAdd, alreadyPresent, tierToAdd, tierAlreadyPresent, kitTools: kitTools === true, setsDefaultMode: acceptEdits === true };
|
|
508
721
|
};
|
|
509
722
|
|
|
510
|
-
export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true } = {}, deps = {}) => {
|
|
723
|
+
export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true, kitTools = false } = {}, deps = {}) => {
|
|
511
724
|
const projectDir = cwd ?? deps.cwd ?? process.cwd();
|
|
512
725
|
const preflight = preflightVelocityProfile({ cwd: projectDir }, deps);
|
|
513
|
-
const plan = planVelocityProfile(preflight, { acceptEdits });
|
|
726
|
+
const plan = planVelocityProfile(preflight, { acceptEdits, kitTools });
|
|
514
727
|
// Drift guard runs on BOTH dry-run and apply (so a dry-run faithfully predicts the apply) and
|
|
515
728
|
// validates the FULL audited core, not just the to-add delta — a drifted core entry is caught even
|
|
516
729
|
// when it is already present in the user's allow list (and toAdd is a subset of the core anyway).
|
|
517
730
|
validateProfile();
|
|
731
|
+
// A --kit-tools run additionally validates the SELECTED tier against core + tier — flagless
|
|
732
|
+
// behavior never depends on local skill paths (the derivation does not even run without the flag).
|
|
733
|
+
if (kitTools === true) {
|
|
734
|
+
const tier = deriveKitToolsAllowlist({ projectDir });
|
|
735
|
+
validateProfile(tier, [...UNIVERSAL_READONLY_ALLOWLIST, ...tier]);
|
|
736
|
+
}
|
|
518
737
|
const resultBase = { ...preflight, ...plan };
|
|
519
738
|
if (dryRun) return { wrote: false, dryRun: true, ...resultBase };
|
|
520
739
|
|
|
@@ -528,7 +747,7 @@ export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true }
|
|
|
528
747
|
const fs = fsDeps(deps);
|
|
529
748
|
const settingsPath = join(projectDir, SETTINGS_FILE);
|
|
530
749
|
if (preflight.claudeDirAbsent) fs.mkdir(join(projectDir, CLAUDE_DIR), { recursive: true });
|
|
531
|
-
const merged = mergeProjectSettings(preflight.projectSettings.data, plan.toAdd, acceptEdits);
|
|
750
|
+
const merged = mergeProjectSettings(preflight.projectSettings.data, [...plan.toAdd, ...plan.tierToAdd], acceptEdits);
|
|
532
751
|
fs.writeFile(settingsPath, formatJson(merged, preflight.projectSettings.eol ?? LF), UTF8);
|
|
533
752
|
return { wrote: true, dryRun: false, settingsPath, ...resultBase };
|
|
534
753
|
};
|
|
@@ -541,6 +760,7 @@ export const parseArgs = (argv) => {
|
|
|
541
760
|
if (arg === FLAG_DRY_RUN) return { ...state, dryRunFlag: true };
|
|
542
761
|
if (arg === FLAG_APPLY) return { ...state, apply: true };
|
|
543
762
|
if (arg === FLAG_ACCEPT_EDITS) return { ...state, acceptEdits: true };
|
|
763
|
+
if (arg === FLAG_KIT_TOOLS) return { ...state, kitTools: true };
|
|
544
764
|
if (arg === FLAG_CWD) {
|
|
545
765
|
const next = allArgs[index + 1];
|
|
546
766
|
if (next === undefined || next.startsWith('-')) throw fail(EXIT_USAGE, `${FLAG_CWD} needs a directory argument`);
|
|
@@ -549,7 +769,7 @@ export const parseArgs = (argv) => {
|
|
|
549
769
|
if (arg.startsWith('-')) throw fail(EXIT_USAGE, `unknown flag: ${arg}`);
|
|
550
770
|
throw fail(EXIT_USAGE, `unexpected argument: ${arg}`);
|
|
551
771
|
},
|
|
552
|
-
{ help: false, dryRunFlag: false, apply: false, acceptEdits: false, cwd: undefined, skipNext: false },
|
|
772
|
+
{ help: false, dryRunFlag: false, apply: false, acceptEdits: false, kitTools: false, cwd: undefined, skipNext: false },
|
|
553
773
|
);
|
|
554
774
|
|
|
555
775
|
if (parsed.dryRunFlag && parsed.apply) throw fail(EXIT_USAGE, `${FLAG_DRY_RUN} and ${FLAG_APPLY} cannot be used together`);
|
|
@@ -558,6 +778,7 @@ export const parseArgs = (argv) => {
|
|
|
558
778
|
dryRun: parsed.apply ? false : true,
|
|
559
779
|
apply: parsed.apply,
|
|
560
780
|
acceptEdits: parsed.acceptEdits,
|
|
781
|
+
kitTools: parsed.kitTools,
|
|
561
782
|
cwd: parsed.cwd,
|
|
562
783
|
};
|
|
563
784
|
};
|
|
@@ -572,7 +793,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
572
793
|
return EXIT_OK;
|
|
573
794
|
}
|
|
574
795
|
const result = writeVelocityProfile(
|
|
575
|
-
{ cwd: args.cwd ?? deps.cwd ?? process.cwd(), acceptEdits: args.acceptEdits, dryRun: args.dryRun },
|
|
796
|
+
{ cwd: args.cwd ?? deps.cwd ?? process.cwd(), acceptEdits: args.acceptEdits, dryRun: args.dryRun, kitTools: args.kitTools },
|
|
576
797
|
deps,
|
|
577
798
|
);
|
|
578
799
|
log(formatVelocityProfileResult(result));
|