@jawache/flow 0.1.2 → 0.3.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.
@@ -0,0 +1,289 @@
1
+ ---
2
+ summary: "upgrade to 0.3.0 from 0.2.3"
3
+ ---
4
+
5
+ # Upgrade to 0.3.0
6
+
7
+ [All guides](./index.md)
8
+
9
+ 0.3.0 parses each shell command as bash before it judges it, and loads a config more strictly. Most repos re-run `flow init`, add one line to `.githooks/pre-commit`, and run `flow test`. A repo with its own command rules or its own checks may need more of section 2.
10
+
11
+ - **Command rules.** A `banCommands` entry, a `substitutionInProse` head and a `justfile` pack `recipes` key each name a command in words, such as `"git push --force"`, not a regex. Each is compared with the program and arguments of one command at a time. `matchHeredocs` is removed.
12
+ - **The parse.** A check of your own reads it as `ctx.commands`. The old `tokenizeCommand` and `gitInvocations` are removed.
13
+ - **The load.** A guardrail with no block case, an empty `.on()` or `.ignore()`, and a breadcrumb with both `.text(…)` and `.file(…)` no longer load.
14
+ - **The hooks and the gate.** They run the repo's own `@jawache/flow` package first, then the `flow` on the `PATH`.
15
+ - **Uninstall.** `flow uninstall` takes flow's wiring off a repo, and the `flow` pack refuses it when an agent runs it.
16
+ - **A subagent's hand-back.** `flow init` registers Claude Code's `SubagentStop` hook, so a turn-end rule runs when a subagent hands back. A breadcrumb bound at turn-end is now shown.
17
+
18
+ ## 1 · Install 0.3.0
19
+
20
+ ```sh
21
+ npm i -D @jawache/flow@0.3.0 # when flow is a dev dependency of the repo
22
+ npm i -g @jawache/flow@0.3.0 # when `flow` is installed globally
23
+ ```
24
+
25
+ With a dev dependency, type each `flow` command on this page as `npx flow`.
26
+
27
+ ## 2 · Required changes
28
+
29
+ ### 2.1 · Re-run `flow init`
30
+
31
+ **Applies if:** the Claude Code settings file has a hook whose command is the bare `flow hook <event>`. Run `grep -n '"flow hook ' .claude/settings.json ~/.claude/settings.json`; a line printed means yes. With flow as a dev dependency and no `flow` on the `PATH`, `flow status` also prints a red `binary` line naming the hooks.
32
+
33
+ **Change:** run `flow init`. It never writes over `flow.config.ts` or `.githooks/pre-commit`. It rewrites each bare registration in place to the command that runs the repo's own package first:
34
+
35
+ ```sh
36
+ f="$CLAUDE_PROJECT_DIR/node_modules/@jawache/flow/dist/flow.mjs"; [ -f "$f" ] && exec node "$f" hook <event>; exec flow hook <event>
37
+ ```
38
+
39
+ When the repo's `package.json` lists `@jawache/flow`, the hooks now go in the repo's own `.claude/settings.json`, and `flow init` rewrites the bare commands in `~/.claude/settings.json` too, so each hook runs once. Commit the repo's `.claude/settings.json`.
40
+
41
+ **Why:** the bare command runs only the `flow` on the `PATH`, so a repo with flow only as a dev dependency answers no hook.
42
+
43
+ ### 2.2 · Re-run `flow init` to register `SubagentStop`
44
+
45
+ **Applies if:** `flow status` prints a red `hooks` line that says ``does not call flow for SubagentStop — re-run `flow init` ``. An init before 0.3.0 wrote five registrations, and `SubagentStop` was not one.
46
+
47
+ **Change:** run `flow init`. It adds the one registration and keeps every other key in the settings file:
48
+
49
+ ```
50
+ + ~/.claude/settings.json — SubagentStop
51
+ ```
52
+
53
+ The new entry runs the same command as the other five, with `subagent-stop` as the event. With flow as a dev dependency, the file is the repo's `.claude/settings.json`: commit it.
54
+
55
+ **Why:** Claude Code sends `Stop` only when the main chat hands back. A subagent's hand-back sends `SubagentStop`, and without it no turn-end rule runs for a subagent.
56
+
57
+ ### 2.3 · Add the repo-first line to `.githooks/pre-commit`
58
+
59
+ **Applies if:** `grep -n 'node_modules/@jawache/flow/dist/flow.mjs' .githooks/pre-commit` prints nothing. `flow init` does not change an existing pre-commit hook.
60
+
61
+ **Change:** add this line above `exec flow hook commit`:
62
+
63
+ ```sh
64
+ f="$PWD/node_modules/@jawache/flow/dist/flow.mjs"; [ -f "$f" ] && exec node "$f" hook commit
65
+ ```
66
+
67
+ The end of the hook then reads:
68
+
69
+ ```sh
70
+ f="$PWD/node_modules/@jawache/flow/dist/flow.mjs"; [ -f "$f" ] && exec node "$f" hook commit
71
+ exec flow hook commit
72
+ ```
73
+
74
+ **Why:** without it, the gate runs only the `flow` on the `PATH`. A repo with flow only as a dev dependency then refuses every commit with `flow: not found`, and a repo with both runs the global version instead of the one in its lockfile.
75
+
76
+ ### 2.4 · Give every guardrail a block case
77
+
78
+ **Applies if:** `flow status` reports a `no-block-cases` refusal. A guardrail whose `.test(…)` holds only `pass` cases, or no cases at all, no longer loads. In 0.2.3, a guardrail with no cases was refused as `no-cases`, and one with pass cases alone loaded.
79
+
80
+ **Change:** add one input the rule must refuse to `.test({ block: [...] })`:
81
+
82
+ ```ts
83
+ // before
84
+ .test({ pass: ["drizzle-kit generate"] })
85
+ // after
86
+ .test({ pass: ["drizzle-kit generate"], block: ["drizzle-kit drop"] })
87
+ ```
88
+
89
+ **Why:** a rule with no block case has never been seen to refuse anything, so nothing proves it catches what it is for.
90
+
91
+ ### 2.5 · Remove an empty `.on()` or `.ignore()`, and choose `.text(…)` or `.file(…)`
92
+
93
+ **Applies if:** `flow status` reports `empty-scope` or `double-prose`. `empty-scope` is an `.on()` or `.ignore()` given no globs, on an entry or an override, including a spread list that turns out empty, such as `.on(...files)`. `double-prose` is a breadcrumb, or an override of one, that says both `.text(…)` and `.file(…)`.
94
+
95
+ **Change:** for `empty-scope`, delete the call, or give it at least one glob. For `double-prose`, keep one of the two. In 0.2.3, `.text(…)` was the one shown.
96
+
97
+ **Why:** an empty `.on()` scoped the entry to nothing, so it never fired; and a breadcrumb shows one prose, so the second was ignored.
98
+
99
+ ### 2.6 · Rename `no-cases` to `no-block-cases` in code that reads refusal codes
100
+
101
+ **Applies if:** `git grep -n "no-cases"` prints a line outside `node_modules`.
102
+
103
+ **Change:** `"no-cases"` → `"no-block-cases"`. `REFUSAL_CODES` also gains `empty-scope` and `double-prose`.
104
+
105
+ **Why:** the refusal now covers a guardrail whose cases are all pass cases, not only one with no cases.
106
+
107
+ ### 2.7 · Rewrite each `banCommands` entry in words
108
+
109
+ **Applies if:** `git grep -n -A3 "banCommands(" -- ':!node_modules'` prints a rule without `wholeLine: true`. Each entry of that rule is now a command in words, not a regex. Two signs show an entry still written as a regex:
110
+
111
+ - **It refuses to load.** An entry with a backslash, or one that is not one command, such as `(git|hg) push`, stops the config loading. `flow status` and `flow test` print `bad-check-options`.
112
+ - **It loads and matches nothing.** An entry such as `git push.*--force`, `^git push` or `--no-verify` is read as a program and its words. `flow test` fails its block case with `expected a block and the check passed it`.
113
+
114
+ A refused entry prints:
115
+
116
+ ```
117
+ flow: the config will not load — 1 refusal.
118
+ bad-check-options: `local.noForcePush` bans `git\s+push\b.*--force`, which is not one command in words. Name a program and the arguments that make it dangerous, such as "git push --force"; a regex goes under `wholeLine: true`.
119
+ ```
120
+
121
+ **Change:** write each entry as the program, then the arguments that make the command dangerous, each a whole word. [banCommands](../checks/ban-commands.md#how-a-command-is-matched) gives the matching rules.
122
+
123
+ | 0.2.3 pattern | 0.3.0 entries |
124
+ | --- | --- |
125
+ | `(?:^\|[\n;&\|(]\s*)\s*work\s+plan\s+tick\b` | `"work plan tick"` |
126
+ | `\bnpx\s+vitest\b` | `"npx vitest"` |
127
+ | `git\s+push\b.*\s(--force\|-f)(\s\|$)` | `"git push --force"`, `"git push -f"` |
128
+ | `rm\s+-(rf\|fr)\b` | `"rm -rf"` |
129
+ | `(?:drizzle-kit\s+drop\|wrangler\s+d1\s+delete)` | `"drizzle-kit drop"`, `"wrangler d1 delete"` |
130
+
131
+ ```ts
132
+ // before
133
+ .check(banCommands({ ban: ["git\\s+push\\b.*\\s(--force|-f)(\\s|$)"] }))
134
+ // after
135
+ .check(banCommands({ ban: ["git push --force", "git push -f"] }))
136
+ ```
137
+
138
+ - **One entry per alternative.** A pattern with `(a|b)` becomes one entry for each.
139
+ - **Drop what stood around the words.** Separators such as `(?:^|[\n;&|]\s*)`, and `\b`, `\s+` and `.*`, go. The parse finds where each command starts, and each word is compared whole.
140
+ - **One entry for every order.** `"rm -rf"` also refuses `rm -fr build` and `rm -r -f build`, so a pattern listing each order becomes one entry.
141
+ - **No wrapper or assignment in front.** `"sudo rm -rf"` and `"FOO=1 rm -rf"` refuse to load. Write `"rm -rf"`: the parse steps past `sudo`, `env`, `nohup`, `nice`, `time`, `timeout`, `command`, `exec`, `xargs` and a `FOO=1` assignment to the program that runs.
142
+ - **A pattern that spans two commands on purpose** keeps its regex, under `wholeLine: true`. The pattern is then tried against the whole line, less its heredoc bodies:
143
+
144
+ ```ts
145
+ .check(banCommands({ ban: ["curl[^\\n]*\\|\\s*sh\\b"], wholeLine: true }))
146
+ ```
147
+
148
+ - **A pattern about a flag's value or about text**, such as a word inside a commit message, is not a command in words. Write a check of your own that reads [`ctx.commands`](../reference/ctx.md#ctxcommands). [How to write your own check](../how-to/write-your-own-check.md) gives the steps.
149
+
150
+ **Why:** a regex over a command's text read `/usr/local/bin/work plan tick` as no tick, `rm -fr` as no `-rf`, and `work task-list` as `work task`. An entry in words is compared with the program and the arguments the parse found.
151
+
152
+ ### 2.8 · Remove `matchHeredocs`
153
+
154
+ **Applies if:** `git grep -n matchHeredocs -- ':!node_modules'` prints a line.
155
+
156
+ **Change:** delete the key from the `banCommands` options. `tsc` reports it as an unknown property, and flow ignores it at run time. A heredoc's body is never matched by `banCommands`, unless a shell reads it as commands. A rule that read a heredoc's body is about text:
157
+
158
+ - **A backtick in prose,** built with `substitutionInProse(heads)`, becomes the stock check (2.9).
159
+ - **A commit's message,** such as one fed by `git commit -F - <<'EOF'`, is read by `commitMessage(ctx)`.
160
+ - **Any other body** is in the `heredocs` of each command in [`ctx.commands`](../reference/ctx.md#ctxcommands), as `{ body, quoted }`. Read it in a check of your own.
161
+
162
+ **Why:** an entry in words is a program and its arguments, and a heredoc's body is neither.
163
+
164
+ ### 2.9 · Rewrite each `substitutionInProse` head in words
165
+
166
+ **Applies if:** `git grep -n substitutionInProse -- ':!node_modules'` prints a line in your config or your own packs.
167
+
168
+ **Change:** bind `substitutionInProse` as a stock check, and write each head as a command in words. A head is matched the way a `banCommands` entry is:
169
+
170
+ ```ts
171
+ // before
172
+ .check(banCommands({ ban: [substitutionInProse(["notes\\s+add\\b"])], matchHeredocs: true }))
173
+ // after
174
+ .check(substitutionInProse({ heads: ["notes add"] }))
175
+ ```
176
+
177
+ - **A git subcommand,** such as `commit`, goes in `gitSubcommands: ["commit"]`. It is matched after git's own options, so `git -C '/a b' commit` is `commit`.
178
+ - **A head that is not one command in words** stops the config loading:
179
+
180
+ ```
181
+ flow: the config will not load — 1 refusal.
182
+ bad-check-options: `local.noBacktickInNotes` names the head `notes\s+add\b`, which is not one command in words. Name a program and the arguments that make it record prose, such as "gh pr".
183
+ ```
184
+
185
+ [substitutionInProse](../checks/substitution-in-prose.md) lists what is refused and what passes.
186
+
187
+ **Why:** a head names which command records prose, and that is a program and its arguments, read from the parse as `banCommands` reads it.
188
+
189
+ ### 2.10 · Rewrite the `justfile` pack's `recipes` keys in words
190
+
191
+ **Applies if:** `git grep -n -A4 "pack(justfile" -- ':!node_modules'` shows a `recipes` map.
192
+
193
+ **Change:** write each key as a `banCommands` entry in words (2.7):
194
+
195
+ ```ts
196
+ // before
197
+ pack(justfile, { exempt: [], recipes: { "npx vitest\\b": "just test" } }),
198
+ // after
199
+ pack(justfile, { exempt: [], recipes: { "npx vitest": "just test" } }),
200
+ ```
201
+
202
+ `"npx vitest"` refuses `npx vitest run` and `npx --yes vitest`, and passes `npx vitest-preview`. A key still written as a regex stops the config loading:
203
+
204
+ ```
205
+ flow: the config will not load — 1 refusal.
206
+ bad-check-options: `justfile.useTheRecipe` bans `npx vitest\b`, which is not one command in words. Name a program and the arguments that make it dangerous, such as "git push --force"; a regex goes under `wholeLine: true`.
207
+ ```
208
+
209
+ **Why:** `justfile.useTheRecipe` passes the keys to `banCommands` as its entries.
210
+
211
+ ### 2.11 · Replace the removed command readers
212
+
213
+ **Applies if:** `git grep -n -E "tokenizeCommand|gitInvocations|GitInvocation|substitutionInProse\(\[|commitMessage\(ctx\.command|gitDirPrefix\(ctx\.command" -- ':!node_modules'` prints a line.
214
+
215
+ **Change:**
216
+
217
+ | 0.2.3 | 0.3.0 |
218
+ | --- | --- |
219
+ | `tokenizeCommand(ctx.command)` | `ctx.commands`: one entry per command, each with `program`, `args`, `text`, `heredocs`, `substitutions`, `redirects` and `nested`, or `null` when the line does not parse. See [ctx.commands](../reference/ctx.md#ctxcommands) |
220
+ | `gitInvocations(ctx.command)`, `GitInvocation` | `gitCommands(ctx)`, `GitCommand`. Each has `globals`, `subcommand` and `args` as before, plus `command`, the parsed command it was read from, and `argsAt`, where `args` starts in `command.args`. A line that does not parse gives `[]`, not `null` |
221
+ | `commitMessage(ctx.command)` | `commitMessage(ctx)` |
222
+ | `gitDirPrefix(ctx.command)` | `gitDirPrefix(ctx)` |
223
+ | `banCommands({ ban: [substitutionInProse(heads)], matchHeredocs: true })` | `substitutionInProse({ heads })`, a stock check, with each head in words (2.9) |
224
+
225
+ `commitMessage` and `gitDirPrefix` take any object with `command` and, optionally, `commands`, so a ctx passes as it is. `shellCommands(ctx)` returns `ctx.commands`, or parses `ctx.command` when the ctx has no `commands`. `ShellCommand`, `ShellHeredoc`, `ShellSubstitution` and `ShellRedirect` are exported as types.
226
+
227
+ `programName(c.program)` is new. It returns a program's name without its path: `/usr/bin/git` gives `git`.
228
+
229
+ **Why:** every reading of a command now goes through the one parse, and the hand-written tokeniser behind these functions is gone.
230
+
231
+ ### 2.12 · Stop reading `description` in a category
232
+
233
+ **Applies if:** a `defineCategory` classifier in the repo reads `description` from its facts. `git grep -n defineCategory` finds each classifier.
234
+
235
+ **Change:** read `facts.agentType` instead. `description` is still on `SessionFacts`, so a config that names it compiles, but it is always `undefined`.
236
+
237
+ **Why:** the description came from a file Claude Code does not write. A subagent's type now comes from the hook message.
238
+
239
+ ## 3 · What now blocks or passes differently
240
+
241
+ - **A command that only mentions a banned one passes.** `echo 'npx vitest run'`, `gh issue create --body "ran npx vitest run"`, and a heredoc body that names a command are no longer refused. `git push origin main; ls -f` no longer reads as a force push.
242
+ - **A banned command in a nested command is refused.** A command inside `$( … )`, backticks or `<( … )`, in a `bash -c`, `sh -c` or `eval` string, or behind `sudo`, `env` or `timeout`, is matched as a command of its own.
243
+ - **A here-string fed to a shell is matched as commands.** `bash <<< 'rm -rf build'` is refused by a rule that bans `rm -rf`, as `bash <<'EOF'` with the same line already was.
244
+ - **A program run by its path is refused.** A command rule in words compares the program without its path. `/usr/local/bin/work plan tick F7.B1` is refused by `work.ticksAreTheChilds`, and `/usr/local/bin/npx vitest run` by a `justfile` `recipes` key of `"npx vitest"`.
245
+ - **An argument is compared whole.** `work task-list` passes `work.planIsTheParents`, which bans `work task`. `npx vitest-preview` passes a `recipes` key of `"npx vitest"`.
246
+ - **A flag in any order is refused.** The arguments of an entry match in any order and at any position. `work --verbose plan tick F7.B1` is refused by `work.ticksAreTheChilds`. An entry of `"rm -rf"` refuses `rm -fr build` and `rm -r -f build`. If one of these is now refused, the rule's message says what to run instead.
247
+ - **`git.noForcePush` refuses every spelling of a force-push.** `git -C <dir> push --force`, `/usr/bin/git push -f`, `git push -fu` and `git push origin +main` are refused.
248
+ - **`git.noShellSubstitutionInProse` and `work.noShellSubstitutionInProse` read the parse.** They now pass a backtick in a quoted heredoc, such as `gh pr create --body-file - <<'EOF'`, a backslash-escaped backtick inside double quotes, and a backtick in another command on the same line. They now refuse a bare backtick outside quotes and a backtick in an unquoted heredoc. If one is refused, single-quote the argument, escape each backtick as `` \` ``, or feed the text through a quoted heredoc.
249
+ - **`git.conventionalCommitFormat` reads the commit's own message.** For `git commit -F - <<'EOF'`, it reads that command's heredoc, not the first heredoc on the line. For `-m "$(cat <<'EOF' … EOF)"`, it reads the heredoc's body.
250
+ - **`git.noGateSkip` is new in the `git` pack.** It refuses `git commit --no-verify` (and `-n`), `git -c core.hooksPath=… commit`, `git commit-tree`, `git update-ref` and `git hash-object -w`, which commit without the commit gate. If one is refused, commit normally and fix what the gate refuses. A repo whose workflow needs one of them disables the entry in `flow.config.ts`.
251
+ - **Deletes and reads are read from the parse.** An `rm` inside `$( … )`, a `bash -c` string or behind `sudo` is judged as a delete before it runs; an `rm` named in a quoted argument or a heredoc body is not. The files a command reads, for the breadcrumbs they show, are found the same way.
252
+ - **A config that does not load turns flow off, and says so.** In 0.2.3, every write, delete and command was refused until the config was fixed. Now every hook lets the call through and prints the load error with it, and only the commit gate refuses, until the config loads.
253
+ - **A subagent is classified.** Rules scoped to a category that matches on `agentType` now fire in a real subagent. In 0.2.3, no subagent had a category.
254
+ - **A turn-end rule runs when a subagent hands back.** A rule with no `.for(…)` now holds a subagent as it holds the main chat, and a rule with `.for(…)` runs only for an agent that has one of the categories it names. Claude Code's internal agents, which make prompt suggestions and answer `/btw` side questions, run no rule. See [the turn-end moment](../reference/moments.md#the-turn-end-moment).
255
+ - **A turn-end breadcrumb is shown.** In 0.2.3, a breadcrumb bound at `turn-end` loaded and was never shown. Now it is shown as a note when an agent hands back, the first time and again once the context size has grown by `driftTokens`. Each time it shows, the agent takes one more turn to read it before it hands back.
256
+ - **`flow init` leaves other hooks alone, and exits 1 until flow is installed.** When git already runs hooks from another folder, such as husky's `.husky/_` or a hook in `.git/hooks`, `flow init` does not set `core.hooksPath`. It exits 1, and its output ends with `FLOW IS NOT INSTALLED`, until the `pre-commit` hook git runs calls flow. In 0.2.3, it set `core.hooksPath` to `.githooks` and turned those hooks off. A setup script that runs `flow init && …` in such a repo now stops at init. In a husky repo where an older `flow init` set `core.hooksPath`, husky sets it back to `.husky/_` on its next install, and `flow status` then prints a red `hooks-path` line. See [when git already runs other hooks](../reference/cli.md#when-git-already-runs-other-hooks).
257
+ - **`flow.noUninstall` is new in the `flow` pack.** It refuses `flow uninstall`, with or without `--machine`, when an agent runs it, by any runner such as `npx flow uninstall` or `node node_modules/@jawache/flow/dist/flow.mjs uninstall`. If flow should come off a repo, run `flow uninstall` in your own terminal. See [what `flow uninstall` removes](../reference/cli.md#what-flow-uninstall-removes).
258
+ - **A `wholeLine` entry that is not a valid regex refuses to load.** `banCommands` compiles each `wholeLine` pattern when the rule loads, and `flow status` prints `bad-check-options` for one that does not compile, such as `curl(`. In 0.2.3, it threw on the first command the rule judged.
259
+ - **A `pre-commit` hook git cannot run does not count as calling flow.** When git runs its hooks from a folder other than `.githooks`, and the `pre-commit` hook there calls flow but is not executable, `flow init` and `flow status` print `FLOW IS NOT INSTALLED`, and the block says to run `chmod +x` on it. A hook `flow init` wrote is executable already.
260
+ - **A file git puts in place is not judged.** After `git switch`, `git checkout` or `git pull`, a file that matches the commit `HEAD` points to is not judged and not written back, when that commit was in the repo before the call. In 0.2.3, it was judged like an edit.
261
+
262
+ ## 4 · Optional
263
+
264
+ - **Take flow off a repo with `flow uninstall`.** It deletes the commit gate, unsets `core.hooksPath`, deletes `.flow/`, and removes flow's six hook entries from the repo's `.claude/settings.json`. `flow uninstall --machine` removes them from the machine-wide settings file too. `flow.config.ts` is kept, and `flow init` wires the repo again. [What `flow uninstall` removes](../reference/cli.md#what-flow-uninstall-removes) lists every path.
265
+ - **Read the parse in a check of your own.** `ctx.commands` lists each command the line runs. [ctx.commands](../reference/ctx.md#ctxcommands) has the fields. This check compares each program without its path, with `programName`, so `/usr/bin/gh` counts as `gh`:
266
+
267
+ ```ts
268
+ const noBareGh = defineCheck((_opts: Record<string, never>) => (ctx) => {
269
+ const bare = (ctx.commands ?? []).find((c) => programName(c.program) === "gh");
270
+ return bare === undefined ? ctx.ok() : ctx.fail(`\`${bare.text}\` runs gh directly — run it as \`work gh …\``);
271
+ });
272
+ ```
273
+
274
+ - **Bind `substitutionInProse` for a command of your own that records prose,** such as a deploy script that takes a message:
275
+
276
+ ```ts
277
+ .check(substitutionInProse({ heads: ["notes add"] }))
278
+ .message(SUBSTITUTION_MESSAGE)
279
+ ```
280
+
281
+ - **Say where the `secrets` pack's key is kept.** `pack(secrets, { custody: "file" })` for a key in `.env.keys`; the default, `"native"`, is the OS secret store. With `"file"`, the default encrypt command passes `--no-native`.
282
+
283
+ ## 5 · Verify
284
+
285
+ ```sh
286
+ flow status # green
287
+ flow test # every case green
288
+ flow hook commit --all # exit 0
289
+ ```
@@ -0,0 +1,45 @@
1
+ ---
2
+ summary: "one page per release"
3
+ ---
4
+
5
+ # Upgrading flow — migration guides
6
+
7
+ Each page lists what a repo that uses flow must change to upgrade to one version from the version before it. A repo that uses flow has a `flow.config.ts`, a `.githooks/pre-commit` written by `flow init`, and flow's hook registrations in the Claude Code settings file.
8
+
9
+ ## Upgrade across several versions
10
+
11
+ Apply the pages one at a time, oldest first. For each page:
12
+
13
+ 1. Install that page's version.
14
+ 2. Make every change under **Required changes** whose **Applies if** check says yes.
15
+ 3. Read **What now blocks or passes differently**.
16
+ 4. Run the page's **Verify** steps, and fix what they report before you open the next page.
17
+
18
+ Find the version a repo has now with `npm ls @jawache/flow`, or `flow --version` for a global install.
19
+
20
+ From 0.2.2, these pages are in the npm package. After `npm i -D @jawache/flow@<target>`, they are at `node_modules/@jawache/flow/docs/user/migrations/`, and that folder has every guide up to `<target>`. They are also on GitHub, at <https://github.com/jawache/flow/tree/main/docs/user/migrations>.
21
+
22
+ To give the upgrade to an agent working in that repo, use this instruction, with the two versions filled in:
23
+
24
+ ```text
25
+ Upgrade @jawache/flow in this repo from <current> to <target>. Read the migration guides
26
+ index at node_modules/@jawache/flow/docs/user/migrations/index.md (or
27
+ https://github.com/jawache/flow/tree/main/docs/user/migrations), then apply every guide
28
+ after <current> up to and including <target>, one at a time, oldest first, following the
29
+ steps that index lists. Stop and report if a Verify step fails.
30
+ ```
31
+
32
+ ## The guides
33
+
34
+ | Version | Released | Upgrades from | Changes needed |
35
+ | --- | --- | --- | --- |
36
+ | [0.3.0](./0.3.0.md) | 2026-10-17 | 0.2.3 | Re-run `flow init`. Add the repo-first line to `.githooks/pre-commit`. Give every guardrail a block case. Rewrite each `banCommands` entry, `substitutionInProse` head and `justfile` `recipes` key in words. Replace `tokenizeCommand` and `gitInvocations` |
37
+ | [0.2.3](./0.2.3.md) | 2026-10-07 | 0.2.2 | None. Bind the `git` pack if the repo does not, to refuse a commit that credits Claude |
38
+ | [0.2.2](./0.2.2.md) | 2026-10-07 | 0.2.1 | Edit `.githooks/pre-commit`, and any other command that runs `flow commit` |
39
+ | [0.2.1](./0.2.1.md) | 2026-10-02 | 0.2.0 | None |
40
+ | [0.2.0](./0.2.0.md) | 2026-10-01 | 0.1.2 | Re-run `flow init`. Remove `override(docs.noMarkdownInUserDocs)`. Check `banCommands` rules about heredoc text |
41
+ | [0.1.2](./0.1.2.md) | 2026-09-16 | 0.1.1 | Only for a repo with its own `depcruise` rule whose layers are written as regexes |
42
+ | [0.1.1](./0.1.1.md) | 2026-09-16 | 0.1.0 | None |
43
+ | [0.1.0](./0.1.0.md) | 2026-09-15 | a checkout linked with `npm link` | Install from npm. Rename the `guard` pack to `flow`. Replace removed checks and entries |
44
+
45
+ The full list of changes in each release is in [CHANGELOG.md](../../../CHANGELOG.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jawache/flow",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Guardrails and breadcrumbs as code — packs you import, a config that typechecks, one adapter per harness.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,7 +32,8 @@
32
32
  "dist/flow.mjs",
33
33
  "dist/index.mjs",
34
34
  "dist/packs.mjs",
35
- "dist/types/"
35
+ "dist/types/",
36
+ "docs/user/migrations/"
36
37
  ],
37
38
  "engines": {
38
39
  "node": ">=24"
@@ -45,7 +46,8 @@
45
46
  },
46
47
  "dependencies": {
47
48
  "@ast-grep/napi": "^0.42.3",
48
- "dependency-cruiser": "^18.1.0"
49
+ "dependency-cruiser": "^18.1.0",
50
+ "unbash": "5.1.0"
49
51
  },
50
52
  "devDependencies": {
51
53
  "@ast-grep/lang-python": "^0.0.6",