@jawache/flow 0.1.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/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Asim Hussain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # flow
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.
6
+
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.
9
+
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.
14
+
15
+ ---
16
+
17
+ ## Why
18
+
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.
24
+
25
+ flow's answer is that **nothing defaults and nothing loads quietly**:
26
+
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
49
+ ```
50
+
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`.
57
+
58
+ ## Five minutes, from nothing
59
+
60
+ ```sh
61
+ mkdir /tmp/flow-demo && cd /tmp/flow-demo && git init -q
62
+ flow init
63
+ ```
64
+
65
+ That writes:
66
+
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 · Stop
74
+ + git config core.hooksPath .githooks
75
+ next: `flow status` — what is bound, and what is not wired yet.
76
+ ```
77
+
78
+ Six things, and each one is checkable:
79
+
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` | four 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:
93
+
94
+ ```sh
95
+ flow status
96
+ ```
97
+
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 block risky edits before they land (and again at commit); …
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 · 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.
134
+
135
+ Then prove each rule to yourself, in this order. Each step takes seconds.
136
+
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.
139
+
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 — the commit gate holds.** Have the agent write a file containing the literal marker
144
+ `DO`-`NOT`-`COMMIT` (spelled whole), stage it, and commit. Git refuses at the hook, naming the rule
145
+ and the file. Take the marker out and the same commit lands.
146
+
147
+ **4 — a rule bound to WHO, not what.** Ask the main chat to make a small commit: it lands. Ask it to
148
+ delegate the identical commit to a subagent: refused. Same act, different wearer, different answer —
149
+ that is `.for(subagent)`, and the category recognises itself from what the host wrote, never from
150
+ anything the session claimed about itself.
151
+
152
+ **5 — break it, and watch everything stop.** Delete a `.message(…)` from `flow.config.ts` and save.
153
+ Your editor goes red on that line. Ask the agent to edit any file, or to run any command: both are
154
+ refused, with the load error on screen. Undo the edit and the next action passes. There is no
155
+ notice-and-proceed anywhere.
156
+
157
+ The one exception is the repair: while the config is broken, a write to `flow.config.ts` — or to a
158
+ pack it imports, whatever you called that folder — still goes through. The surface is read out of
159
+ the config's own relative import lines, as text, because the module is the thing that will not load.
160
+ Without the exception the rule demanding a fix would also forbid it — you would never notice,
161
+ because your editor has no hooks in front of it, but anything that does is locked out of its own
162
+ repair. Commands stay refused, and so does the commit gate, so nothing written under the exception
163
+ reaches a commit until the config loads green.
164
+
165
+ **6 — run every rule's own cases.**
166
+
167
+ ```sh
168
+ flow test
169
+ # flow test — 7 cases over 4 guardrails, all green
170
+ ```
171
+
172
+ Delete a rule's `.test(…)` block and the config stops loading at all, naming the entry.
173
+
174
+ ## Proving the packs themselves
175
+
176
+ `flow test` proves the rules in *your* config. One command proves the ten this package ships, and it
177
+ is the one to run before you trust a change to any of them:
178
+
179
+ ```sh
180
+ just test-packs
181
+ ```
182
+
183
+ It builds the bundles, runs `flow init` in a throwaway repo that has never heard of flow, binds every
184
+ shipped pack at once with a stranger's parameters — one shell script for the toolchain, `core/` for
185
+ the pure home, `rules/` for the repo's own pack — and then drives the rails: `flow status` green,
186
+ each pack's cases run pack by pack, and one real refusal at each of write · delete · command ·
187
+ commit · turn-end. It prints what it bound and what refused, and exits non-zero naming the pack the
188
+ moment a count moves.
189
+
190
+ ## The config
191
+
192
+ Open `flow.config.ts`. It is ordinary code, and it is the whole guard:
193
+
194
+ ```ts
195
+ import { commit, command, defineCategory, defineConfig, definePack, guardrail, pack } from "@jawache/flow";
196
+ import { flow } from "@jawache/flow/packs";
197
+
198
+ const subagent = defineCategory("subagent", (facts) => facts.subagent);
199
+
200
+ export const demo = definePack("demo", {
201
+ noForcePush: guardrail()
202
+ .at(command)
203
+ .check((ctx) => (/--force/.test(ctx.command ?? "") ? ctx.fail("rewrites shared history") : ctx.ok()))
204
+ .message("Force-pushing rewrites history everyone else has.")
205
+ .test({ pass: ["git push origin main"], block: ["git push --force origin main"] }),
206
+ });
207
+
208
+ export default defineConfig([pack(flow, { packs: [] }), pack(demo)]);
209
+ ```
210
+
211
+ Six things to know, and then you can write your own:
212
+
213
+ - **A sentence says everything, and defaults nothing.** `.at(…)` when it fires · `.on(…)`/`.ignore(…)`
214
+ which files · `.for(…)` which actor · `.check(…)` the question · `.message(…)` what you are told ·
215
+ `.test({ pass, block })` what proves it. Say a verb twice and the compiler stops you.
216
+ - **A check is a function of `ctx`** and reaches the world only through it — `ctx.file`, `ctx.command`,
217
+ `ctx.staged`, `ctx.exec`, `ctx.fs`, `ctx.git`. That is what lets the same check run live, run in a
218
+ case, and replay from a recording without a repo.
219
+ - **A pack is just code.** `definePack` in a file here, or a package you install: identical shape.
220
+ Publishing one is promotion, not a rewrite.
221
+ - **Packs come with the install, behind their own door.** `@jawache/flow` is the GRAMMAR a rule is
222
+ written in; `@jawache/flow/packs` is the CONTENT — ten opinionated packs (`git` · `justfile` ·
223
+ `node` · `secrets` · `typescript` · `fcis` · `tdd` · `docs` · `flow` · `work`), each bound by one
224
+ `pack(…)` line, and an unbound one costs nothing. A pack names nothing it does not ship: the
225
+ recipe your gate runs is a typed, mandatory parameter, so a config that never says which command
226
+ cuts a release does not compile — and `flow`, flow's own self-protection, takes the folder your
227
+ own packs live in, so it never guesses at a folder your repo does not have. `flow init` binds
228
+ that one for you.
229
+ - **A repo bends a pack it did not write** with `override(pack.entry)`, which speaks only what it
230
+ changes — a different glob, a different message, or `.disabled("why")`, and the reason is what
231
+ `flow status` prints beside it.
232
+ - **A category is a name with its recognizer aboard.** It reads host-written evidence — a sidecar the
233
+ harness wrote, the brief a spawn was given — and never a claim the session made about itself,
234
+ because permissions that rest on a claim rest on a lie.
235
+
236
+ ## Moments
237
+
238
+ | moment | when | guardrail | breadcrumb |
239
+ | --- | --- | --- | --- |
240
+ | `session` | a chat starts, resumes, or is compacted | — | ✓ |
241
+ | `touch` | a tool call names a file | — | ✓ |
242
+ | `write` | a file is about to be written | ✓ | — |
243
+ | `delete` | a command is about to remove a file | ✓ | — |
244
+ | `command` | a shell command is about to run | ✓ | — |
245
+ | `commit` | git's pre-commit hook, over the staged set | ✓ | — |
246
+ | `turn-end` | the agent is about to hand back | ✓ | — |
247
+
248
+ A guardrail blocks; a breadcrumb is a note and has no rail to block with. The commit moment is not
249
+ delivered by any harness — git's own hook is — which is why every harness gets the gates for free.
250
+
251
+ ## Every verb
252
+
253
+ | | |
254
+ | --- | --- |
255
+ | `flow init [--empty]` | scaffold a config, arm the gate, register the hooks |
256
+ | `flow status [--json]` | what is bound here, at which moments, and what is not wired yet |
257
+ | `flow test [config]` | every rule's own cases, driven |
258
+ | `flow facts [--json]` | what the record and the conversations say about this repo |
259
+ | `flow replay <file>` | a recorded session, back through the engine — no repo, no harness |
260
+ | `flow hook <event>` | what the harness's registrations call. Not for you |
261
+ | `flow commit <files…>` | what git's pre-commit hook calls. Not for you |
262
+
263
+ Two switches, both files, both one command:
264
+
265
+ ```sh
266
+ touch .flow/off # every rail silenced. rm it to turn the guard back on
267
+ touch .flow/record # capture sessions so `flow replay` can run them again
268
+ ```
269
+
270
+ ## Exit codes
271
+
272
+ | | |
273
+ | --- | --- |
274
+ | `0` | yes — nothing refused, and every fitting is in place |
275
+ | `1` | **the report is no** — `flow status` found a fitting missing, or `flow test` found a case that fails |
276
+ | `2` | **something refused** — a rule blocked, or the config would not load, or a verb was misused |
277
+
278
+ **Exit 2 is a refusal at every rail, and it has to be.** It is the host's own refusal channel, and
279
+ a hook has exactly two codes to answer with — so a blocked write and a config that will not load
280
+ are both 2, because from a hook's seat they are one instruction: *this did not happen, read
281
+ stderr*. The commit gate answers the same way, for the same reason.
282
+
283
+ **Exit 1 belongs to the two verbs you ask rather than obey.** `flow status` and `flow test` are
284
+ asked a question, so they have room for a third answer, and "not fully in force" is a genuinely
285
+ different fact from "caught something". A config that will not load collapses even that room: it
286
+ makes both of them answer 2, because there is nothing left to report on.
287
+
288
+ This table is pinned by the suite — `e2e/product.test.ts` drives every row against the binary, so a
289
+ code that moves fails a test rather than quietly making this page wrong.
290
+
291
+ ## Working on flow
292
+
293
+ ```sh
294
+ just # the catalogue — every recipe carries its own [doc]
295
+ just test-commit # what the commit gate runs: the rules cases, the typecheck, the suite
296
+ just gate # the gate itself, over every file git can see
297
+ ```
298
+
299
+ flow guards itself: `flow.config.ts` at the root binds the ten packs above through
300
+ `@jawache/flow/packs` — the same import a stranger writes — plus one pack this repo writes,
301
+ `guards/house.ts`. A door that stops exporting, a rule that stops loading or a bundle that stops
302
+ building refuses this repo's own next commit first.
303
+
304
+ Longer documentation is in [`docs/user/`](docs/user/index.html): the guidebook, the five-minute
305
+ quick start, the config reference, how to author a pack, and one page per stock check.
306
+
307
+ ## Licence
308
+
309
+ MIT.