@jawache/flow 0.2.1 → 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.
package/README.md CHANGED
@@ -1,314 +1,89 @@
1
1
  # flow
2
2
 
3
- **Guardrails and breadcrumbs as code.** Your whole guard is one TypeScript file: rules that block
4
- what must not happen, notes that arrive when they are useful, and a compiler that refuses the config
5
- before it ever runs.
3
+ **Guardrails for AI coding agents, written as code.** flow checks every change a coding agent makes to your repo against rules you write in one TypeScript file. When a change breaks a rule, flow refuses it: a banned command never runs, a refused file write is put back, and a refused commit does not land. The agent is told why, in your words. flow works with [Claude Code](https://claude.com/claude-code), and its commit rules run for people too.
6
4
 
7
- Not to be confused with **Flow, the JavaScript type checker** from Meta. No relation, no shared
8
- history, nothing in common but four letters. This flow is a guard for coding agents.
5
+ Instructions in a `CLAUDE.md` file are advice: an agent can misread or skip them, more often as a session grows long. A flow rule runs every time, at the moment it applies, and a refused action does not happen.
9
6
 
10
- Requires **Node >= 24**. Your config is TypeScript and is loaded by node's own type stripping, which
11
- is on by default from 24 (the Active LTS) — there is no build step and there will not be one, because
12
- a guard that compiles its own config is a guard with a build to go wrong. On an older node, flow
13
- tells you the version it needs instead of dying on a syntax error.
7
+ Not to be confused with Flow, Meta's JavaScript type checker. No relation.
14
8
 
15
- ---
9
+ ## Why use flow
16
10
 
17
- ## Why
11
+ - **Every change is checked, whichever tool made it.** flow records the files before each tool call and compares them after. A file written by the Edit tool, a shell command (`sed -i`, a heredoc, a script, a code generator), an MCP tool or a subagent is checked the same way, and a refused file is put back.
12
+ - **A banned command is stopped before it runs.** A force-push, a `git reset --hard`, an `npm install` you did not want: the rule refuses the command, and the agent gets the rule's message instead of the command's output.
13
+ - **Your commit rules hold for everyone.** A git pre-commit hook runs the same rules over the staged changes, whether you, an agent or a merge makes the commit, including files the commit deletes or renames.
14
+ - **Rules are tested code.** Each rule lists the inputs it must let through and the inputs it must refuse. A rule with no refusal case does not load, `flow test` runs every case, and your editor flags a mistyped rule, because the rules are TypeScript.
15
+ - **A broken guard is never silent.** If the config does not load, every guardrail and breadcrumb is off, and the agent is given the load error with every write, delete and command. The commit gate refuses every commit until the config loads.
16
+ - **Notes arrive when they matter.** A breadcrumb is a note shown to the agent the first time it reads or changes a file in a given area, instead of one long instruction file read once at the start.
17
+ - **Rules can depend on who is acting.** A rule can apply only to subagents, or let only one named agent write a folder. flow identifies an agent from what Claude Code records about it, never from what the agent says about itself.
18
+ - **Ten packs come with it.** Rules for git, node, TypeScript, secrets, the justfile, docs, tests, code layout, agent workflow and the guard itself, each turned on with one line and adjusted with `override(…)`.
18
19
 
19
- A guard is worth having only if it is true. The failure that matters is not a rule that fires when it
20
- should not — you notice that within a minute. It is a rule that reads as armed, is counted as armed,
21
- and matches nothing: a glob for a folder that was renamed, a script that no longer imports, a fence
22
- pointed the wrong way. That config is indistinguishable from a working one until the day it was
23
- supposed to save you.
20
+ ## What a rule looks like
24
21
 
25
- flow's answer is that **nothing defaults and nothing loads quietly**:
22
+ This rule is in the demo config `flow init` writes:
26
23
 
27
- - A rule is written in a closed grammar, so a misspelled verb is a red squiggle in your editor.
28
- - Every rule carries its own cases — what must pass it, and what it must block. A rule with no
29
- block-case does not load. `flow test` runs them all.
30
- - A scope that could never narrow anything is refused, not ignored.
31
- - If the config will not load, **every gated moment refuses**, loudly, until you fix it. A guard
32
- that fails open is a guard that lies about being there.
33
-
34
- ## Install
35
-
36
- In the repo you want guarded, as a dev dependency — so every clone and every CI run gets the same
37
- flow, pinned in your lockfile:
38
-
39
- ```sh
40
- npm i -D @jawache/flow
41
- npx flow init
42
- ```
43
-
44
- Or on your PATH, for a folder with no `package.json` of its own — which is what the five-minute
45
- walkthrough below uses:
46
-
47
- ```sh
48
- npm i -g @jawache/flow
24
+ ```ts
25
+ noForcePush: guardrail()
26
+ .at(command)
27
+ .check(banCommands({ ban: ["git push --force", "git push -f", "git push --force-with-lease"] }))
28
+ .message("Force-pushing rewrites history everyone else has. Push a correcting commit, or ask first.")
29
+ .test({ pass: ["git push origin main"], block: ["git push --force origin main", "git push --force-with-lease origin main"] }),
49
30
  ```
50
31
 
51
- Either way `flow init` does the wiring, and you never link anything by hand. When `@jawache/flow`
52
- does not resolve from the repo — a global install, a bare demo folder — init links the flow that is
53
- running and says so on the line. Install it as a dependency instead and that step is simply absent.
54
-
55
- To work *on* flow rather than with it: clone it, `just install`, `just link` — the binary then runs
56
- from your checkout, undone with `npm unlink -g @jawache/flow`.
32
+ `.at(command)` says when the rule runs: before every shell command. `.check(…)` is what it checks, `.message(…)` is what the agent is told, and `.test(…)` lists the cases `flow test` runs. When an agent tries `git push --force origin main`, the command does not run, and the agent is shown:
57
33
 
58
- ## Five minutes, from nothing
59
-
60
- ```sh
61
- mkdir /tmp/flow-demo && cd /tmp/flow-demo && git init -q
62
- flow init
63
- ```
34
+ ```text
35
+ flow — command blocked before it ran:
64
36
 
65
- That writes:
37
+ ✗ demo.noForcePush · git push --force origin main
38
+ Force-pushing rewrites history everyone else has. Push a correcting commit, or ask first.
39
+ matches banned `git push --force`
66
40
 
67
- ```
68
- flow init — created:
69
- + flow.config.ts
70
- + .githooks/pre-commit
71
- + .flow/ — flow's own state, self-ignoring, never committed
72
- + node_modules/@jawache/flow → …/flow (a link to the flow that is running — nothing here resolved @jawache/flow)
73
- + ~/.claude/settings.json — SessionStart · PreToolUse · PostToolUse · PostToolUseFailure · Stop
74
- + git config core.hooksPath .githooks
75
- next: `flow status` — what is bound, and what is not wired yet.
41
+ Adjust the command, then retry.
76
42
  ```
77
43
 
78
- Six things, and each one is checkable:
44
+ ## Get started
79
45
 
80
- | what | why |
81
- | --- | --- |
82
- | `flow.config.ts` | flow's own `flow` pack plus a demo guard you can read in one screen — the only file that turns anything on |
83
- | `.githooks/pre-commit` | the commit gate. Never written over one you already have |
84
- | `.flow/` | flow's own state and logs. It ignores itself, so your `.gitignore` is untouched |
85
- | `node_modules/@jawache/flow` | a link to the flow that is running, because this folder resolves no `@jawache/flow` of its own. `npm i -D @jawache/flow` and this row disappears |
86
- | the host's `settings.json` | five hook registrations, so a coding agent in any repo asks flow first |
87
- | `core.hooksPath` | what makes git run the gate. It is per-clone, so every fresh checkout runs `flow init` |
88
-
89
- `flow init --empty` gives you the same wiring with no opinions, for a repo that knows what it wants.
90
- Run `flow init` again whenever you like — it adds only what is missing and says so.
91
-
92
- Now look at what you have:
46
+ flow needs Node 24 or later, git, and Claude Code for the session rules. Install it globally, then run it in the repo you want guarded:
93
47
 
94
48
  ```sh
49
+ npm i -g @jawache/flow
50
+ cd my-repo
51
+ flow init
95
52
  flow status
96
53
  ```
97
54
 
98
- ```
99
- flow is ON — 4 guardrails · 2 breadcrumbs, every one resolved and able to fire
100
- config /tmp/flow-demo/flow.config.ts
101
- state /tmp/flow-demo/.flow
102
- session no live session has marked this worktree yet
103
- categories subagent — what the entries below bind to
104
- session
105
- 🍞 flow.orientation
106
- This repo is guarded by flow. Guardrails are rules that refuse a change, and every change to the fi…
107
- touch
108
- 🍞 flow.editingTheGuardrails — on flow.config.ts .claude/settings.json .claude/settings.local.json .claude/agents/** .claude/skills/**
109
- You are editing the guardrails themselves. If this edit weakens, disables or removes a guardrail or…
110
- delete
111
- ✗ flow.noDeleteGuardrails — on flow.config.ts .githooks/pre-commit
112
- That command would delete this repo's guard surface (flow.config.ts binds every rule; the packs it …
113
- command
114
- ✗ demo.noForcePush
115
- Force-pushing rewrites history everyone else has. Push a correcting commit, or ask first.
116
- commit
117
- ✗ demo.noMarkedFiles
118
- A file carrying the do-not-commit marker is staged. Take the marker out, or unstage the file.
119
- ✗ demo.chatCommits — for subagent
120
- A subagent does not commit — hand the change back to the chat that spawned it.
121
- ✓ config: /tmp/flow-demo/flow.config.ts
122
- ✓ commit-gate: .githooks/pre-commit runs `flow commit` over the staged set
123
- ✓ hooks-path: core.hooksPath = .githooks
124
- ✓ hooks: ~/.claude/settings.json — SessionStart · PreToolUse · PostToolUse · PostToolUseFailure · Stop
125
- green — every rule loads, every fitting is in place.
126
- ```
127
-
128
- Both blocks above are the real thing: the suite runs `flow init` and `flow status` in a throwaway
129
- repo and pins them against this file, with only the three paths (this repo, your home, the checkout)
130
- standing in for the ones you will see.
131
-
132
- Green means guarded. Anything not true yet is a red line carrying its own fix, and `flow status`
133
- exits non-zero — so a setup script can just ask.
55
+ To guard one repo without a global install, install flow as a dev dependency instead: `npm i -D @jawache/flow`, then `npx flow init`. Git and Claude Code run the repo's copy when it has one, and the global one otherwise. [How to install flow](./docs/user/how-to/install-flow.md) has the details.
134
56
 
135
- Then prove each rule to yourself, in this order. Each step takes seconds.
57
+ `flow init` writes `flow.config.ts`, a git pre-commit hook and flow's hook registrations for Claude Code. `flow status` lists every rule and checks the wiring: green means every rule loads and is in place. Then follow [Getting started](./docs/user/tutorials/getting-started.md), which takes you from an unguarded repo to a refused commit in five minutes.
136
58
 
137
- **1 — the notes arrive.** Open a coding session in `/tmp/flow-demo` and say hello. The
138
- orientation breadcrumb is injected into the session as context, before the agent does anything.
59
+ ## Documentation
139
60
 
140
- **2 — a banned command is refused.** Ask the agent to force-push the branch. It is stopped
141
- *before the command runs*, with the rule's own sentence, and there is no way around it.
142
-
143
- **3 — a shell edit is refused too.** Ask the agent to add a `TODO` to a guarded file with a
144
- heredoc or `sed -i` instead of the Edit tool. The write goes through, so the file on disk changes.
145
- The same rule then checks it, and flow puts the file back to the content it had before. The refusal
146
- is printed beside the command's own output. Notice that a refused write never stays, whichever tool
147
- made it.
148
-
149
- **4 — the commit gate holds.** Have the agent write a file containing the literal marker
150
- `DO`-`NOT`-`COMMIT` (spelled whole), stage it, and commit. Git refuses at the hook, naming the rule
151
- and the file. Take the marker out and the same commit lands.
152
-
153
- **4 — a rule bound to WHO, not what.** Ask the main chat to make a small commit: it lands. Ask it to
154
- delegate the identical commit to a subagent: refused. Same act, different wearer, different answer —
155
- that is `.for(subagent)`, and the category recognises itself from what the host wrote, never from
156
- anything the session claimed about itself.
157
-
158
- **5 — break it, and watch everything stop.** Delete a `.message(…)` from `flow.config.ts` and save.
159
- Your editor goes red on that line. Ask the agent to edit any file, or to run any command: both are
160
- refused, with the load error on screen. Undo the edit and the next action passes. There is no
161
- notice-and-proceed anywhere.
162
-
163
- The one exception is the repair: while the config is broken, a write to `flow.config.ts` — or to a
164
- pack it imports, whatever you called that folder — still goes through. The surface is read out of
165
- the config's own relative import lines, as text, because the module is the thing that will not load.
166
- Without the exception the rule demanding a fix would also forbid it — you would never notice,
167
- because your editor has no hooks in front of it, but anything that does is locked out of its own
168
- repair. Commands stay refused, and so does the commit gate, so nothing written under the exception
169
- reaches a commit until the config loads green.
170
-
171
- **6 — run every rule's own cases.**
172
-
173
- ```sh
174
- flow test
175
- # flow test — 7 cases over 4 guardrails, all green
176
- ```
177
-
178
- Delete a rule's `.test(…)` block and the config stops loading at all, naming the entry.
179
-
180
- ## Proving the packs themselves
181
-
182
- `flow test` proves the rules in *your* config. One command proves the ten this package ships, and it
183
- is the one to run before you trust a change to any of them:
184
-
185
- ```sh
186
- just test-packs
187
- ```
188
-
189
- It builds the bundles, runs `flow init` in a throwaway repo that has never heard of flow, binds every
190
- shipped pack at once with a stranger's parameters — one shell script for the toolchain, `core/` for
191
- the pure home, `rules/` for the repo's own pack — and then drives the rails: `flow status` green,
192
- each pack's cases run pack by pack, and one real refusal at each of write · delete · command ·
193
- commit · turn-end. It prints what it bound and what refused, and exits non-zero naming the pack the
194
- moment a count moves.
195
-
196
- ## The config
197
-
198
- Open `flow.config.ts`. It is ordinary code, and it is the whole guard:
199
-
200
- ```ts
201
- import { commit, command, defineCategory, defineConfig, definePack, guardrail, pack } from "@jawache/flow";
202
- import { flow } from "@jawache/flow/packs";
203
-
204
- const subagent = defineCategory("subagent", (facts) => facts.subagent);
205
-
206
- export const demo = definePack("demo", {
207
- noForcePush: guardrail()
208
- .at(command)
209
- .check((ctx) => (/--force/.test(ctx.command ?? "") ? ctx.fail("rewrites shared history") : ctx.ok()))
210
- .message("Force-pushing rewrites history everyone else has.")
211
- .test({ pass: ["git push origin main"], block: ["git push --force origin main"] }),
212
- });
213
-
214
- export default defineConfig([pack(flow, { packs: [] }), pack(demo)]);
215
- ```
216
-
217
- Six things to know, and then you can write your own:
218
-
219
- - **A sentence says everything, and defaults nothing.** `.at(…)` when it fires · `.on(…)`/`.ignore(…)`
220
- which files · `.for(…)` which actor · `.check(…)` the question · `.message(…)` what you are told ·
221
- `.test({ pass, block })` what proves it. Say a verb twice and the compiler stops you.
222
- - **A check is a function of `ctx`** and reaches the world only through it — `ctx.file`, `ctx.command`,
223
- `ctx.staged`, `ctx.exec`, `ctx.fs`, `ctx.git`. That is what lets the same check run live, run in a
224
- case, and replay from a recording without a repo.
225
- - **A pack is just code.** `definePack` in a file here, or a package you install: identical shape.
226
- Publishing one is promotion, not a rewrite.
227
- - **Packs come with the install, behind their own door.** `@jawache/flow` is the GRAMMAR a rule is
228
- written in; `@jawache/flow/packs` is the CONTENT — ten opinionated packs (`git` · `justfile` ·
229
- `node` · `secrets` · `typescript` · `fcis` · `tdd` · `docs` · `flow` · `work`), each bound by one
230
- `pack(…)` line, and an unbound one costs nothing. A pack names nothing it does not ship: the
231
- recipe your gate runs is a typed, mandatory parameter, so a config that never says which command
232
- cuts a release does not compile — and `flow`, flow's own self-protection, takes the folder your
233
- own packs live in, so it never guesses at a folder your repo does not have. `flow init` binds
234
- that one for you.
235
- - **A repo bends a pack it did not write** with `override(pack.entry)`, which speaks only what it
236
- changes — a different glob, a different message, or `.disabled("why")`, and the reason is what
237
- `flow status` prints beside it.
238
- - **A category is a name with its recognizer aboard.** It reads host-written evidence — a sidecar the
239
- harness wrote, the brief a spawn was given — and never a claim the session made about itself,
240
- because permissions that rest on a claim rest on a lie.
241
-
242
- ## Moments
243
-
244
- | moment | when | guardrail | breadcrumb |
245
- | --- | --- | --- | --- |
246
- | `session` | a chat starts, resumes, or is compacted | — | ✓ |
247
- | `touch` | a tool call names a file, or a shell command reads a file | — | ✓ |
248
- | `write` | a file is created or changed. A write from the Edit tool is checked before the file changes; a write from a shell command is checked the moment it has changed. A refused write is undone | ✓ | — |
249
- | `delete` | a file is removed. An `rm` is checked before it runs; any other removal is checked the moment the file is gone. A refused delete puts the file back | ✓ | — |
250
- | `command` | a shell command is about to run | ✓ | — |
251
- | `commit` | git's pre-commit hook, over the staged set | ✓ | — |
252
- | `turn-end` | the agent is about to hand back | ✓ | — |
253
-
254
- A guardrail blocks; a breadcrumb is a note and has no rail to block with. The commit moment is not
255
- delivered by any harness — git's own hook is — which is why every harness gets the gates for free.
256
-
257
- ## Every verb
258
-
259
- | | |
260
- | --- | --- |
261
- | `flow init [--empty]` | scaffold a config, arm the gate, register the hooks |
262
- | `flow status [--json]` | what is bound here, at which moments, and what is not wired yet |
263
- | `flow test [config]` | every rule's own cases, driven |
264
- | `flow facts [--json]` | what the record and the conversations say about this repo |
265
- | `flow replay <file>` | a recorded session, back through the engine — no repo, no harness |
266
- | `flow hook <event>` | what the harness's registrations call. Not for you |
267
- | `flow commit <files…>` | what git's pre-commit hook calls. Not for you |
268
-
269
- Two switches, both files, both one command:
270
-
271
- ```sh
272
- touch .flow/off # every rail silenced. rm it to turn the guard back on
273
- touch .flow/record # capture sessions so `flow replay` can run them again
274
- ```
275
-
276
- ## Exit codes
277
-
278
- | | |
61
+ | To | Read |
279
62
  | --- | --- |
280
- | `0` | yes — nothing refused, and every fitting is in place |
281
- | `1` | **the report is no** — `flow status` found a fitting missing, or `flow test` found a case that fails |
282
- | `2` | **something refused** — a rule blocked, or the config would not load, or a verb was misused |
63
+ | Learn flow by doing, in five minutes | [Getting started](./docs/user/tutorials/getting-started.md) |
64
+ | Solve a specific problem: protect files, ban a command, gate a commit on your tests | [The how-to guides](./docs/user/index.md#get-something-done) |
65
+ | Look up the rule grammar, the moments, `ctx` and test cases | [The config](./docs/user/reference/config.md) · [moments](./docs/user/reference/moments.md) · [`ctx`](./docs/user/reference/ctx.md) |
66
+ | Look up a command, a switch or an exit code | [The flow command](./docs/user/reference/cli.md) |
67
+ | Write your own check or pack | [How to write your own check](./docs/user/how-to/write-your-own-check.md) · [how to share a pack](./docs/user/how-to/share-a-pack.md) |
68
+ | See what a shipped pack or check does | [The packs](./docs/user/packs/index.md) · [the checks](./docs/user/index.md#the-stock-checks--one-page-each) |
69
+ | Understand why flow works the way it does | [How flow works](./docs/user/explanation/how-flow-works.md) |
70
+ | Upgrade a repo that already uses flow | [Migration guides](./docs/user/migrations/index.md) |
71
+ | See what changed in each release | [CHANGELOG.md](./CHANGELOG.md) |
283
72
 
284
- **Exit 2 is a refusal at every rail, and it has to be.** It is the host's own refusal channel, and
285
- a hook has exactly two codes to answer with — so a blocked write and a config that will not load
286
- are both 2, because from a hook's seat they are one instruction: *this did not happen, read
287
- stderr*. The commit gate answers the same way, for the same reason.
73
+ ## How it works
288
74
 
289
- **Exit 1 belongs to the two verbs you ask rather than obey.** `flow status` and `flow test` are
290
- asked a question, so they have room for a third answer, and "not fully in force" is a genuinely
291
- different fact from "caught something". A config that will not load collapses even that room: it
292
- makes both of them answer 2, because there is nothing left to report on.
293
-
294
- This table is pinned by the suite — `e2e/product.test.ts` drives every row against the binary, so a
295
- code that moves fails a test rather than quietly making this page wrong.
75
+ `flow init` registers flow with Claude Code, and adds a git pre-commit hook. Claude Code then runs `flow` when a session starts, before and after every tool call, and when the agent finishes. Git runs `flow hook commit` before every commit. Each time, flow loads `flow.config.ts` and runs the rules bound to that moment: `session`, `touch`, `write`, `delete`, `command`, `commit` or `turn-end`. A guardrail refuses by exiting with code 2, which Claude Code and git both treat as "do not go ahead", and its message is shown to the agent. A breadcrumb never refuses: its text is added to the agent's context. [Moments](./docs/user/reference/moments.md) describes each one.
296
76
 
297
77
  ## Working on flow
298
78
 
299
79
  ```sh
300
- just # the catalogue — every recipe carries its own [doc]
301
- just test-commit # what the commit gate runs: the rules cases, the typecheck, the suite
302
- just gate # the gate itself, over every file git can see
80
+ just install && just link # run `flow` from your checkout
81
+ just # every recipe, with what it does
82
+ just test-commit # what the commit gate runs: the rule cases, the typecheck, the suite
83
+ just test-packs # the ten shipped packs, bound in a throwaway repo
303
84
  ```
304
85
 
305
- flow guards itself: `flow.config.ts` at the root binds the ten packs above through
306
- `@jawache/flow/packs` — the same import a stranger writes — plus one pack this repo writes,
307
- `guards/house.ts`. A door that stops exporting, a rule that stops loading or a bundle that stops
308
- building refuses this repo's own next commit first.
309
-
310
- Longer documentation is in [`docs/user/`](docs/user/index.md): the guidebook, the five-minute
311
- quick start, the config reference, how to author a pack, and one page per stock check.
86
+ flow guards its own repo. `flow.config.ts` binds the ten shipped packs, plus one pack this repo writes, `guards/house.ts`, so a change that breaks a rule, an export or the build is refused at this repo's own next commit.
312
87
 
313
88
  ## Licence
314
89