@sabaiway/agent-workflow-kit 1.31.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 CHANGED
@@ -4,6 +4,52 @@ 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
+ ## 1.32.0 — Approval-idle reduction: the opt-in `velocity --kit-tools` tier, an audited core extension, and the standing-consent advisory (AD-040)
8
+
9
+ A **feature** release; packaging-only for deployments (lineage stays `1.3.0` — no `docs/ai`
10
+ structure change, no migration). Routine read-only kit-tool invocations — the session-start
11
+ discovery line, the procedures advisor, the status/backends/gates checks — stop idling on
12
+ approval prompts, opt-in and honestly labeled; nothing that writes, commits, or publishes gets
13
+ any quieter.
14
+
15
+ - **`velocity --kit-tools` (opt-in tier).** On top of the read-only core, seeds 12 entries derived
16
+ from the RUNNING skill's own location at seed time: 8 read-only kit tools as resolved-absolute
17
+ script path + args wildcard (`recipes` / `procedures` / `family-registry` / `detect-backends` /
18
+ `commands` / `review-state` / `manifest/validate` / `release-scan`), `run-gates.mjs` as ONE
19
+ exact byte-string pinned `--cwd <resolved project root>` and advertised **project-exec, never
20
+ "read-only"** (a wildcard would be broader than the AD-037 hook boundary), and the three
21
+ default-dry-run writers' exact arg-free preview byte-strings (`velocity-profile`,
22
+ `cheap-agents`, `gate-hook`) — every `--apply`/`--write`/`--yes` still prompts. Fail-safe by
23
+ construction: a moved skill or stale path simply prompts again; non-POSIX / space- /
24
+ quote-carrying paths are refused up front with a typed error (hand-add fallback). Flagless
25
+ `velocity` behavior is unchanged (validates core-only, never depends on skill paths).
26
+ - **Dead-rule prevention as a test.** The `velocity.md` tier subsection lists the covered dispatch
27
+ line per tool (the documented-invocation source); the new `test/kit-readonly-tools.test.mjs`
28
+ substitutes the resolved skill dir (+ project root for run-gates) into each line and asserts the
29
+ seeded byte-form matches (prefix for wildcard, equality for exact) — plus the tier ↔
30
+ `commands.mjs` catalog-partition guard (run-gates the only project-exec member; the two
31
+ non-mode-backed validators get a writes-nothing source assertion).
32
+ - **Audited read-only core 18 → 31 (the AD-021 empirical method, probe record in AD-040).**
33
+ Survivors: `diff`, `stat`, `du`, `basename`, `dirname`, `realpath`, `git rev-parse`,
34
+ `git blame`, `git shortlog`, `git describe`, and the FIXED forms `git tag --list`,
35
+ `git stash list`, `git worktree list` (their bare forms mutate — probe-proven). FAILED and
36
+ excluded: `file` (`-C -m` compiles a magic FILE WRITE) and `git cat-file` (`--textconv`/
37
+ `--filters` run configured filters; `git show` already covers the reads). The PreToolUse hook's
38
+ `SEEDED_READONLY_CORE` extends in LOCKSTEP (order-sensitive parity guard); an already-placed
39
+ hook keeps the OLD core (a strict subset) until a delete-to-reseed refresh.
40
+ - **Sharper pre-existing advisory.** `node …`-shaped allow entries OUTSIDE the derived tier
41
+ (foreign script path, foreign `run-gates --cwd` root) are now flagged for hand review — the
42
+ tier's shape can never hide arbitrary local JS.
43
+ - **`set-recipe` standing-consent advisory (wording-only).** After a `--write` that names a
44
+ reviewed/council recipe, the mode file now advises the one-time HAND-adds
45
+ (`codex-review` / `agy-review` / `grounding.mjs`) to `settings.local.json` — stating plainly
46
+ that auto-approval spends subscription quota without a per-run prompt, the kit never writes
47
+ that file, and the entry must match the invocation byte-form including quoting. Solo recipes
48
+ get no advisory; the tool echo is untouched.
49
+ - **Honesty floor, twinned.** The velocity residual notice + its `velocity.md` prose mirror now
50
+ both carry the approval floor: every writer apply-class flag still prompts, clobber-protection
51
+ STOPs still stop, the three release asks (commit/push/publish) stay maintainer-owned.
52
+
7
53
  ## 1.31.0 — Progressive disclosure: SKILL.md becomes a thin router over references/modes + references/shared (AD-039)
8
54
 
9
55
  A **feature** release; packaging-only for deployments (the deployment lineage stays `1.3.0` — 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: '1.31.0'
6
+ version: '1.32.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "1.31.0",
6
+ "version": "1.32.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -71,6 +71,13 @@ export const SEEDED_READONLY_CORE = Object.freeze([
71
71
  'Bash(git ls-files:*)',
72
72
  'Bash(git check-ignore:*)',
73
73
  'Bash(git branch --list:*)',
74
+ 'Bash(git rev-parse:*)',
75
+ 'Bash(git blame:*)',
76
+ 'Bash(git shortlog:*)',
77
+ 'Bash(git describe:*)',
78
+ 'Bash(git tag --list:*)',
79
+ 'Bash(git stash list:*)',
80
+ 'Bash(git worktree list:*)',
74
81
  'Bash(npm view:*)',
75
82
  'Bash(npm ls:*)',
76
83
  'Bash(npm outdated:*)',
@@ -82,6 +89,12 @@ export const SEEDED_READONLY_CORE = Object.freeze([
82
89
  'Bash(readlink:*)',
83
90
  'Bash(which:*)',
84
91
  'Bash(grep:*)',
92
+ 'Bash(diff:*)',
93
+ 'Bash(stat:*)',
94
+ 'Bash(du:*)',
95
+ 'Bash(basename:*)',
96
+ 'Bash(dirname:*)',
97
+ 'Bash(realpath:*)',
85
98
  ]);
86
99
 
87
100
  // Baked frozen copy of velocity-profile.mjs RUNTIME_RESIDUAL_FORMS (the documented residual a
@@ -16,6 +16,7 @@ Run **`node ${CLAUDE_SKILL_DIR}/tools/set-recipe.mjs [--set <activity>.<slot>=<r
16
16
  1. **Grammar — always fully-qualified `<activity>.<slot>`** (the kit never guesses the activity; a bare `review=council` is rejected). `review` accepts `solo|reviewed|council`; `execute` accepts `solo|delegated`. Activities/slots: `plan-authoring.review`, `plan-execution.execute`, `plan-execution.review`.
17
17
  2. **Preview by default** — prints `current → proposed` for the **changed** slots only, plus the **effective recipe resolved against live backend readiness** (degradation stated honestly, e.g. *council requested, 1 ready reviewer → runs reviewed until a 2nd backend is ready*). It writes **nothing**. Re-run with **`--write`** to apply (same effective/degradation note — a direct `--write` is never quieter than the preview). `--unset` returns a slot to its computed default (reverting needs no hand-edit either). A no-op `--set` (slot already equals) writes nothing and never re-seeds the onboarding note.
18
18
  3. **`--write`** applies via a hardened, atomic write (deployment-gated — refuses to scatter a config into a repo with no `docs/ai`; exclusive-create temp + rename; symlink/TOCTOU-safe; last-writer-wins). It preserves the onboarding note + every untouched slot, normalizing to canonical 2-space JSON.
19
+ **Standing-consent advisory (after a successful `--write` only, and only when the written config names a `reviewed`/`council` recipe — a solo recipe gets NO advisory):** advise the ONE-TIME **hand-adds** to the maintainer's `.claude/settings.local.json` — `Bash(codex-review:*)` + `Bash(agy-review:*)` + `Bash(node <skill-dir>/tools/grounding.mjs:*)`, with the path your project actually reaches the kit by. State plainly: (a) auto-approving a review wrapper **spends subscription quota without a per-run prompt** — that is exactly what standing consent means here, so it is the maintainer's call; (b) the kit **never writes that file** — these stay hand-adds; (c) `grounding.mjs --out` writes ONE scratch facts file — that write is what the standing consent covers; (d) the entry must match the invocation **byte-form your project actually uses, INCLUDING quoting** — the procedures advisor renders a QUOTED skill path, and a hand-add covering it must use the same spelling (a mismatched spelling is a dead rule that simply prompts).
19
20
  4. **Exit codes:** `0` success (an explicit recipe that gracefully degrades is still `0`); `2` usage (a bare/duplicate op, or `--write` with no ops); `1` config error (malformed/unreadable config — the file is left **untouched**, never clobbered) or a write STOP (no deployment / a symlinked config). A `1`/`2` failure is loud; on a malformed config, offer to show the parse error so you can help the user fix the JSON.
20
21
 
21
22
  Output is **English/structured** — **localize it to the user's conversational language** when you narrate. Surface the effective-recipe/degradation note plainly.
@@ -4,15 +4,30 @@ The opt-in onboarding **velocity profile** — it seeds a fixed, audited **read-
4
4
 
5
5
  **Version-status routing (like the other writer modes):** read `docs/ai/.workflow-version` first — not-deployed → bootstrap; stamp < `1.3.0` → `upgrade`; stamp > head / unparseable → STOP. The tool enforces this in code too (`--apply` STOPs unless the stamp is the lineage head).
6
6
 
7
- Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs [--dry-run | --apply] [--accept-edits] [--cwd <dir>]`:
7
+ Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs [--dry-run | --apply] [--kit-tools] [--accept-edits] [--cwd <dir>]`:
8
8
 
9
9
  1. **`--dry-run` first, always** (the default — changes nothing). It prints: the fixed read-only core it would add; a **read-only advisory** that lists your `package.json` `scripts` as **unaudited candidates you may add BY HAND** (inspect each first) to `.claude/settings.json` / `settings.local.json` — the tool **never** writes them and flags obviously-mutating names as "do not add"; any **pre-existing non-read-only `Bash(...)` entries** to consider removing by hand; and the honest residual notice (below). It STOPs (zero writes) on a symlinked `.claude` / non-regular `settings.json`, malformed settings JSON, or an unsafe `permissions.defaultMode` — `bypassPermissions` or anything outside `default`/`acceptEdits`/`plan`, present in **either** `settings.json` or `settings.local.json`.
10
- 2. **Ask the `acceptEdits` opt-in** via **`AskUserQuestion` where supported**, the safe option FIRST:
10
+ 2. **Ask the opt-ins** via **`AskUserQuestion` where supported** — two separate choices, the safe option FIRST in each. First the **`--kit-tools` tier** (separate from `acceptEdits`; **"core only" remains the default answer** — seed the tier only on an explicit yes, then add `--kit-tools` to the `--apply` re-run). Then the `acceptEdits` opt-in:
11
11
  - **"Keep per-edit approval prompts (recommended)"** — seed only the read-only allowlist; file edits still prompt.
12
- - **"Auto-accept file edits (`defaultMode: acceptEdits`)"** — present the honest FULL posture: it auto-applies Edit/Write AND auto-runs `mkdir`/`touch`/`mv`/`cp` in the working dir, is paired with the read-only allowlist, and — stated plainly — a settings-level allow rule is a **trust posture, not a sandbox**: a read-only entry can still write a file via output redirection, and (Claude Code's allow rules do not inspect command substitution) could in principle run another command via `cmd $(…)`. velocity **never adds `commit`/`push`/`publish` as allow rules** — so a direct `git push` still ASKs — and the residual guard for the seeded core ships as the opt-in PreToolUse hook (`${CLAUDE_SKILL_DIR}/references/modes/hook.md`; probe-proven on the `--output` family — current engine builds already intercept redirection/substitution upstream). Note also that a `defaultMode` in `settings.local.json` would override this project-level write (local > project), since velocity writes only `.claude/settings.json`.
12
+ - **"Auto-accept file edits (`defaultMode: acceptEdits`)"** — present the honest FULL posture: it auto-applies Edit/Write AND auto-runs `mkdir`/`touch`/`mv`/`cp` in the working dir, is paired with the read-only allowlist, and — stated plainly — a settings-level allow rule is a **trust posture, not a sandbox**: a read-only entry can still write a file via output redirection, and (Claude Code's allow rules do not inspect command substitution) could in principle run another command via `cmd $(…)`. velocity **never adds `commit`/`push`/`publish` as allow rules** — so a direct `git push` still ASKs — and the residual guard for the seeded core ships as the opt-in PreToolUse hook (`${CLAUDE_SKILL_DIR}/references/modes/hook.md`; probe-proven on the `--output` family — current engine builds already intercept redirection/substitution upstream). The approval **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. Note also that a `defaultMode` in `settings.local.json` would override this project-level write (local > project), since velocity writes only `.claude/settings.json`.
13
13
  3. **Only on an explicit yes**, re-run with `--apply` (add `--accept-edits` only if they chose the second option). It merges-don't-clobber (preserves `includeCoAuthoredBy`, every key, and existing allow entries) and writes **only** `.claude/settings.json`.
14
14
  4. **Surface delegation-readiness, read-only.** If they want a step run Delegated, set it with `/agent-workflow-kit set-recipe --set plan-execution.execute=delegated` (`${CLAUDE_SKILL_DIR}/references/modes/set-recipe.md`) or by hand-editing `docs/ai/orchestration.json`; **velocity itself never writes the orchestration config.**
15
15
 
16
+ **The `--kit-tools` tier (opt-in).** On top of the read-only core, `--kit-tools` seeds an audited kit-tool tier — every path **resolved absolute from the running skill at seed time**, so the posture is **fail-safe**: a moved or reinstalled skill leaves a stale rule that simply **prompts again** (re-run velocity to reseed; nothing widens silently). A seeded entry equals its covered dispatch line below with `${CLAUDE_SKILL_DIR}` replaced by the resolved skill dir and `${PROJECT_ROOT}` by this project's resolved root, **UNQUOTED** — a quoted path is a different byte string and will still prompt (a dead rule, not a hole). The covered dispatch lines (this list IS the documented-invocation source):
17
+
18
+ - `node ${CLAUDE_SKILL_DIR}/tools/recipes.mjs --active-line` — and any other args (wildcard)
19
+ - `node ${CLAUDE_SKILL_DIR}/tools/procedures.mjs <activity>` (wildcard)
20
+ - `node ${CLAUDE_SKILL_DIR}/tools/family-registry.mjs --json` (wildcard)
21
+ - `node ${CLAUDE_SKILL_DIR}/tools/detect-backends.mjs` (wildcard)
22
+ - `node ${CLAUDE_SKILL_DIR}/tools/commands.mjs` (wildcard)
23
+ - `node ${CLAUDE_SKILL_DIR}/tools/review-state.mjs --check` (wildcard)
24
+ - `node ${CLAUDE_SKILL_DIR}/tools/manifest/validate.mjs --strict <skill-dir>` (wildcard)
25
+ - `node ${CLAUDE_SKILL_DIR}/tools/release-scan.mjs <path>` (wildcard)
26
+ - `node ${CLAUDE_SKILL_DIR}/tools/run-gates.mjs --cwd ${PROJECT_ROOT}` — **EXACT byte-string only**, and honestly **project-exec, not read-only**: it runs YOUR declared `docs/ai/gates.json` commands — the same trust boundary the opt-in hook grants byte-exact per-cmd. A wildcard would be BROADER than that boundary (`--cwd <dir>` executes another project's declared gates), so the bare cwd-defaulting form, any other `--cwd`, and `--only` forms all still prompt.
27
+ - Writer previews, **exact arg-free dry-run byte-strings only** (for these three writers the arg-free default IS the preview): `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs` · `node ${CLAUDE_SKILL_DIR}/tools/cheap-agents.mjs` · `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs` — every `--apply`/`--write`/`--yes` still prompts, always.
28
+
29
+ Honesty notes: tier entries get **NO PreToolUse-hook residual coverage** — the opt-in hook's residual ask-net guards only the seeded read-only CORE prefixes, so the tier rides the same settings-level residual posture as the core (redirection / command substitution are not inspectable at the settings layer; see the residual notice). A skill or project path that cannot survive UNQUOTED in a byte-exact rule (spaces, metacharacters, non-POSIX) **STOPs the tier up front with a clear error** — nothing is seeded. Anything you want covered beyond the tier — such paths, this repo's own relative-path spellings, other tools — stays a **BY-HAND add** to your settings, with the path your project actually reaches the kit by. Pre-existing `node …` allow entries that do NOT match the seeded tier byte-forms stay flagged by the advisory for hand review.
30
+
16
31
  **Invariants:** creates `.claude/` if absent and writes **only** `.claude/settings.json` (no other file); **never** allowlists commit/push/publish; **never** writes `settings.local.json`; never commits; opt-in `acceptEdits`, never silent.
17
32
 
18
33
  **Exit codes:** `0` done / dry-run; `1` a precondition STOP (stamp not current, unsafe mode, malformed settings, symlinked `.claude` / non-regular target); `2` bad arguments.
@@ -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
- const collectPreExistingNonReadonly = (sources) =>
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((entry) => !screenAllowlistEntry(entry))
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 cmd = getBashAllowCommand(pattern);
392
- if (!cmd) return false;
393
- if (hasShellMetacharacter(cmd)) return false;
394
-
395
- const tokens = tokenizeCommand(cmd);
396
- if (tokens[0] === GIT_COMMAND) return GIT_READONLY_SUBCOMMANDS.includes(getSubcommand(tokens));
397
- if (tokens[0] === NPM_COMMAND) return NPM_READONLY_SUBCOMMANDS.includes(getSubcommand(tokens));
398
- return isSingleShellToken(cmd, tokens) && SHELL_READONLY.includes(cmd);
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 read-only core: every entry
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 UNIVERSAL_READONLY_ALLOWLIST (a guard against the constant drifting). Pure; owns no exit
411
- * codes a CLI maps the typed `.code` to a process exit.
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 (!UNIVERSAL_READONLY_ALLOWLIST.includes(entry)) {
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
- const preExistingNonReadonly = collectPreExistingNonReadonly([
484
- { source: SETTINGS_FILE, data: projectSettings.data },
485
- { source: SETTINGS_LOCAL_FILE, data: localSettings.data },
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
- return { toAdd, alreadyPresent, setsDefaultMode: acceptEdits === true };
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));