@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 +21 -0
- package/README.md +309 -0
- package/dist/flow.mjs +167 -0
- package/dist/index.mjs +18 -0
- package/dist/packs.mjs +116 -0
- package/dist/types/adapter/archive.d.ts +45 -0
- package/dist/types/adapter/claude.d.ts +157 -0
- package/dist/types/adapter/domain.d.ts +1112 -0
- package/dist/types/adapter/product.d.ts +10 -0
- package/dist/types/checks/domain.d.ts +792 -0
- package/dist/types/engine/domain.d.ts +747 -0
- package/dist/types/engine/state.d.ts +104 -0
- package/dist/types/errors.d.ts +14 -0
- package/dist/types/flow.d.ts +1 -0
- package/dist/types/glob.d.ts +53 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/language/domain.d.ts +641 -0
- package/dist/types/packs/docs.d.ts +49 -0
- package/dist/types/packs/fcis.d.ts +71 -0
- package/dist/types/packs/flow.d.ts +50 -0
- package/dist/types/packs/git.d.ts +107 -0
- package/dist/types/packs/index.d.ts +10 -0
- package/dist/types/packs/justfile.d.ts +83 -0
- package/dist/types/packs/node.d.ts +39 -0
- package/dist/types/packs/secrets.d.ts +117 -0
- package/dist/types/packs/tdd.d.ts +52 -0
- package/dist/types/packs/typescript.d.ts +101 -0
- package/dist/types/packs/work.d.ts +49 -0
- package/dist/types/version.d.ts +2 -0
- package/package.json +61 -0
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.
|