@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.
@@ -1,5 +1,5 @@
1
- import { type BreadcrumbMoment, type EntrySpec, type GuardrailMoment, type LoadResult, type Moment, type Refusal, type SessionFacts, type TurnAction } from "../language/domain.ts";
2
- import { type Block, type Bound, type Cause, type Metrics, type MomentsView, type Row, type TerrainNode } from "../engine/domain.ts";
1
+ import { type BreadcrumbMoment, type Ctx, type EntrySpec, type LoadedEntry, type ExecResult, type GuardrailMoment, type LoadResult, type Moment, type Refusal, type SessionFacts, type ShellCommand, type ShellStep, type TurnAction } from "../language/domain.ts";
2
+ import { type AgentOf, type Block, type Bound, type Cause, type Metrics, type MomentsView, type Row, type TerrainNode } from "../engine/domain.ts";
3
3
  /**
4
4
  * One hook payload, as Claude Code writes it on stdin.
5
5
  *
@@ -10,7 +10,13 @@ import { type Block, type Bound, type Cause, type Metrics, type MomentsView, typ
10
10
  */
11
11
  export interface HookPayload {
12
12
  readonly session_id?: string;
13
+ /** Present on every call a SUBAGENT makes, and only then: the host's id for that one spawn. */
13
14
  readonly agent_id?: string;
15
+ /**
16
+ * The agent type the host started this subagent as, sent beside `agent_id` on its every call.
17
+ * Host-written: the session cannot change the message, which is why a category may key on it.
18
+ */
19
+ readonly agent_type?: string;
14
20
  readonly hook_event_name?: string;
15
21
  readonly transcript_path?: string;
16
22
  readonly stop_hook_active?: boolean;
@@ -18,10 +24,22 @@ export interface HookPayload {
18
24
  readonly tool_input?: Record<string, unknown>;
19
25
  /** SessionStart's reason: `startup` · `resume` · `clear` · `compact` — the compaction announcement. */
20
26
  readonly source?: string;
27
+ /**
28
+ * The host's id for one tool call, the same on its PreToolUse and its PostToolUse. It is the key
29
+ * the snapshot taken before the call is filed under and looked up by after it.
30
+ */
31
+ readonly tool_use_id?: string;
32
+ /**
33
+ * The permission mode the session runs under (`default` · `acceptEdits` · `plan` · `auto` · …),
34
+ * as the host states it on every call. Recorded and reported; never a category key.
35
+ */
36
+ readonly permission_mode?: string;
37
+ /** What the tool answered, on the after-call rails. Read for the host's own changed-file list. */
38
+ readonly tool_response?: unknown;
21
39
  readonly [key: string]: unknown;
22
40
  }
23
41
  /**
24
- * The five events flow registers for, named by the EVENT and never by the job.
42
+ * The events flow answers, named by the EVENT and never by the job.
25
43
  *
26
44
  * Carried from the old engine unchanged, and the naming rule with it: call a hook
27
45
  * `flow hook plan-drift` and the day a second concern wants the same host event you register a
@@ -41,54 +59,25 @@ export declare const CONFIG_FILE = "flow.config.ts";
41
59
  * Every relative module the config names, READ OUT OF ITS TEXT.
42
60
  *
43
61
  * A regex over source, deliberately, and it is the only reader that can work here: this is asked
44
- * about a file that will not import, so anything that had to resolve, parse or evaluate it would
62
+ * about a file that may not import, so anything that had to resolve, parse or evaluate it would
45
63
  * be asking the broken thing to answer for itself. A line like `import { house } from
46
64
  * "./guards/house.ts";` survives almost every way a config breaks, because what breaks a config is
47
65
  * a missing `.message(…)` or a stray brace two hundred lines below its imports.
48
66
  *
49
67
  * Only `./…` and `../…` count. A bare specifier is a package — `@jawache/flow`, or a pack somebody
50
- * published — and a package is not this repo's source to repair. A `..` segment climbs out of the
51
- * root, so those are dropped too: the config sits at the repo root and the surface is
52
- * repo-relative or it is nothing. A leading dot in a FOLDER name is ordinary and is kept —
53
- * `./.guard/house.ts` is a pack like any other.
68
+ * published — and a package is not this repo's source. A `..` segment climbs out of the root, so
69
+ * those are dropped too: the config sits at the repo root and `guardPaths` is repo-relative or it
70
+ * is nothing. A leading dot in a FOLDER name is ordinary and is kept — `./.guard/house.ts` is a pack
71
+ * like any other.
54
72
  *
55
73
  * A `from '…'` phrase inside a QUOTED STRING is harvested as well — a rule whose `.message(…)`
56
- * spells an import line puts that path on the surface though no import named it. ACCEPTED, not
74
+ * spells an import line puts that path among the guard's files though no import named it. ACCEPTED, not
57
75
  * overlooked: telling a real import from a sentence quoting one takes a parser, and a parser is
58
76
  * the one thing a file that will not parse cannot give. The cost is bounded and one-directional —
59
- * the surface only ever WIDENS, it widens only while the config is already refusing every other
60
- * write, and the commit gate goes on refusing until the config loads green.
77
+ * the list only ever WIDENS, so the worst it does is report one extra path as a guard file.
61
78
  */
62
79
  export declare function configImports(configText: string): readonly string[];
63
- /**
64
- * THE CONFIG SURFACE — the files a write may target while the guard is broken, and nothing else.
65
- *
66
- * Fail-loud says a config that will not load refuses every gated moment, and it is right: a guard
67
- * that quietly carries on with yesterday's rules is a guard that lies about being there. But taken
68
- * without exception it also refuses the ONE write that can end the outage, and then the doctrine's
69
- * own instruction — "adjust the change so it passes, then retry" — is impossible to obey. A human
70
- * in an editor never meets this (no hooks sit in front of them, so they just undo); an agent that
71
- * broke the config is locked out of repairing it, and the repo stays broken until a human arrives.
72
- * Measured twice on the crossing that introduced it, once for real.
73
- *
74
- * So the exception is exactly as wide as the repair and no wider: a write whose target is the
75
- * config or a pack it imports. Everything else still refuses — every other path, every command,
76
- * and the commit gate, which goes on refusing until the config loads green, so nothing written
77
- * under the exception can reach a commit unreviewed by a working guard.
78
- *
79
- * DERIVED FROM THE CONFIG'S OWN TEXT, and it used to be a two-line constant: the config file, plus
80
- * one repo's pack folder spelled into the engine. Every other repo got a repair exception over a
81
- * folder it does not have, and an agent that broke the config there could not reach the pack it
82
- * broke. The text is readable when the module is not, so nothing is given up by asking it — and the
83
- * answer now follows whatever a repo actually called the folder.
84
- *
85
- * The FOLDER of each imported pack comes with the file, which is the one deliberate widening. A
86
- * repair is rarely one file: a pack imports its own helpers, and those the config never names. The
87
- * import graph cannot be walked (walking it means resolving, and resolving is what is broken), so
88
- * the folder is the honest approximation — and it is exactly the width the old constant had.
89
- */
90
- export declare function configSurface(configText: string | null): readonly string[];
91
- export declare const HOOK_EVENTS: readonly ["session-start", "pre-tool-use", "post-tool-use", "stop", "notification"];
80
+ export declare const HOOK_EVENTS: readonly ["session-start", "pre-tool-use", "post-tool-use", "post-tool-use-failure", "stop", "subagent-stop", "notification"];
92
81
  export type HookEvent = (typeof HOOK_EVENTS)[number];
93
82
  export declare function isHookEvent(word: string): word is HookEvent;
94
83
  /**
@@ -97,38 +86,56 @@ export declare function isHookEvent(word: string): word is HookEvent;
97
86
  *
98
87
  * Two facts are stated here rather than discovered by somebody whose rule never fires:
99
88
  *
100
- * · `commit` is delivered, but not by the harness — git's pre-commit hook calls `flow commit`,
101
- * which is why every harness gets the gates for free and why it is in this list at all.
102
- * · turn-end BRIEFING is not delivered. Stop's decision object carries no context channel, so a
103
- * `breadcrumb().at(turnEnd)` has nowhere to be shown; the guardrail rail at that moment works
104
- * perfectly. Reporting an entry that goes dark is `flow status`'s job (F6).
89
+ * · `commit` is delivered, but not by the harness — git's pre-commit hook calls `flow hook
90
+ * commit`, which is why every harness gets the gates for free and why it is in this list at all.
91
+ * · turn-end BRIEFING is delivered, on the `Stop` and `SubagentStop` answers'
92
+ * `additionalContext`: the host labels it hook feedback rather than an error, and the agent
93
+ * takes one more turn to read it. A refusal at the same hand-back carries the note beside it
94
+ * (`toResult`), so the agent is held and told at once rather than held, then told on the next.
105
95
  * · command BRIEFING is delivered, on the same PreToolUse answer the command guardrails use: when
106
96
  * nothing blocks, the hook's `additionalContext` carries the note, and when something does, the
107
- * refusal wins — which is the right order, since a blocked command is not about to run.
97
+ * refusal wins — which is the right order, since a blocked command is not about to run. The
98
+ * files a Bash command reads ride that same answer as touches (`bashReads`), so an area's note
99
+ * arrives before the output does.
108
100
  *
109
- * It is two lists rather than one because "turn-end" is a word in both vocabularies and the answer
110
- * differs between them — a single flat list could only lie about one of them.
101
+ * It is two lists rather than one because a moment is delivered for a KIND of entry: `command` and
102
+ * `turn-end` are words in both vocabularies, and another harness may carry one rail there and not
103
+ * the other — a single flat list could only lie about one of them.
111
104
  */
112
105
  export declare const DELIVERS: {
113
106
  guard: readonly GuardrailMoment[];
114
107
  brief: readonly BreadcrumbMoment[];
115
108
  };
109
+ /**
110
+ * Does a note at this moment ride a refusal, rather than wait for the call that runs?
111
+ *
112
+ * Before a call, a refusal means the call never runs, so a note about it is held back until the
113
+ * retry that does — and is not marked shown, or it would stay silent until drift. A held turn is
114
+ * different: the agent is about to act on the refusal anyway, and the note is about that same
115
+ * turn, so both go to it together.
116
+ */
117
+ export declare function ridesRefusal(moment: BreadcrumbMoment): boolean;
116
118
  /**
117
119
  * One event in the engine's vocabulary, minus the two things the engine's own event types demand
118
- * and this file cannot know: who the session is (`wearing`, which needs the sidecar and the stored
120
+ * and this file cannot know: who the session is (`wearing`, which needs the transcript and the stored
119
121
  * verdict) and how far it has drifted (`tokens`, which needs the transcript). The shell adds both,
120
122
  * because both come off disk.
121
123
  */
122
124
  export type AdapterEvent = {
123
125
  readonly rail: "guard";
124
126
  readonly moment: GuardrailMoment;
125
- readonly file?: {
126
- readonly path: string;
127
- readonly content: string;
128
- } | undefined;
127
+ readonly file?: Ctx["file"];
129
128
  readonly command?: string | undefined;
129
+ /** command: the line's parse, made once here and handed to the engine with the line. */
130
+ readonly commands?: Ctx["commands"];
130
131
  readonly staged?: readonly string[] | undefined;
131
132
  readonly turn?: readonly TurnAction[] | undefined;
133
+ /**
134
+ * The change has already LANDED: the file came out of the delta after the call, not out of a
135
+ * tool's input before it. A refusal of an observed write is undone from the snapshot, and it
136
+ * is refused under its own banner.
137
+ */
138
+ readonly observed?: boolean | undefined;
132
139
  } | {
133
140
  readonly rail: "brief";
134
141
  readonly moment: BreadcrumbMoment;
@@ -147,8 +154,21 @@ export interface EventWorld {
147
154
  readonly root: string;
148
155
  /** A file as it stands right now, or null when it is not there — an Edit is a diff against it. */
149
156
  readonly read: (path: string) => string | null;
150
- /** What the actor did this turn. Only `stop` asks, so only `stop` pays for it. */
157
+ /** What the actor did this turn. Only the two turn-end rails ask, so only they pay for it. */
151
158
  readonly turn?: (() => readonly TurnAction[]) | undefined;
159
+ /**
160
+ * The agent the SESSION itself runs as (`--agent`, or the `agent` setting), or null when it runs
161
+ * as none. Only `subagent-stop` asks, and only of a payload that names an agent type, because it
162
+ * costs the parent's transcript: it is how an internal agent's hand-back is told from a subagent's.
163
+ */
164
+ readonly sessionAgent?: (() => string | null) | undefined;
165
+ /** The home directory, for a Bash read through `~`. Absent, such a path is simply not named. */
166
+ readonly home?: string | undefined;
167
+ /**
168
+ * What the call changed in the tree, from the snapshot taken before it. Only the after-call
169
+ * rails carry it; absent, the call changed nothing flow could see.
170
+ */
171
+ readonly changes?: readonly Change[] | undefined;
152
172
  }
153
173
  /** An absolute path inside the repo → the repo-relative spelling every glob is written against. */
154
174
  export declare function relativise(path: string, root: string): string;
@@ -183,30 +203,211 @@ export declare function wouldBeFile(tool: string, input: Record<string, unknown>
183
203
  content: string;
184
204
  } | null;
185
205
  /**
186
- * The paths a Bash line would remove — `rm`, `git rm`, and the SOURCE side of `mv` / `git mv`.
206
+ * The paths a Bash line would remove — `rm`, `git rm`, and the SOURCE side of `mv` / `git mv` —
207
+ * read from those commands' arguments in the parse.
187
208
  *
188
209
  * Deletion has no tool of its own: it is a side effect of a shell command, so the delete moment is
189
- * assembled here or it does not exist at all. `git mv` was the old engine's blind spot; it is
190
- * covered, and quoting is respected, so a commit message mentioning `rm` is not a removal.
210
+ * assembled here or it does not exist at all. The parse decides what is a command, so a commit
211
+ * message mentioning `rm`, a heredoc body that names a file, or an `echo 'rm a.ts'` is not a
212
+ * removal, while an `rm` inside a `$( … )` or a `bash -c` string, which runs, is.
191
213
  *
192
- * A line that cannot be tokenised yields NOTHING, deliberately: `null` from the tokeniser means "I
193
- * cannot tell", and a guard that guesses at a line it could not parse is worse than one that stands
194
- * aside. The command rail still sees the raw line either way.
214
+ * A line that does not parse yields NOTHING, deliberately: the parse's `null` means "I cannot tell",
215
+ * and a guard that guesses at a line it could not parse is worse than one that stands aside. The
216
+ * command rail still sees the raw line either way.
217
+ */
218
+ export declare function deleteTargets(commands: readonly ShellCommand[] | null): string[];
219
+ /** Where a Bash call ran, and the two directories its paths are judged against. */
220
+ export interface BashWhere {
221
+ /** The shell's working directory when the call started. */
222
+ readonly cwd: string;
223
+ /** The repo root. Paths outside it are not an area of this repo and are dropped. */
224
+ readonly root: string;
225
+ /** What `~` and `$HOME` mean; absent, a path through them is not known. */
226
+ readonly home?: string | undefined;
227
+ }
228
+ /** What a Bash command reads in the repo, repo-relative, in order, each once. */
229
+ export interface BashReads {
230
+ /** Files it reads by name — `cat`, `head`, `sed -n`, `grep` on a file, a `<` redirect. */
231
+ readonly reads: readonly string[];
232
+ /**
233
+ * Paths a search walks or lists — `rg`, `find`, `ls`, `grep -r`, a `git` pathspec — which may be
234
+ * directories. They touch an area; they are not a file the session read.
235
+ */
236
+ readonly searched: readonly string[];
237
+ }
238
+ /**
239
+ * The repo paths a Bash command reads or searches — the `READERS` table, a `<` redirect, a `git`
240
+ * pathspec — read from the parse's steps, and followed through `cd`, `VAR=`, `for` loops and the
241
+ * commands a `$(…)` runs. The repo root itself is not an area and is left out. Best-effort, failing
242
+ * open: see the section head. A line that does not parse names nothing.
195
243
  */
196
- export declare function deleteTargets(command: string): string[];
244
+ export declare function bashReads(steps: readonly ShellStep[] | null, where: BashWhere): BashReads;
197
245
  /**
198
246
  * A payload → every event it carries. ZERO OR MORE, and the plural is not hedging.
199
247
  *
200
- * One Bash call is genuinely two moments — the command about to run, and each file an `rm` in it
201
- * would remove — and the old engine ran two evaluations off that one hook for exactly this reason.
248
+ * One Bash call is genuinely several moments — the command about to run, each file an `rm` in it
249
+ * would remove, and each file it reads — and the old engine ran two evaluations off that one hook
250
+ * for exactly this reason.
202
251
  * A signature returning one event would push the second rail back into the shell, where it could
203
252
  * not be tested as a decision.
204
253
  *
205
254
  * An event flow was not asked for answers with an empty list, never with an invented moment.
206
255
  */
207
256
  export declare function toEvent(hook: HookEvent, payload: HookPayload, world: EventWorld): AdapterEvent[];
257
+ /**
258
+ * Does this hook have nothing to run for this config — so the shell can answer before it reads a
259
+ * thing?
260
+ *
261
+ * A subagent's hand-back costs the parent's transcript (whose agent the session runs as) and the
262
+ * subagent's own (its turn), and it comes at every hand-back of every subagent. A config that binds
263
+ * no entry at turn-end, guardrail or breadcrumb, has nothing either read could feed, so neither is
264
+ * made. A disabled entry binds nothing.
265
+ *
266
+ * A repo that is RECORDING never answers early: the tape is for replaying a session against rules
267
+ * written later, so it keeps every hand-back whatever is bound today.
268
+ */
269
+ export declare function nothingBound(hook: HookEvent, entries: readonly LoadedEntry[], recording: boolean): boolean;
208
270
  /** What a post-tool-use call was about, repo-relative: a path, or nothing this rail can steer on. */
209
271
  export declare function touchedPath(payload: HookPayload, root: string): string | null;
272
+ /**
273
+ * One path a call changed. `before` is null for a file the call created, `after` for one it
274
+ * removed; a path with both is a file it changed.
275
+ */
276
+ export interface Change {
277
+ readonly path: string;
278
+ readonly before: string | null;
279
+ readonly after: string | null;
280
+ }
281
+ /**
282
+ * What the shell could see of one call — the changes, or why it could not see them.
283
+ *
284
+ * A FAULT IS NOT AN EMPTY DELTA, and the difference is the guard's whole doctrine: "I looked and
285
+ * nothing changed" and "I could not look" must never be spelled the same way, or a snapshot that
286
+ * failed would let every write through as clean.
287
+ */
288
+ export type Delta = {
289
+ readonly ok: true;
290
+ readonly changes: readonly Change[];
291
+ } | {
292
+ readonly ok: false;
293
+ readonly fault: string;
294
+ };
295
+ /**
296
+ * Where git should look for the ignored paths `snapshotInclude` names: each glob's literal head,
297
+ * so git walks `config/` for `config/*.local.json` and not the whole repo. A glob with no literal
298
+ * head (`**` + `/.env`) has to be looked for everywhere.
299
+ */
300
+ export declare function snapshotPathspecs(include: readonly string[]): string[];
301
+ /** Of the ignored paths git listed under those heads, the ones a `snapshotInclude` glob matches. */
302
+ export declare function included(listed: readonly string[], include: readonly string[]): string[];
303
+ /**
304
+ * Of the paths a call changed, the ones the AGENT changed. The rest are git's — a checkout, a reset
305
+ * or a fast-forward put them there, from a commit the repo already had, and the commit gate judged
306
+ * that content when it was committed — so they leave the diff before anything reads them: no rule,
307
+ * no write-back, no breadcrumb, no shell-write row. CONTENT, never the command line: git run by a
308
+ * recipe is caught the same way, and the `sed` in `git switch x && sed -i … f` is still judged.
309
+ *
310
+ * - A file on disk after the call is git's when it is identical to its copy in HEAD after the call.
311
+ * - A file the call removed is git's when HEAD held it BEFORE the call and does not hold it after:
312
+ * git removed it by moving HEAD. A file no commit holds — an untracked file, an included `.env` —
313
+ * is the agent's to answer for when it goes.
314
+ *
315
+ * `nameStatus` is the call's own diff, `git diff-tree -r -z --name-status --no-renames`, in git's
316
+ * order. `differsAfter` and `differsBefore` are every path where HEAD's tree after the call, and
317
+ * before it, disagree with the tree after the call. `differsAfter` is null when HEAD after the call
318
+ * is no commit the repo had before it — none yet, or one the call made, whose content no gate is
319
+ * known to have judged — and then nothing is git's. `differsBefore` is null where there was no HEAD
320
+ * before; for a path gone from disk, being in it is exactly "HEAD held it before".
321
+ */
322
+ export declare function authored(nameStatus: string, differsAfter: readonly string[] | null, differsBefore: readonly string[] | null): string[];
323
+ /**
324
+ * Git's answer as paths, one per line — the plainest shape git has, and the only one flow parses.
325
+ *
326
+ * ONE reader for it in the adapter: `flow status` asks git what it ignores and the realWorld asks
327
+ * for the staged set, and both used to split, trim and drop blank lines under two names. `null` is
328
+ * a command that never ran, and it is no paths.
329
+ */
330
+ export declare function pathLines(stdout: string | null): string[];
331
+ /**
332
+ * What a commit changes, in the shape the commit gate judges: the files it will hold, which of
333
+ * those the last commit does not have, and the files it removes. A rename is a removal of the old
334
+ * path plus an add of the new one, so it needs no shape of its own.
335
+ */
336
+ export interface StagedChanges {
337
+ /** Every path the commit will hold that it changes — the subjects of a file rule. */
338
+ readonly files: readonly string[];
339
+ /** The `files` the last commit does not have. Undefined when git could not say. */
340
+ readonly added: readonly string[] | undefined;
341
+ /** The paths the last commit has and this one will not. */
342
+ readonly deleted: readonly string[];
343
+ }
344
+ /** NUL-separated paths, as git prints them under `-z` — no quoting, so a path is a path. */
345
+ export declare function nulPaths(stdout: string): string[];
346
+ /**
347
+ * The staged changes, read from `git diff --cached --name-status --no-renames -z`.
348
+ *
349
+ * ONE question to git, and every staged change comes back with what it is: `A` added, `D` deleted,
350
+ * anything else changed in place. `--no-renames` splits a rename into the delete and the add it is.
351
+ */
352
+ export declare function stagedChanges(nameStatus: string): StagedChanges;
353
+ /**
354
+ * The whole tree as one commit — `flow hook commit --all` — against the last commit's own list.
355
+ *
356
+ * `present` is every file git can see that is on disk; `tree` is `git ls-tree -r --name-only -z
357
+ * HEAD`. It fails in two ways that answer differently. Before the first commit there is no HEAD, so
358
+ * every file is new and nothing is removed. Any other failure is unknown: `added` stays undefined,
359
+ * so a check asks the disk and refuses rather than allows.
360
+ */
361
+ export declare function treeChanges(present: readonly string[], tree: ExecResult, hasHead: () => boolean): StagedChanges;
362
+ /** The key one call's snapshot is filed under — the host's `tool_use_id` — or null when it sent none. */
363
+ export declare function callKey(payload: HookPayload): string | null;
364
+ /**
365
+ * The paths the PreToolUse rail already judged for this call, repo-relative, BY RAIL.
366
+ *
367
+ * An Edit, a Write or a MultiEdit was judged as a write, on the file it would produce; an `rm` or
368
+ * an `mv` source was judged as a delete, on the file it was about to remove. Both were allowed there
369
+ * or the call would not have run, and judging them again after it would log every such decision
370
+ * twice. The rail matters: a path judged only as a delete says nothing about content written to it
371
+ * afterwards in the same call.
372
+ */
373
+ export declare function preJudged(payload: HookPayload, root: string): {
374
+ readonly writes: string[];
375
+ readonly deletes: string[];
376
+ };
377
+ /**
378
+ * One tool call as flow's own log holds it: who made it, when its snapshot was taken, and when its
379
+ * diff was, once it has been. A record with no `done` is a call still in flight.
380
+ */
381
+ export interface CallRecord {
382
+ /** The host's `tool_use_id`. */
383
+ readonly key: string;
384
+ readonly session: string;
385
+ readonly agent: string;
386
+ readonly tool: string;
387
+ /** When the snapshot before the call was taken, in epoch milliseconds. */
388
+ readonly at: number;
389
+ /** When the diff after it was taken. Absent while the call is in flight. */
390
+ readonly done?: number | undefined;
391
+ }
392
+ /** A call record read back off disk, or null when the file is not one. */
393
+ export declare function callRecord(key: string, raw: unknown): CallRecord | null;
394
+ /**
395
+ * The calls of OTHER actors that overlapped this one in time: started before this diff, and
396
+ * finished after this snapshot or not finished at all.
397
+ *
398
+ * A call that finished inside this one's window counts as well as one still in flight: its write
399
+ * landed between this snapshot and this diff just the same. Calls of the same session and agent
400
+ * never count — the host does not run an agent's writing calls side by side.
401
+ */
402
+ export declare function overlapping(me: CallRecord, log: readonly CallRecord[]): CallRecord[];
403
+ /**
404
+ * The finished calls the log can forget: no call still in flight started before one finished, so no
405
+ * diff still to come can overlap it. Keeping every record for a day would make every diff read a
406
+ * day of records.
407
+ */
408
+ export declare function forgettable(log: readonly CallRecord[]): string[];
409
+ /** How a call is named in a message: the tool, the host's id, and whose it was. */
410
+ export declare function callName(call: CallRecord): string;
210
411
  /** What the binary prints and exits with. The three IO edges as data, so a test can read them. */
211
412
  export interface HookResult {
212
413
  /** stdout — a JSON decision the host reads. Empty means nothing to say. */
@@ -221,7 +422,61 @@ export declare const ALLOW: HookResult;
221
422
  export interface Refused {
222
423
  readonly moment: GuardrailMoment;
223
424
  readonly block: Block;
425
+ /** The path of a change that had already landed when this refused it — the one to put back. */
426
+ readonly observed?: string | undefined;
427
+ }
428
+ /**
429
+ * What putting refused changes back came to — written by the shell, which did the writing.
430
+ *
431
+ * `failed` is the loud half: a file flow could not write back still holds what the call wrote, and
432
+ * the message must say so rather than claim a revert that did not happen.
433
+ */
434
+ export interface Undone {
435
+ /** The changes written back to what they were before the call. */
436
+ readonly restored: readonly Change[];
437
+ /** Paths the call changed that no rule refused — left as the call wrote them. */
438
+ readonly kept: readonly string[];
439
+ readonly failed: readonly {
440
+ readonly path: string;
441
+ readonly why: string;
442
+ }[];
443
+ /** What the refusal was allowed to undo — what the message explains when it was not everything. */
444
+ readonly scope?: RevertScope | undefined;
224
445
  }
446
+ /** Which of a call's changes are put back, and which stay as the call left them. */
447
+ export interface Reversal {
448
+ readonly revert: readonly Change[];
449
+ readonly kept: readonly string[];
450
+ }
451
+ /**
452
+ * What a refusal is allowed to undo, beside the refusals themselves: the repo's `revert` setting,
453
+ * and the other calls that overlapped this one.
454
+ *
455
+ * A command started in the background gets nothing here, by ruling: what it writes after its call
456
+ * returns lands in some later call's diff and is judged and put back there like anything else.
457
+ * The harness runs no hook when the process ends, so any mark saying "not this call's" would have
458
+ * to outlive the process by a guess — and a guess that let `sleep 1` buy one unreverted write.
459
+ */
460
+ export interface RevertScope {
461
+ /** The config's `revert` setting: the refused files alone, or every file the call changed. */
462
+ readonly revert: "refused" | "all";
463
+ /** Other actors' calls that overlapped this one. Any of them, and `"all"` is not honoured. */
464
+ readonly overlap: readonly CallRecord[];
465
+ }
466
+ /** The default scope: the refused files, and no overlap. */
467
+ export declare const REFUSED_ONLY: RevertScope;
468
+ /**
469
+ * THE REVERT SCOPE: the files a rule refused, or every file the call changed when the repo asked
470
+ * for that, and never more than the refused files while another actor's call overlapped this one
471
+ * — a diff taken then can hold that call's writes, and "all" would put back work that was not this
472
+ * call's to lose.
473
+ *
474
+ * Most writing calls touch one file, and the other files a multi-file command wrote are not wrong
475
+ * because one of them is — they are listed as kept, so the agent knows they stand. A refusal that
476
+ * names no landed path (a fault on a pre-emptive rail) reverts nothing: a revert is only ever of a
477
+ * change flow saw land, never a guess.
478
+ */
479
+ export declare function reversal(changes: readonly Change[], refusals: readonly Refused[], scope?: RevertScope): Reversal;
225
480
  /** A note with its prose resolved: a `file:` breadcrumb is read by the shell, never here. */
226
481
  export interface Shown {
227
482
  readonly entry: string | null;
@@ -232,59 +487,60 @@ export interface Shown {
232
487
  export interface Answer {
233
488
  readonly refused: readonly Refused[];
234
489
  readonly shown: readonly Shown[];
490
+ /** What was put back, when a refusal was of changes that had already landed. */
491
+ readonly undone?: Undone | undefined;
235
492
  }
236
493
  /**
237
- * Is this event the WRITE that could fix a config that will not load?
238
- *
239
- * The one exception to fail-loud, and it is deliberately the narrowest shape that works: a write
240
- * (or a delete) whose target is the guard's own source. `configSurface` above says why that surface
241
- * is read out of the config's own text rather than resolved.
494
+ * What the agent is told beside the load error, on every answer it rides: what that error means.
242
495
  *
243
- * A COMMAND is never a repair, however plausible it looks. `sed -i` on the config would qualify by
244
- * intent and there is no way to tell it from `rm -rf` before it runs — a command rail sees a string,
245
- * not a target — so the command rail stays fully closed in the broken state, and the agent's route
246
- * back is the edit tools, which name the file they are about to write.
247
- *
248
- * It lives HERE rather than in the shell beside its caller because it decides and reads nothing.
249
- * The coverage gate over this file is what keeps the exception's exact width asserted.
496
+ * The load error says what broke; this says what flow is doing about it. Without it, a call that
497
+ * went through reads like a guard that judged it and passed.
250
498
  */
251
- export declare function repairs(event: AdapterEvent, surface: readonly string[]): boolean;
499
+ export declare const FLOW_OFF = "flow is OFF until flow.config.ts loads: every write, delete and command goes through unchecked, and every commit is refused.";
252
500
  /**
253
501
  * WHAT EVERY RAIL ANSWERS WHILE THE CONFIG WILL NOT LOAD — one decision for both ways it can break.
254
502
  *
255
- * A config has two ways of not loading, and they used to get two different answers. One will not
256
- * IMPORT — a missing module, a syntax error — which the shell catches as a thrown error. The other
257
- * imports fine and the GRAMMAR refuses it: a missing `.message(…)`, an override naming an entry the
258
- * pack does not have, a mandatory parameter nobody supplied. Only the first ever reached the repair
259
- * exception, because the second was refused a layer deeper inside `guard()` — so the break the
260
- * README and the guidebook both tell a reader to make was the one break an agent could not repair
261
- * itself out of. To everyone who meets them the two are the same fact, "the guard is entirely off",
262
- * and now they are the same answer as well.
263
- *
264
- * TURN-END IS TOLD, NOT REFUSED. Holding a turn open on a broken config prevents nothing risky —
265
- * every write, command and commit is already refused and the commit gate stays shut — while it does
266
- * stop the agent handing back to the one person who can fix it. So the fault is reported and the
267
- * turn ends.
503
+ * A config has two ways of not loading. One will not IMPORT — a missing module, a syntax error —
504
+ * which the shell catches as a thrown error. The other imports fine and the GRAMMAR refuses it: a
505
+ * missing `.message(…)`, an override naming an entry the pack does not have, a mandatory parameter
506
+ * nobody supplied. To everyone who meets them the two are the same fact, "the guard is entirely
507
+ * off", so they get the same answer.
508
+ *
509
+ * NOTHING IS REFUSED HERE (docs/user/explanation/fail-loud.md). A guard that refused every call
510
+ * would refuse the repair too — the fix for a missing package is `npm ci`, a command — and a write
511
+ * made now reaches the commit gate anyway, which stays shut until the config loads and then checks
512
+ * the staged files against every guardrail. The cost is the guardrails on commands, which nothing
513
+ * later can check; the agent is told on every call that they are off.
514
+ *
515
+ * TOLD BEFORE THE CALL, ONCE. The load error rides the answer to a session start and to every
516
+ * write, delete or command about to run — one notice per answer, however many events the payload
517
+ * carried. A touch never carries it, on either rail: a Read is not told, and a Bash call is told by
518
+ * its command, not by the files it reads. So the after-call rail, whose events are observed changes
519
+ * and touches, stays silent: the agent was told when the call was asked for, and saying it again
520
+ * with the result is the same sentence twice.
521
+ *
522
+ * TURN-END IS TOLD, NOT REFUSED. Holding a turn open on a broken config prevents nothing — the
523
+ * commit gate stays shut — while it does stop the agent handing back to the one person who can fix
524
+ * it. So the fault is reported and the turn ends. Not as a note either: a turn-end note keeps the
525
+ * agent working for one more turn, which is the same hold by a softer name.
268
526
  *
269
527
  * THE ENGINE HAS ITS OWN PAIR OF `!load.ok` BRANCHES — `guard()` blocks with the fault, `brief()`
270
528
  * notices it — and they are NOT this rule written twice. They answer a different question: "there
271
- * is nothing to run, say so", for the two callers that have no repair to make and no turn to end,
272
- * namely `replay` and the commit gate. Deleting them was considered at F3.B3 and measured first:
273
- * `flow replay` over a config the grammar refuses reaches `brief()`'s branch and emits the fault
274
- * notice, so the branch is live and its deletion would have dropped that notice silently. What IS
275
- * shared is the shape — `Outage extends Answer`, so the hook rail's answer and every other answer
276
- * are one type rather than two that happen to match.
529
+ * is nothing to run, say so", for the two callers that have no call to let through and no turn to
530
+ * end, namely `replay` and the commit gate. `flow replay` over a config the grammar refuses reaches
531
+ * `brief()`'s branch and emits the fault notice, so that branch is live. What IS shared is the
532
+ * shape — `Outage extends Answer`, so the hook rail's answer and every other answer are one type.
277
533
  */
278
534
  export interface Outage extends Answer {
279
535
  /**
280
- * The sentence turn-end is TOLD instead of being refused with, or null when no turn ended here.
536
+ * The sentence turn-end is TOLD with, or null when no turn ended here.
281
537
  *
282
- * It is not `shown`: the Stop rail has no context channel to inject prose into, which is what
283
- * `DELIVERS` already says, so the shell writes this one to stderr and answers 0.
538
+ * It is not `shown`: a note at turn-end keeps the agent working for one more turn, and this
539
+ * sentence is for the human the turn hands back to, so the shell writes it to stderr and answers 0.
284
540
  */
285
541
  readonly told: string | null;
286
542
  }
287
- export declare function whileBroken(events: readonly AdapterEvent[], surface: readonly string[], message: string): Outage;
543
+ export declare function whileBroken(events: readonly AdapterEvent[], message: string): Outage;
288
544
  /**
289
545
  * The one code `flow status` answers with.
290
546
  *
@@ -315,7 +571,11 @@ export declare function briefBlock(shown: readonly Shown[]): string;
315
571
  * moment. Exit 2 is the host's documented refusal channel and the model reads stderr on all three
316
572
  * rails, so flow has one refusal and one place to look when it fails to arrive.
317
573
  *
318
- * Injection stays the decision object, because there is no other channel for it.
574
+ * Injection stays the decision object, because there is no other channel for it — with one
575
+ * exception. Notes beside a refusal go on stderr after it: the host reads that stderr as the
576
+ * agent's next instruction, and its hooks reference says no JSON field on stdout can override an
577
+ * exit 2, so stderr is the one place both are sure to arrive. Only a held turn has any: before a
578
+ * call, the shell judges no notes once something refused (`ridesRefusal`).
319
579
  */
320
580
  export declare function toResult(hook: HookEvent, answer: Answer): HookResult;
321
581
  /**
@@ -326,20 +586,43 @@ export declare function toResult(hook: HookEvent, answer: Answer): HookResult;
326
586
  * naming an event that never happened. The banner comes off the MOMENT, which every rail has,
327
587
  * rather than off the event, which only the harness ones do.
328
588
  */
329
- export declare function refused(refusals: readonly Refused[]): HookResult | null;
589
+ export declare function refused(refusals: readonly Refused[], undone?: Undone): HookResult | null;
590
+ /**
591
+ * What the after-call rail says when it could not see what the call changed.
592
+ *
593
+ * FAIL LOUD, NEVER FAIL CLEAN. A snapshot that could not be taken, or a diff that could not be
594
+ * read, is not an empty delta: the call is not waved through as having changed nothing, and nothing
595
+ * is put back on a guess. Exit 2 on this rail cannot block — the call has run — so it is a report,
596
+ * and it goes where the model reads the tool's result.
597
+ */
598
+ export declare function deltaFault(fault: string): HookResult;
330
599
  /** Hand the agent some context and allow — the one shape every steering rail emits. */
331
600
  export declare function decision(hook: HookEvent, additionalContext: string): string;
601
+ /** The rails that run once a tool call has landed: their exit 2 reports, it cannot block. */
602
+ export declare const AFTER_CALL: ReadonlySet<HookEvent>;
332
603
  /**
333
- * What the guard says when it could not read its own payload.
604
+ * The answer a rail gives when it could not read its payload — and which rails must be LOUD.
334
605
  *
335
606
  * The three outcomes of a read are not one outcome. "There was nothing to guard" is safe and common
336
607
  * — a hand-run, an empty stdin — and silence is the right answer to it. "I could not SEE what I was
337
608
  * guarding" is the old engine's invisible swallow: both were folded into `{}`, so a PreToolUse rail
338
- * holding an unreadable payload found no tool call, allowed, and told nobody. The sentence is the
339
- * same on every rail; what differs is whether the rail blocks with it, and that is the caller's
340
- * decision because it is a fact about what the event is FOR.
609
+ * holding an unreadable payload found no tool call, allowed, and told nobody. So every rail says one
610
+ * fault line, and it keeps READ apart from PARSE: a payload that never arrived and one that arrived
611
+ * and was not JSON are different faults. The line names the hook and the cause, never "the guard",
612
+ * because on an after-call rail the call has already run and must not read as refused (#12).
613
+ *
614
+ * The three rails that judge a tool call answer exit 2: PreToolUse, where it refuses the call it
615
+ * could not see, and the two after-call rails, where every change the call made is judged — there
616
+ * exit 2 blocks nothing (the call has run) and puts the line in front of the model beside the
617
+ * result, so a call whose changes went unjudged is never passed as clean. The rest only add a
618
+ * breadcrumb or end a turn, and say it on stderr with exit 0.
619
+ *
620
+ * PreToolUse and the annotating rails print the line bare; an after-call rail wraps it in
621
+ * `deltaFault`, the one message for "flow could not see what this call changed", because the host
622
+ * labels its exit 2 a "blocking error" and the bare line read as a refusal of a call that had
623
+ * already landed (#12).
341
624
  */
342
- export declare function faultText(hook: HookEvent, why: "unreadable" | "unparseable", detail: string): string;
625
+ export declare function readFault(hook: HookEvent, why: "unreadable" | "unparseable", detail: string): HookResult;
343
626
  /** One line of a transcript, parsed. `record` is the raw object — this file reads it, nobody else. */
344
627
  export interface TranscriptLine {
345
628
  /** The record's `type`: `user`, `assistant`, or whatever else the host writes. */
@@ -435,28 +718,31 @@ export declare function briefHead(jsonl: string, cap?: number): string;
435
718
  * outright — which is every repo's own first commit.
436
719
  */
437
720
  export declare function branchFromHead(head: string): string | null;
438
- /**
439
- * The sidecar beside a subagent's transcript: `…/agent-<id>.jsonl` → `…/agent-<id>.meta.json`.
440
- *
441
- * The host writes one for every spawn — present 335/335 in the probe — and it is the strongest
442
- * evidence in the system precisely because the session did not write it. A session could claim to
443
- * be anything, and permissions resting on the claim would rest on a lie.
444
- */
445
- export declare function sidecarPath(transcriptPath: string): string | null;
446
721
  /**
447
722
  * Host-written evidence about one session × agent, assembled — the shape a classifier sees.
448
723
  *
449
- * The sidecar's absence is itself the evidence for the top-level session: there is no positive
450
- * marker for being the parent, only the absence of having been spawned. A sidecar that will not
451
- * parse is NOT the same as none — the session was spawned, we simply cannot say as what — so
452
- * `subagent` stays true and `agentType` is left undefined, and a category keyed on a specific type
453
- * declines rather than the parent category wrongly claiming it.
724
+ * The payload's `agent_id` is what makes a call a subagent's, whether or not any file exists for
725
+ * it, and its `agent_type` is the type that subagent was started as. Both are in the message the
726
+ * host writes, which the session cannot change. A subagent whose message carries no type is
727
+ * spawned as something unknown: `subagent` stays true and `agentType` is left undefined, so a
728
+ * category keyed on a specific type declines rather than the parent category wrongly claiming it.
729
+ * The parent is recognised by ABSENCE — there is no positive marker for being the top-level
730
+ * session, only the absence of having been spawned.
731
+ *
732
+ * `transcript` is this agent's own transcript, a subagent's included, and its head is the brief.
454
733
  */
455
734
  export declare function sessionFactsFrom(args: {
456
735
  readonly payload: HookPayload;
457
736
  readonly transcript: string | null;
458
- readonly sidecar: string | null;
459
737
  }): SessionFacts;
738
+ /**
739
+ * Which agent one hook message came from — the one reading of it, for its state, its classification
740
+ * and its log rows alike.
741
+ *
742
+ * The type is read only beside an `agent_id`: the host also sends `agent_type` on a top-level
743
+ * session started with `--agent`, and that session is still the parent.
744
+ */
745
+ export declare function agentOf(payload: HookPayload): AgentOf;
460
746
  /**
461
747
  * A tool call → the row that records it, or null when the payload names no tool at all.
462
748
  *
@@ -466,6 +752,36 @@ export declare function sessionFactsFrom(args: {
466
752
  * name, because "the session ran fifty greps here" is a fact about the area.
467
753
  */
468
754
  export declare function toolRow(payload: HookPayload, root?: string): Row | null;
755
+ /**
756
+ * The files a Bash call reads by name, one `read` row each — written beside its tool row, so the
757
+ * record counts a `cat` of a file as a touch of it the way it counts a Read.
758
+ *
759
+ * Files only. A directory a search walks is an area, not a file anybody read, and counted as one it
760
+ * would name a folder as a node of the tree the rows are drawn into.
761
+ */
762
+ export declare function shellReadRows(payload: HookPayload, root: string, home?: string): Row[];
763
+ /**
764
+ * The files one call changed that its own tool row does not already say, one `write` row each —
765
+ * what a heredoc, a `sed -i`, a script or a recipe wrote, and what an Edit's call changed besides
766
+ * the file it named.
767
+ *
768
+ * A SPAWN writes none of its own. The Agent call's diff holds everything its subagent wrote through
769
+ * calls that are logged under their own rows, and counted again here every edit a builder made would
770
+ * be in the record twice.
771
+ */
772
+ export declare function writeRows(payload: HookPayload, changes: readonly Change[], root: string): Row[];
773
+ /**
774
+ * Claude Code's own list of the files a command changed, where the host sent one, set against
775
+ * flow's diff — a `mismatch` row when they disagree, and nothing when they agree or the host said
776
+ * nothing.
777
+ *
778
+ * A CROSS-CHECK, NEVER AN INPUT. The host's list (`bashEditDiff`, a public beta, on by default only
779
+ * in some permission modes) decides nothing here: it is recorded so a reader of `flow facts` can see
780
+ * where the two views of one call differ. A host path outside the repo is not a file flow could
781
+ * have seen, so it is left out rather than reported; and when the host says its list was cut short
782
+ * (`moreFiles`), a file only flow saw is not a disagreement anybody can claim.
783
+ */
784
+ export declare function mismatchRow(payload: HookPayload, changes: readonly Change[], root: string): Row | null;
469
785
  /**
470
786
  * The variables git exports into a hook's environment, which REDIRECT any nested `git` command back
471
787
  * at the parent repo — a `git init` in a temp directory would suddenly write the real index.
@@ -520,14 +836,24 @@ export declare function classifyStore(paths: readonly string[]): {
520
836
  transcripts: TranscriptRef[];
521
837
  ignored: string[];
522
838
  };
839
+ /**
840
+ * The path of THIS agent's own transcript, from a live payload — null when the payload names none.
841
+ *
842
+ * On a subagent's call the host sends the PARENT's `transcript_path`, and keeps the subagent's own
843
+ * transcript under it, in the store's layout: `…/<session>.jsonl` →
844
+ * `…/<session>/subagents/agent-<agent_id>.jsonl`. The parent's path is the parent's transcript.
845
+ * Null too when a subagent's half is not what that layout needs — an id that could step out of the
846
+ * folder included, since it becomes part of a path that is then read.
847
+ */
848
+ export declare function ownTranscriptPath(payload: HookPayload): string | null;
523
849
  /**
524
850
  * The sidecar the host writes beside every subagent transcript, read.
525
851
  *
526
852
  * `toolUseId` is the field that makes this more than a label: it is the id of the parent's own
527
- * `Agent` tool call, so a sidecar and the block that spawned it join exactly. That join is what
528
- * lets cost attribution and classification agree about who an actor was — the sidecar says what a
529
- * spawn WAS, the parent's block says when it happened and what it was asked for, and neither
530
- * alone answers both.
853
+ * `Agent` tool call, so a sidecar and the block that spawned it join exactly. That join names an
854
+ * actor for cost attribution, after the fact — the sidecar says what a spawn WAS, the parent's
855
+ * block says when it happened and what it was asked for, and neither alone answers both. A live
856
+ * session is not classified from it: that reads the agent type in its hook message.
531
857
  */
532
858
  export interface SpawnMeta {
533
859
  readonly agentType: string | null;
@@ -577,9 +903,10 @@ export interface Spawn {
577
903
  * Every spawn a parent transcript made, by tool_use id — the parent's half of the join.
578
904
  *
579
905
  * It is the WEAKER half and is read as such: what the parent asked for is a claim by the session
580
- * being classified, while the sidecar is host-written and unforgeable. The classification order
581
- * (engine, `spawnedAs`) reads the sidecar and never this. What this is for is everything the
582
- * sidecar cannot say — when the spawn happened, and where in the parent's own transcript to look.
906
+ * being classified, while the sidecar is host-written and unforgeable, and so outranks it here.
907
+ * Neither decides who a live session is: classification (engine, `spawnedAs`) reads the agent type
908
+ * in the hook message. What this is for is everything the sidecar cannot say — when the spawn
909
+ * happened, and where in the parent's own transcript to look.
583
910
  */
584
911
  export declare function spawnsIn(jsonl: string): Map<string, Spawn>;
585
912
  /** Text with terminal colour codes taken out — a recorded command line is full of them. */
@@ -610,6 +937,8 @@ export type TranscriptEvent = {
610
937
  readonly input: Record<string, unknown>;
611
938
  /** The file the call names, repo-relative. Empty when it names none. */
612
939
  readonly path: string;
940
+ /** The repo files a Bash call reads by name (`bashReads`), when it reads any. */
941
+ readonly reads?: readonly string[];
613
942
  };
614
943
  /**
615
944
  * A transcript → the narrative events in it: typed prompts, tool calls, and their results.
@@ -620,10 +949,16 @@ export type TranscriptEvent = {
620
949
  * doing the same over the same files, so "what is an assistant line" had two answers free to
621
950
  * drift. It reads through `transcriptLines` now, and owns only the pairing.
622
951
  */
623
- export declare function parseEvents(jsonl: string, root?: string): TranscriptEvent[];
952
+ export declare function parseEvents(jsonl: string, root?: string, home?: string): TranscriptEvent[];
624
953
  /** Does this prompt read as the human pushing back? */
625
954
  export declare function isCorrection(prose: string): boolean;
626
- /** The tools a repo's justfile actually drives — what a direct invocation would be bypassing. */
955
+ /**
956
+ * The tools a repo's justfile actually drives — what a direct invocation would be bypassing.
957
+ *
958
+ * Each recipe's body is read through the parse as one script, so a loop or an `if` that spans its
959
+ * lines is read whole; a body that does not parse whole is read line by line, and a line that does
960
+ * not parse names nothing. Just's own `@` and `-` line prefixes are dropped first.
961
+ */
627
962
  export declare function recipeTools(justfile: string): string[];
628
963
  /** What one Bash line was: the recipes it drove, the tools it went round, and whether it committed. */
629
964
  export interface BashClass {
@@ -631,7 +966,12 @@ export interface BashClass {
631
966
  readonly bypasses: readonly string[];
632
967
  readonly commits: boolean;
633
968
  }
634
- /** Read one command line against the repo's own recipe tools. HEURISTIC, like everything below. */
969
+ /**
970
+ * Read one command line against the repo's own recipe tools, from its parse. A heredoc's body is
971
+ * text the command is fed, so a file being written that names a tool is not a run of it — while
972
+ * the line around the heredoc is judged like any other, and so is a body a shell runs. A line that
973
+ * does not parse is nothing: no recipe, no bypass, no commit.
974
+ */
635
975
  export declare function classifyBash(command: unknown, tools: readonly string[]): BashClass;
636
976
  /** A pointer into a transcript — the shape every heuristic finding takes. */
637
977
  export interface Pointer {
@@ -662,6 +1002,10 @@ export interface Narrative {
662
1002
  readonly stats: {
663
1003
  readonly edits: number;
664
1004
  readonly writes: number;
1005
+ /** Files changed round the edit tools — a heredoc, a `sed -i`, a script — from flow's own diff. */
1006
+ readonly shellWrites: number;
1007
+ /** Files a Bash command read by name. */
1008
+ readonly shellReads: number;
665
1009
  readonly commits: number;
666
1010
  readonly recipes: Readonly<Record<string, number>>;
667
1011
  readonly bypasses: Readonly<Record<string, number>>;
@@ -671,6 +1015,11 @@ export interface Narrative {
671
1015
  readonly bypassSites: readonly Bypass[];
672
1016
  readonly loops: readonly EditLoop[];
673
1017
  readonly retries: readonly Retry[];
1018
+ /** The paths behind the two shell counts, with how often each. */
1019
+ readonly shell: {
1020
+ readonly writes: Readonly<Record<string, number>>;
1021
+ readonly reads: Readonly<Record<string, number>>;
1022
+ };
674
1023
  /** Every in-repo file the session touched, with how often. The facts layer scores coverage. */
675
1024
  readonly touched: Readonly<Record<string, number>>;
676
1025
  }
@@ -679,8 +1028,14 @@ export interface Narrative {
679
1028
  *
680
1029
  * `tools` is INJECTED — the shell reads the justfile — so the same call is reproducible from a
681
1030
  * fixture, which is the same discipline the guard itself is built on.
1031
+ *
1032
+ * `rows` is the conversation's own stream, when it has one, and it is where a SHELL WRITE comes
1033
+ * from: a transcript says what command ran, never what it changed, and only flow's diff knows that.
1034
+ * A conversation with no stream has no shell writes to count, which is why the count reads zero
1035
+ * there rather than a guess. Shell READS come from the transcript, like every Read does, so they are
1036
+ * counted in every conversation.
682
1037
  */
683
- export declare function narrative(events: readonly TranscriptEvent[], tools?: readonly string[]): Narrative;
1038
+ export declare function narrative(events: readonly TranscriptEvent[], tools?: readonly string[], rows?: readonly Row[]): Narrative;
684
1039
  /**
685
1040
  * Every narrative reading merged into one, each pointer tagged with the transcript it came from.
686
1041
  *
@@ -692,12 +1047,16 @@ export declare function mergeNarratives(each: readonly {
692
1047
  session: string;
693
1048
  read: Narrative;
694
1049
  }[]): Narrative;
1050
+ /**
1051
+ * The repo's own Claude Code settings file, relative to the repo root: guarded here, and where
1052
+ * `settingsFileFor` registers a dev dependency's hooks, and `flow uninstall` takes them out.
1053
+ */
1054
+ export declare const REPO_SETTINGS = ".claude/settings.json";
695
1055
  /**
696
1056
  * The HOST's half of the guard: where the hooks that invoke any of it are registered.
697
1057
  *
698
1058
  * Fixed, because these are the harness's own file names and not a repo's choice — unlike the
699
- * repo's half, which is `configSurface` reading the config's imports. The old list spelled the two
700
- * halves as one constant, with one repo's pack folder standing in for every repo's.
1059
+ * repo's half, which `guardPaths` reads out of the config's imports.
701
1060
  *
702
1061
  * THE GUARD PACK KEEPS ITS OWN, WIDER LIST, and the difference is deliberate — this is the one
703
1062
  * place that says so, because two lists that look like copies are read as drift until somebody
@@ -717,7 +1076,18 @@ export declare function mergeNarratives(each: readonly {
717
1076
  * in, and that is the coupling the fence exists to prevent.
718
1077
  */
719
1078
  export declare const HOST_SURFACE: readonly string[];
720
- /** Every file that decides what the guard does here — the config and its packs, plus the host's. */
1079
+ /**
1080
+ * Every file that decides what the guard does here: the config and the packs it imports, then the
1081
+ * host's half. `weakenedAfterBlock` reads it — "a rail refused, and then somebody edited the guard".
1082
+ *
1083
+ * The repo's half is READ OUT OF THE CONFIG'S TEXT (`configImports`) rather than resolved, so it
1084
+ * answers the same whether or not the config loads, and it follows whatever a repo called its pack
1085
+ * folder — no folder name is spelled into the engine.
1086
+ *
1087
+ * The FOLDER of each imported pack comes with the file, which is the one deliberate widening. A
1088
+ * pack imports its own helpers, and those the config never names. The import graph cannot be
1089
+ * walked from text, so the folder is the honest approximation.
1090
+ */
721
1091
  export declare function guardPaths(configText: string | null): readonly string[];
722
1092
  /** Is this one of the files that decides what the guard does? */
723
1093
  export declare function isGuardPath(path: string, paths: readonly string[]): boolean;
@@ -791,9 +1161,9 @@ export declare function health({ armed, rows, withRecord, analysed }: HealthArgs
791
1161
  *
792
1162
  * The sidecar says what a spawn WAS (`agentType`, unforgeable, written by the host) and the
793
1163
  * parent's own `Agent` block says when it happened and what was asked for. Joining them on
794
- * `toolUseId` is what lets classification and cost attribution name the same actor: a builder
795
- * spawned under a generic bucket is `general-purpose` in the parent's claim and `builder` in the
796
- * host's record, and only one of those is evidence.
1164
+ * `toolUseId` names an actor for cost attribution, after the fact: a builder spawned under a
1165
+ * generic bucket is `general-purpose` in the parent's claim and `builder` in the host's record,
1166
+ * and only one of those is evidence. A live session is classified from its hook message instead.
797
1167
  */
798
1168
  export interface SpawnRecord {
799
1169
  /** The conversation that spawned it. */
@@ -832,6 +1202,59 @@ export interface Uncovered {
832
1202
  * know whether the entries it just bound point anywhere near it.
833
1203
  */
834
1204
  export declare function uncoveredAreas(touched: Readonly<Record<string, number>>, entries: readonly Bound[]): Uncovered[];
1205
+ /**
1206
+ * Whether Claude Code's auto mode is steering the session towards shell edits — `bashFirst` on the
1207
+ * host's auto-mode attachment, which tells the model to read with `cat` and change files with `sed`,
1208
+ * heredocs and scripts rather than Read, Edit and Write. `variant` is the host's own word for how hard
1209
+ * (`strict` · `relaxed`), when it gave one.
1210
+ */
1211
+ export interface Steer {
1212
+ readonly shellFirst: boolean;
1213
+ readonly variant: string | null;
1214
+ }
1215
+ /**
1216
+ * The auto-mode steer a conversation is under, from the LAST auto-mode attachment in its transcript
1217
+ * — or null when it has none, or the last one is the host leaving auto mode.
1218
+ *
1219
+ * Read from the END, and only lines that name the attachment are parsed: a transcript is a couple
1220
+ * of megabytes and the answer is one record.
1221
+ */
1222
+ export declare function steerOf(jsonl: string): Steer | null;
1223
+ /**
1224
+ * The agent a session runs as (`--agent`, or the `agent` setting), from its own transcript — or
1225
+ * null when it runs as none.
1226
+ *
1227
+ * The host writes it as an `agent-setting` record, again as the session goes on. Read from the END,
1228
+ * and only lines that name the record are parsed, as `steerOf` does: the answer is one record.
1229
+ */
1230
+ export declare function agentSettingOf(jsonl: string): string | null;
1231
+ /** The permission mode the last recorded call was made under, or null when no row states one. */
1232
+ export declare function lastMode(rows: readonly Row[]): string | null;
1233
+ /** The permission modes the read conversations ran under, and how many of them auto mode steered. */
1234
+ export interface Modes {
1235
+ /** Recorded calls per permission mode. */
1236
+ readonly calls: Readonly<Record<string, number>>;
1237
+ /** Conversations whose transcript last stood in auto mode. */
1238
+ readonly auto: number;
1239
+ /** Of those, the ones steered towards shell edits, and by which variant. */
1240
+ readonly steered: number;
1241
+ readonly variants: Readonly<Record<string, number>>;
1242
+ }
1243
+ /** Each conversation's rows and steer, added up. */
1244
+ export declare function modesOf(each: readonly {
1245
+ readonly rows: readonly Row[];
1246
+ readonly steer: Steer | null;
1247
+ }[]): Modes;
1248
+ /** Where Claude Code's changed-file list and flow's diff disagreed, over the read conversations. */
1249
+ export interface Mismatches {
1250
+ readonly calls: number;
1251
+ /** Paths only the host listed, and how often. */
1252
+ readonly hostOnly: Readonly<Record<string, number>>;
1253
+ /** Paths only flow's diff held, and how often. */
1254
+ readonly flowOnly: Readonly<Record<string, number>>;
1255
+ }
1256
+ /** The `mismatch` rows, added up. */
1257
+ export declare function mismatchesIn(rows: readonly Row[]): Mismatches;
835
1258
  export interface Facts {
836
1259
  readonly root: string;
837
1260
  readonly store: {
@@ -864,6 +1287,10 @@ export interface Facts {
864
1287
  readonly actors: readonly SpawnRecord[];
865
1288
  /** A rail refused, and then somebody edited the guard. A pointer, never an accusation. */
866
1289
  readonly weakened: readonly Weakening[];
1290
+ /** The permission modes the read conversations ran under, and the auto-mode steer. */
1291
+ readonly modes: Modes;
1292
+ /** Where the host's changed-file list disagreed with flow's diff. */
1293
+ readonly mismatches: Mismatches;
867
1294
  /** How many conversations were marked read, when asked for. Null when not asked. */
868
1295
  readonly marked: number | null;
869
1296
  }
@@ -930,7 +1357,39 @@ export interface Registration {
930
1357
  /** The host's `matcher`. Absent means every occurrence of the event. */
931
1358
  readonly matcher?: string;
932
1359
  }
1360
+ /**
1361
+ * The repo's bin folder, relative to the repo root. flow never runs a `flow` from here: any package
1362
+ * can install one (Facebook's `flow-bin` does), and nothing guards the folder. `hostPath` drops it
1363
+ * from the PATH npm hands one run.
1364
+ */
1365
+ export declare const LOCAL_BIN = "node_modules/.bin";
1366
+ /** Where the repo's own `@jawache/flow` is, relative to the repo root: a dev dependency, or init's link. */
1367
+ export declare const PACKAGE_DIR = "node_modules/@jawache/flow";
1368
+ /** That package's own binary — the file its `bin` names. */
1369
+ export declare const PACKAGE_BIN = "node_modules/@jawache/flow/dist/flow.mjs";
1370
+ /**
1371
+ * The command each registration runs: the repo's own `@jawache/flow` first, then the PATH's.
1372
+ *
1373
+ * The host runs it through a shell with `$CLAUDE_PROJECT_DIR` set to the repo, so a repo with flow
1374
+ * only as a dev dependency answers its hooks, and its lockfile's version wins over a global one. It
1375
+ * runs the package's own file, by path, never a `flow` in node_modules/.bin, which any package can
1376
+ * put there. The same command goes in either settings file, so the host, which merges identical
1377
+ * commands, runs it once when it is in both.
1378
+ */
1379
+ export declare function hookCommand(hook: HookEvent): string;
933
1380
  export declare const HOOK_REGISTRATIONS: readonly Registration[];
1381
+ /**
1382
+ * WHICH SETTINGS FILE holds flow's hooks: the repo's own `.claude/settings.json` when the repo's
1383
+ * `package.json` lists `@jawache/flow` as a dependency or a dev dependency, and the machine-wide
1384
+ * file otherwise.
1385
+ *
1386
+ * A dev dependency guards that one repo: the repo's file is committed, so a fresh clone is guarded
1387
+ * after `npm install`, and a machine with no global flow is not handed a hook that fails in every
1388
+ * other repo it opens. A `package.json` that is missing, or will not parse, lists nothing.
1389
+ */
1390
+ export declare function settingsFileFor(manifest: string | null, machine: string): string;
1391
+ /** Does this `package.json` list `@jawache/flow` as a dependency or a dev dependency? */
1392
+ export declare function listsFlow(manifest: string | null): boolean;
934
1393
  /**
935
1394
  * Is this registration OURS?
936
1395
  *
@@ -944,17 +1403,63 @@ export declare function ourHookCommand(command: string): boolean;
944
1403
  /** The events this settings file already calls flow for. What makes init idempotent and status honest. */
945
1404
  export declare function registeredEvents(settings: unknown): string[];
946
1405
  /**
947
- * A settings file with flow's registrations in it — a NEW object, and only ever added to.
1406
+ * The events flow IS registered for, but with a matcher narrower than this build asks for.
1407
+ *
1408
+ * An older flow registered PostToolUse for six tools, because an after-call hook could only show a
1409
+ * breadcrumb then. Now it is where every change a tool makes is judged, so a registration on six
1410
+ * tools leaves a shell edit through any other tool unjudged — and it LOOKS registered. It is its own
1411
+ * red line for that reason, rather than disappearing inside "registered".
1412
+ */
1413
+ export declare function staleRegistrations(settings: unknown): string[];
1414
+ /**
1415
+ * flow's own registration in the OLD shape, the bare `flow hook <event>`: the word it answers to, or
1416
+ * null when the command is anything else. It finds flow on the PATH alone, so a repo with flow only
1417
+ * as a dev dependency answers none of its hooks. A command with a path to the binary is not this
1418
+ * shape: it names the copy it runs, and that is somebody's choice.
1419
+ */
1420
+ export declare function bareHook(command: string): HookEvent | null;
1421
+ /** The events this settings file calls flow for in the old shape — see `bareHook`. */
1422
+ export declare function bareRegistrations(settings: unknown): string[];
1423
+ /**
1424
+ * THE MACHINE-WIDE FILE, when a dev dependency's hooks are in the repo's own: flow's old-shape
1425
+ * commands there (`bareHook`) rewritten to today's, and nothing else touched — no registration
1426
+ * added, no matcher widened, no other key or hook changed. A NEW object; `rewritten` names the
1427
+ * events, empty when there was nothing to do.
1428
+ *
1429
+ * The host merges only IDENTICAL commands across its settings files. An old `flow hook stop` in the
1430
+ * machine-wide file beside today's command in the repo's would run flow twice for every event, so
1431
+ * every rule would fire twice and every refusal print twice. Made the same command, it runs once.
1432
+ */
1433
+ export declare function withMachineCommandsUpToDate(settings: unknown): {
1434
+ settings: unknown;
1435
+ rewritten: string[];
1436
+ };
1437
+ /**
1438
+ * A settings file with flow's registrations in it — a NEW object, added to, and with flow's own
1439
+ * entries brought up to date.
948
1440
  *
949
- * It is somebody's file and holds far more than us, so nothing is rewritten and nothing is removed:
950
- * a registration on our event that belongs to another tool keeps its place and flow stands beside
951
- * it. Adding what is already there is skipped, which is the whole of what makes `flow init`
952
- * idempotent — a second run has nothing to say.
1441
+ * It is somebody's file and holds far more than us, so nothing of anyone else's is rewritten and
1442
+ * nothing is removed: a registration on our event that belongs to another tool keeps its place and
1443
+ * flow stands beside it. Adding what is already there is skipped, which is the whole of what makes
1444
+ * `flow init` idempotent — a second run has nothing to say.
953
1445
  */
954
1446
  export declare function withRegistrations(settings: unknown): {
955
1447
  settings: unknown;
956
1448
  added: string[];
957
1449
  };
1450
+ /**
1451
+ * A settings file with flow's registrations taken out — a NEW object, and the reverse of
1452
+ * `withRegistrations`.
1453
+ *
1454
+ * On the events flow registers, each hook whose command is flow's (`ourHookCommand`, so an older
1455
+ * flow's bare `flow hook <event>` goes too) is removed; an entry left with no hook goes, an event left
1456
+ * with no entry goes, and a `hooks` key left empty goes. Every other key, event and hook is kept as
1457
+ * it was. `removed` names the events, in registration order, empty when there was nothing of flow's.
1458
+ */
1459
+ export declare function withoutRegistrations(settings: unknown): {
1460
+ settings: unknown;
1461
+ removed: string[];
1462
+ };
958
1463
  /**
959
1464
  * WHERE THE GATE LIVES, spelled once for everyone who names it.
960
1465
  *
@@ -968,24 +1473,139 @@ export declare const GATE_PATH = ".githooks/pre-commit";
968
1473
  /** What arms git, said the way a person types it — the report's line and the fitting's fix. */
969
1474
  export declare const SET_HOOKS_PATH = "git config core.hooksPath .githooks";
970
1475
  /**
971
- * How to arm a gate flow will not write over, in the ONE wording both readers use.
1476
+ * The gate's line that runs the repo's own `@jawache/flow` first: git runs the hook from the repo's
1477
+ * root, so `$PWD` is the repo. Written by `init`, looked for by `status`, and named in its fixes.
1478
+ */
1479
+ export declare const GATE_LOCAL_LINE: string;
1480
+ /** The gate's line that runs flow, last, so its exit code is the hook's. */
1481
+ export declare const GATE_RUN_LINE = "exec flow hook commit";
1482
+ /**
1483
+ * How to arm a gate flow will not write over, in the ONE wording both readers use. Both of the
1484
+ * gate's lines: the run line alone finds flow on the PATH alone, and a repo with flow only as a dev
1485
+ * dependency then fails every commit.
972
1486
  *
973
1487
  * `init` says it about the hook it just declined to overwrite; `status` says it about the hook it
974
1488
  * found unarmed. The same instruction, at the two moments a person can meet it — and two spellings
975
1489
  * of an instruction are two instructions the day one of them is edited.
976
1490
  */
977
- export declare const ADD_THE_GATE_LINE = "add `flow commit $(git diff --cached --name-only)` to it";
1491
+ export declare const ADD_THE_GATE_LINE: string;
1492
+ /**
1493
+ * How to bring a gate written for the old verb up to date, in the ONE wording both readers use.
1494
+ *
1495
+ * The old hook still runs the gate, which is why it is not simply broken: it hands flow a list that
1496
+ * leaves out deletions, and exits before calling flow at all when a commit only deletes files.
1497
+ * `status` says that much; this is the instruction alone, so it ends `init`'s sentence cleanly.
1498
+ */
1499
+ export declare const UPDATE_THE_GATE_LINE: string;
978
1500
  /**
979
1501
  * The pre-commit hook, verbatim.
980
1502
  *
981
- * `flow commit <files…>` rather than a sixth hook, because the commit moment is not delivered by any
982
- * harness — git's own hook is what fires it, which is why every harness gets the gates for free. The
983
- * staged set is computed here rather than inside flow: it is git's own question, and this file is
984
- * already a git hook.
1503
+ * `flow hook commit` — git's hook under the same verb as the harness's hooks. It passes nothing:
1504
+ * flow asks git what the commit adds, changes and deletes, so there is one source of the staged set
1505
+ * whoever calls the gate, and nothing in this file to filter it or to skip flow when it is empty.
985
1506
  */
986
- export declare const PRE_COMMIT = "#!/bin/sh\n# The flow commit gate. Written by `flow init`; `git config core.hooksPath .githooks` arms it.\n#\n# Every at(commit) guardrail runs over the staged set and a block exits non-zero, which is how git\n# refuses the commit. This is the half of the promise the PreToolUse rail cannot make: it fires for\n# a human's commit, a merge and CI, with no session in the room.\nstaged=$(git diff --cached --name-only --diff-filter=ACMR)\n[ -z \"$staged\" ] && exit 0\n# shellcheck disable=SC2086\nexec flow commit $staged\n";
987
- /** Does this pre-commit hook call flow? Asked by `init` before writing, and by `status` after. */
1507
+ export declare const PRE_COMMIT: string;
1508
+ /** Does this pre-commit hook call flow's gate? Asked by `init` before writing, and by `status` after. */
988
1509
  export declare function armsFlow(text: string | null): boolean;
1510
+ /** Does it call the gate's OLD verb, `flow commit` — a gate that runs, and misses deletions? */
1511
+ export declare function armsOldFlow(text: string | null): boolean;
1512
+ /**
1513
+ * The pre-commit hook with flow taken out: `delete` when every command in it is one a `flow init`
1514
+ * wrote, the hook less the lines that run flow when it holds commands of its own, and null when no
1515
+ * line of it is flow's. A line is flow's only when it is, trimmed, exactly one flow wrote: a line
1516
+ * somebody spelled themselves is theirs to remove.
1517
+ */
1518
+ export declare function gateWithoutFlow(text: string | null): {
1519
+ readonly delete: true;
1520
+ } | {
1521
+ readonly delete: false;
1522
+ readonly body: string;
1523
+ } | null;
1524
+ /**
1525
+ * The lines of a hook that call flow, trimmed, in order: each line a `flow init` wrote into a gate
1526
+ * or told a person to add, today's and every older release's, and any other line that runs
1527
+ * `flow hook commit` or `flow commit`. Comments call nothing. What uninstall names for a person to
1528
+ * delete from a hook that is not flow's to edit.
1529
+ */
1530
+ export declare function flowLinesOf(text: string | null): string[];
1531
+ /** What the gate judges: the staged changes, or the whole tree as if all of it were staged. */
1532
+ export type CommitScope = "staged" | "all";
1533
+ /**
1534
+ * `flow hook commit [--all]`, read. Null is a refusal: a file list here would be a second source of
1535
+ * the staged set. `legacy` is the old `flow commit <files…>`, whose list an older hook still passes,
1536
+ * and it is dropped rather than refused so that hook keeps running the gate.
1537
+ */
1538
+ export declare function commitScope(args: readonly string[], legacy: boolean): CommitScope | null;
1539
+ /** The folder git runs hooks from, as the shell found it. One reading, for init and status alike. */
1540
+ export interface HooksFolder {
1541
+ /** As git names it (`git rev-parse --git-path hooks`), spelled by `hooksDirOf`. */
1542
+ readonly dir: string;
1543
+ /** The name of every file in it, `.sample` files included: which of them count is decided here. */
1544
+ readonly files: readonly string[];
1545
+ /** The text of the pre-commit hook a person edits for that folder (`preCommitOf(dir)`), or null. */
1546
+ readonly preCommitText: string | null;
1547
+ /** That file has the execute bit this user needs to run it. False when there is no file. */
1548
+ readonly preCommitExecutable: boolean;
1549
+ }
1550
+ /**
1551
+ * What the pre-commit hook git runs lacks before it runs flow's gate: `add` flow's two lines;
1552
+ * `create` the hook, which is not there; `chmod` a hook that calls flow but that git ignores,
1553
+ * because it lacks the execute bit; or `add-and-chmod`, both.
1554
+ */
1555
+ export type HookFix = "add" | "create" | "chmod" | "add-and-chmod";
1556
+ /** Git runs other hooks, and whether the pre-commit hook it runs calls flow. */
1557
+ export interface OtherHooks {
1558
+ /** The folder git runs hooks from. */
1559
+ readonly dir: string;
1560
+ /** The pre-commit hook a person edits to call flow. */
1561
+ readonly preCommit: string;
1562
+ /** The first fix, or null when that hook calls flow's gate and git runs it: flow is installed. */
1563
+ readonly fix: HookFix | null;
1564
+ }
1565
+ /** Other hooks that do not run flow's gate yet, so flow is not installed. */
1566
+ export type UnwiredHooks = OtherHooks & {
1567
+ readonly fix: HookFix;
1568
+ };
1569
+ /** Does git run other hooks without flow's gate among them? */
1570
+ export declare function unwired(other: OtherHooks | null): other is UnwiredHooks;
1571
+ /**
1572
+ * The pre-commit hook a person edits for this hooks folder.
1573
+ *
1574
+ * Usually the folder's own `pre-commit`. Husky is the exception: it points git at `.husky/_`, a
1575
+ * folder of stubs it rewrites on every install, and each stub runs the hook of the same name one
1576
+ * folder up, so the file a person edits is `.husky/pre-commit`. Any folder named `_` is read that way.
1577
+ */
1578
+ export declare function preCommitOf(dir: string): string;
1579
+ /**
1580
+ * The hooks folder as a person would type it, from git's answer to `git rev-parse --git-path hooks`
1581
+ * run at the repo root: repo-relative when it is inside the repo, absolute when it is not, such as
1582
+ * a worktree's shared `.git/hooks`. Resolved, so `.githooks/` and `./.githooks` both read `.githooks`.
1583
+ */
1584
+ export declare function hooksDirOf(root: string, said: string): string;
1585
+ /**
1586
+ * Does git run hooks other than flow's here? Null when it does not: `core.hooksPath` is flow's own
1587
+ * folder, or it is unset and `.git/hooks` holds nothing but git's own `.sample` files. A
1588
+ * `core.hooksPath` anywhere else counts whatever is in it, and any other file in `.git/hooks` counts,
1589
+ * not only a `pre-commit`: Git LFS installs `pre-push` and `post-checkout` there.
1590
+ */
1591
+ export declare function otherHooks(hooksPath: string | null, folder: HooksFolder): OtherHooks | null;
1592
+ /** The words the block opens with, fixed by the docs, for every reader that looks for them. */
1593
+ export declare const NOT_INSTALLED = "FLOW IS NOT INSTALLED";
1594
+ /**
1595
+ * Git could not name the folder it runs hooks from, in the ONE wording init's report and status's
1596
+ * line share. No advice: telling a husky repo to set `core.hooksPath` is what turns husky off.
1597
+ */
1598
+ export declare const HOOKS_UNKNOWN = "git could not name the folder it runs hooks from, so flow cannot tell whether a commit runs its gate. Run `git rev-parse --git-path hooks` here to see why.";
1599
+ /** Other hooks whose pre-commit calls flow, in the ONE wording init's note and status's line share. */
1600
+ export declare function wiredLine(other: OtherHooks): string;
1601
+ /**
1602
+ * The message for other hooks that do not run flow, in the ONE wording init's block and status's
1603
+ * line share: the folder git runs hooks from, why its pre-commit hook does not run flow's gate, and
1604
+ * the two ways to finish. The first fix is whatever that hook lacks: flow's two lines, the hook
1605
+ * itself, or the execute bit. Init never takes either fix itself: editing or creating another
1606
+ * tool's hook, or moving its hooks, is the person's call.
1607
+ */
1608
+ export declare function notInstalledLines(other: UnwiredHooks): string[];
989
1609
  /** The config file init writes: the demo, or the bare one `--empty` asks for. */
990
1610
  export declare function scaffold(empty: boolean): string;
991
1611
  /** What the shell found before anything was written. Every one is a stat, not a judgement. */
@@ -1000,8 +1620,11 @@ export interface InitFacts {
1000
1620
  readonly hasFlowDir: boolean;
1001
1621
  /** `git config core.hooksPath` as it stands. */
1002
1622
  readonly hooksPath: string | null;
1623
+ /** The folder git runs hooks from, or null outside git or when git could not say. */
1624
+ readonly hooksFolder: HooksFolder | null;
1625
+ /** The settings file flow's hooks go in: the repo's own, repo-relative, or the machine-wide one. */
1003
1626
  readonly settingsPath: string;
1004
- /** The parsed host settings file. */
1627
+ /** That file, parsed. */
1005
1628
  readonly settings: unknown;
1006
1629
  /**
1007
1630
  * The settings file is THERE and would not parse.
@@ -1013,6 +1636,15 @@ export interface InitFacts {
1013
1636
  * settings.json holding JSONC comments: the write was one branch away.
1014
1637
  */
1015
1638
  readonly settingsUnreadable: boolean;
1639
+ /**
1640
+ * The machine-wide settings file, read, when flow's hooks go in the repo's own file — a dev
1641
+ * dependency's — and null when they go in the machine-wide file itself.
1642
+ */
1643
+ readonly machine: {
1644
+ readonly path: string;
1645
+ readonly settings: unknown;
1646
+ readonly unreadable: boolean;
1647
+ } | null;
1016
1648
  /** Does `@jawache/flow` already resolve from this repo? */
1017
1649
  readonly resolves: boolean;
1018
1650
  /**
@@ -1036,17 +1668,36 @@ export interface InitPlan {
1036
1668
  readonly gate: Write | null;
1037
1669
  /** A pre-commit hook that is somebody else's. Never overwritten — reported, with the line to add. */
1038
1670
  readonly gateKept: boolean;
1671
+ /** …and it is flow's own, from before `flow hook commit` — reported with the line to replace. */
1672
+ readonly gateOld: boolean;
1039
1673
  readonly notGit: boolean;
1040
1674
  readonly flowDir: boolean;
1041
1675
  /** The package to link into `node_modules/@jawache/flow`, or null when flow already resolves. */
1042
1676
  readonly link: string | null;
1677
+ /** Point git at `.githooks`: a git repo whose git runs no other hooks, and not already there. */
1043
1678
  readonly hooksPath: boolean;
1679
+ /** Git runs other hooks, so `core.hooksPath` is left alone. Flow is not installed unless it is wired. */
1680
+ readonly otherHooks: OtherHooks | null;
1681
+ /**
1682
+ * A git repo whose git could not name the folder it runs hooks from, so `core.hooksPath` is left
1683
+ * alone too: that folder may hold another tool's hooks, and setting it would turn them off.
1684
+ */
1685
+ readonly hooksUnknown: boolean;
1044
1686
  /** The rewritten settings, or null when they already call flow — or could not be read. */
1045
1687
  readonly settings: {
1046
1688
  readonly path: string;
1047
1689
  readonly value: unknown;
1048
1690
  } | null;
1049
1691
  readonly registered: readonly string[];
1692
+ /**
1693
+ * The machine-wide file with flow's old commands brought up to date, for a dev dependency whose
1694
+ * hooks are in the repo's own file — or null when it has none, or could not be read.
1695
+ */
1696
+ readonly machine: {
1697
+ readonly path: string;
1698
+ readonly value: unknown;
1699
+ readonly rewritten: readonly string[];
1700
+ } | null;
1050
1701
  /** The settings file was there and would not parse, so nothing was registered. Said out loud. */
1051
1702
  readonly settingsUnreadable: string | null;
1052
1703
  }
@@ -1057,9 +1708,125 @@ export declare function planInit(facts: InitFacts): InitPlan;
1057
1708
  *
1058
1709
  * A setup command that prints "done" is a command nobody can check. Failures are handed in rather
1059
1710
  * than thrown because a PARTIAL init is the interesting case: the config landed, the settings file
1060
- * was read-only, and the person needs to know exactly that.
1711
+ * was read-only, and the person needs to know exactly that. `colour` is `wantsColour`'s answer for
1712
+ * the output this is written to.
1061
1713
  */
1062
- export declare function initLines(plan: InitPlan, failures: readonly string[]): string[];
1714
+ export declare function initLines(plan: InitPlan, failures: readonly string[], colour?: boolean): string[];
1715
+ /**
1716
+ * Does this output get colour? Only in a terminal, and never with `NO_COLOR` set to anything but an
1717
+ * empty string, which is the convention at no-color.org. A pipe, a file and a test harness get the
1718
+ * plain text, so nothing that reads the output has escape codes to strip.
1719
+ */
1720
+ export declare function wantsColour(terminal: boolean, noColor: string | undefined): boolean;
1721
+ /**
1722
+ * Init's exit code. 1 when it could not finish, and 1 when flow is not installed or cannot tell: a
1723
+ * script chaining `flow init && …` is asking whether the repo is guarded, and a gate git never runs
1724
+ * is not.
1725
+ */
1726
+ export declare function initCode(plan: InitPlan, failures: readonly string[]): 0 | 1;
1727
+ /** What the shell found before anything was removed. Every one is a stat, not a judgement. */
1728
+ export interface UninstallFacts {
1729
+ readonly hasConfig: boolean;
1730
+ /** The gate as it stands, or null when there is none. */
1731
+ readonly gateText: string | null;
1732
+ /** `git config core.hooksPath` as it stands. */
1733
+ readonly hooksPath: string | null;
1734
+ /** The repo's top folder when the run is in a folder below it, else null. */
1735
+ readonly subfolderOf: string | null;
1736
+ /** The names in `.githooks/`, or null when there is no such folder. */
1737
+ readonly hooksDir: readonly string[] | null;
1738
+ /**
1739
+ * The folder git runs hooks from, read at the repo's top folder as init and status read it, or
1740
+ * null outside git or when git could not say.
1741
+ */
1742
+ readonly hooksFolder: HooksFolder | null;
1743
+ readonly hasFlowDir: boolean;
1744
+ /** `node_modules/@jawache/flow` is a symlink: the link `flow init` makes. A real folder is an install. */
1745
+ readonly linked: boolean;
1746
+ /** Does the repo's `package.json` list `@jawache/flow`? Then flow's hooks are in the repo's own settings file. */
1747
+ readonly listed: boolean;
1748
+ /** The repo's `.claude/settings.json`, parsed; null when there is none. */
1749
+ readonly settings: unknown;
1750
+ /** …it is there and would not parse. */
1751
+ readonly settingsUnreadable: boolean;
1752
+ /** `--machine` was given: the machine-wide settings file is flow's to clean too. */
1753
+ readonly machineFlag: boolean;
1754
+ /** The machine-wide settings file: its absolute path, its JSON (null when there is none), and whether it would not parse. */
1755
+ readonly machineFile: {
1756
+ readonly path: string;
1757
+ readonly settings: unknown;
1758
+ readonly unreadable: boolean;
1759
+ };
1760
+ }
1761
+ /** A settings file with flow's registrations taken out: where it is, what to write, and the events that went. */
1762
+ export interface SettingsWithout {
1763
+ readonly path: string;
1764
+ readonly value: unknown;
1765
+ readonly removed: readonly string[];
1766
+ }
1767
+ /** Everything `flow uninstall` will do, decided before a byte is removed. */
1768
+ export interface UninstallPlan {
1769
+ /** The gate: deleted, rewritten without flow's lines, or null when nothing in it is flow's. */
1770
+ readonly gate: {
1771
+ readonly path: string;
1772
+ readonly body: string | null;
1773
+ } | null;
1774
+ /**
1775
+ * A pre-commit hook that still calls flow after the run, in lines flow did not write there: that
1776
+ * hook, as the report names it, and those lines, for a person to delete before `npm uninstall`.
1777
+ * The hook git runs from another folder, such as husky's `.husky/pre-commit`, which uninstall
1778
+ * never edits, as `flow init` never does; else what is left of `.githooks/pre-commit`. Null when
1779
+ * neither calls flow.
1780
+ */
1781
+ readonly hookCallsFlow: {
1782
+ readonly path: string;
1783
+ readonly lines: readonly string[];
1784
+ } | null;
1785
+ /** Unset `core.hooksPath`: it is `.githooks`, the run is at the repo's top folder, and the folder holds no other hook. */
1786
+ readonly hooksPath: boolean;
1787
+ /**
1788
+ * `core.hooksPath` is `.githooks` but the run is in a subfolder: the repo's top folder, where
1789
+ * `flow uninstall` removes it, or null.
1790
+ */
1791
+ readonly hooksPathFrom: string | null;
1792
+ /** The hooks that keep `core.hooksPath` at `.githooks`, named for the report. */
1793
+ readonly hooksKept: readonly string[];
1794
+ /** Remove `.githooks/`, which nothing else is in. */
1795
+ readonly hooksDir: boolean;
1796
+ readonly flowDir: boolean;
1797
+ readonly link: boolean;
1798
+ /** The repo's settings file without flow's registrations, or null when it holds none. */
1799
+ readonly settings: SettingsWithout | null;
1800
+ /** With `--machine`: the machine-wide settings file without flow's registrations, or null when it holds none. */
1801
+ readonly machine: SettingsWithout | null;
1802
+ /** The settings files flow's to clean that would not parse: nothing written to them, each said out loud. */
1803
+ readonly unreadable: readonly string[];
1804
+ /** How this run was typed, so a failure's report says what to run again. */
1805
+ readonly command: string;
1806
+ readonly keptConfig: boolean;
1807
+ /**
1808
+ * The machine-wide file this repo's hooks are in still holds them, because `--machine` was not
1809
+ * given: its path, said beside the report and in its last line, or null. Without it, a plain run
1810
+ * reports success while every Claude Code session on the machine still calls `flow`.
1811
+ */
1812
+ readonly machineHolds: string | null;
1813
+ /** The command that removes the package, the report's last line. */
1814
+ readonly next: string;
1815
+ }
1816
+ /** What uninstall will remove from this repo. Pure: every branch reads a fact the shell already gathered. */
1817
+ export declare function planUninstall(facts: UninstallFacts): UninstallPlan;
1818
+ /** The one argument `flow uninstall` takes: clean the machine-wide settings file too. */
1819
+ export declare const MACHINE_FLAG = "--machine";
1820
+ /**
1821
+ * `flow uninstall`'s arguments, read: null when they are fine, else the refusal to print. An argument
1822
+ * it does not know is refused rather than ignored, because this verb deletes files and a `--dry-run`
1823
+ * or a `--help` that went ahead would delete them too.
1824
+ */
1825
+ export declare function uninstallRefusal(args: readonly string[]): string | null;
1826
+ /** A step that could not be done answers 1, whether the shell hit it or the plan did. */
1827
+ export declare function uninstallCode(plan: UninstallPlan, failures: readonly string[]): 0 | 1;
1828
+ /** The report: every removal named, what was kept and why, each failure, and the command to run next. */
1829
+ export declare function uninstallLines(plan: UninstallPlan, failures: readonly string[]): string[];
1063
1830
  /** One thing the world around the config has to be, and whether it is. A red line carries its fix. */
1064
1831
  export interface Fitting {
1065
1832
  readonly id: string;
@@ -1077,8 +1844,18 @@ export interface StatusFacts {
1077
1844
  readonly load: LoadResult | null;
1078
1845
  readonly gateText: string | null;
1079
1846
  readonly hooksPath: string | null;
1847
+ /** The folder git runs hooks from, read as init reads it, or null outside git or when git could not say. */
1848
+ readonly hooksFolder: HooksFolder | null;
1080
1849
  readonly settingsPath: string;
1081
1850
  readonly settings: unknown;
1851
+ /**
1852
+ * The machine-wide settings file, parsed, when flow's hooks are in the repo's own file — a dev
1853
+ * dependency's — and null when `settingsPath` is the machine-wide file itself.
1854
+ */
1855
+ readonly machine: {
1856
+ readonly path: string;
1857
+ readonly settings: unknown;
1858
+ } | null;
1082
1859
  /**
1083
1860
  * The grammars the config declared, each with its library path made absolute and a straight
1084
1861
  * answer about whether that file is there.
@@ -1092,13 +1869,64 @@ export interface StatusFacts {
1092
1869
  readonly libraryPath: string;
1093
1870
  readonly present: boolean;
1094
1871
  }[];
1095
- /** Who last worked in this worktree, from the marker the write rail leaves. Null when nobody has. */
1872
+ /**
1873
+ * Who last worked in this worktree, from the marker the write rail leaves. Null when nobody has.
1874
+ * `mode` is the permission mode its last recorded call stated, null when none did; `steer` is auto
1875
+ * mode's steer as its transcript last stated it, asked only under auto mode.
1876
+ */
1096
1877
  readonly session: {
1097
1878
  readonly id: string;
1098
1879
  readonly agent: string | null;
1099
1880
  readonly wearing: readonly string[];
1881
+ readonly mode?: string | null | undefined;
1882
+ readonly steer?: Steer | null | undefined;
1100
1883
  } | null;
1884
+ /**
1885
+ * What git ignores here, one path per entry, a directory with its trailing `/` — plus any literal
1886
+ * path a write or delete rule names that git would ignore though it does not exist yet (`.env`).
1887
+ */
1888
+ readonly ignored: readonly string[];
1889
+ /** The config's `snapshotInclude` globs: the ignored paths the snapshot records anyway. */
1890
+ readonly snapshotInclude: readonly string[];
1891
+ /**
1892
+ * Where the flow the hooks and the gate run is, as three stats. `listed`: the repo's
1893
+ * `package.json` lists `@jawache/flow`. `copy`: its package's binary, `PACKAGE_BIN`, is there —
1894
+ * a dev dependency's, or the link `flow init` made to the flow that ran it. `path`: a `flow` on
1895
+ * the PATH this command was run with, less what npm added for this run (`hostPath`) — the nearest
1896
+ * reading there is of the PATH git and the host hand their hooks. `binaryFitting` judges them.
1897
+ */
1898
+ readonly binary: {
1899
+ readonly listed: boolean;
1900
+ readonly copy: boolean;
1901
+ readonly path: boolean;
1902
+ };
1101
1903
  }
1904
+ /**
1905
+ * The literal head of a glob, cut back to a whole segment: `src/gen/**` → `src/gen/`, `.env` →
1906
+ * `.env`, `**` + `/*.ts` → nothing. A glob with no wildcard is all head.
1907
+ */
1908
+ export declare function staticPrefix(glob: string): string;
1909
+ /**
1910
+ * The paths those rules name OUTRIGHT — a glob with no wildcard, such as `.env`. A file that does
1911
+ * not exist yet is in no listing of ignored files, so status asks git about these by name.
1912
+ */
1913
+ export declare function literalScopes(load: LoadResult | null): string[];
1914
+ /**
1915
+ * THE SNAPSHOT'S BLIND SPOTS, as red lines: a write or delete rule whose scope reaches a path git
1916
+ * ignores, where `snapshotInclude` does not bring it back.
1917
+ *
1918
+ * The snapshot is git's view, so a shell write to an ignored file is never in the diff and never
1919
+ * judged — and a rule guarding `.env` would load green and watch nothing. So the gap is said where a
1920
+ * person looks, naming the rule and the setting.
1921
+ *
1922
+ * WHAT "REACHES" MEANS, and why it is not "the glob could match something in there". An ignored
1923
+ * FILE is reached when the rule's globs match it. An ignored DIRECTORY is reached when a glob names
1924
+ * it — its literal head lies inside the directory (`dist/**` over an ignored `dist/`), or sits above
1925
+ * it and continues with `**` (`src/**` over an ignored `src/gen/`). A glob that is all wildcard,
1926
+ * `**` + `/*.ts`, does not reach `node_modules/` by this measure, though strictly it matches files in
1927
+ * there: every repo with such a rule would be red over a folder no rule was written about.
1928
+ */
1929
+ export declare function snapshotGaps(entries: readonly Bound[], ignored: readonly string[], include: readonly string[]): Fitting[];
1102
1930
  /** The whole answer. */
1103
1931
  export interface Status {
1104
1932
  readonly root: string;
@@ -1115,6 +1943,15 @@ export interface Status {
1115
1943
  readonly refusals: readonly Refusal[];
1116
1944
  readonly green: boolean;
1117
1945
  }
1946
+ /**
1947
+ * THE PATH GIT AND THE HOST HAND THEIR HOOKS, read off the PATH this command was run with: every
1948
+ * folder of it but a `node_modules/.bin` that npm put there for this one run — the repo's own or an
1949
+ * ancestor's, which `npx flow` and `npm run` add, and npx's cache of a package it fetched. Counted,
1950
+ * `npx flow status` would find a dev dependency's copy "on the PATH" that git never sees.
1951
+ *
1952
+ * Another repo's node_modules/.bin stays: it is on the PATH because someone put it there.
1953
+ */
1954
+ export declare function hostPath(dirs: readonly string[], root: string): string[];
1118
1955
  /** Does this adapter carry that moment, for that KIND of entry? */
1119
1956
  export declare function delivers(kind: EntrySpec["kind"], moment: Moment): boolean;
1120
1957
  /** The gathered facts, judged. Never throws: a broken config is a red line, not an exception. */