@jawache/flow 0.1.1 → 0.2.1

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,6 +1,13 @@
1
1
  import type { LoadResult } from "../language/domain.ts";
2
2
  import { type Facts, type SelectOpts, type SpawnMeta, type TranscriptRef } from "./domain.ts";
3
- /** Where this harness keeps a working directory's conversations. */
3
+ /**
4
+ * Where this harness keeps a working directory's conversations.
5
+ *
6
+ * Under the host's own folder, and `CLAUDE_CONFIG_DIR` moves that folder — the host honours it for
7
+ * the store as it does for settings.json, so a store read from `~/.claude` regardless would be
8
+ * somebody else's. A `home` passed in is taken as the user's home and wins: that is a test naming
9
+ * its own world.
10
+ */
4
11
  export declare function projectDir(cwd: string, home?: string): string;
5
12
  /** One conversation in the store, with everything findable about it without opening it twice. */
6
13
  export interface StoredTranscript {
@@ -1,5 +1,5 @@
1
1
  import { type ExecResult, type FlowConfig, type LoadResult, type Grammar, type Settings, type World } from "../language/domain.ts";
2
- import { type HookEvent, type HookPayload, type HookResult } from "./domain.ts";
2
+ import { type CallRecord, type Change, type Delta, type HookEvent, type HookPayload, type HookResult } from "./domain.ts";
3
3
  /**
4
4
  * Run one command the way a check means it — through a shell, in the repo, hermetically.
5
5
  *
@@ -62,6 +62,45 @@ export declare function wearer(root: string): {
62
62
  readonly agent: string | null;
63
63
  readonly wearing: readonly string[];
64
64
  };
65
+ /**
66
+ * RECORD THE TREE BEFORE A CALL, filed under the host's `tool_use_id`.
67
+ *
68
+ * A snapshot that cannot be taken is RECORDED as a fault rather than skipped, so the after-call rail
69
+ * reports it instead of finding nothing and calling the call clean. `who` is kept beside the tree so
70
+ * a snapshot with no after is readable as a call still in flight, per session and agent.
71
+ */
72
+ export declare function takeSnapshot(root: string, key: string, include: readonly string[], who: {
73
+ readonly session: string;
74
+ readonly agent: string;
75
+ readonly tool: string;
76
+ }): void;
77
+ /**
78
+ * WHAT THE CALL CHANGED: the tree now against the tree its snapshot recorded, path by path, with
79
+ * the content either side. The before-snapshot is kept — a revert reads from it — until `forget`.
80
+ */
81
+ export declare function observeDelta(root: string, key: string, include: readonly string[]): Delta & {
82
+ readonly before?: string;
83
+ };
84
+ /**
85
+ * PUT CHANGES BACK from the before-snapshot — a changed or removed file to its old content, a file
86
+ * the call created removed. Answers what could not be put back, never throws: a revert that failed
87
+ * is said to the model, loudly, rather than claimed.
88
+ */
89
+ export declare function revertChanges(root: string, tree: string, changes: readonly Change[]): {
90
+ path: string;
91
+ why: string;
92
+ }[];
93
+ /**
94
+ * THE CALL LOG — every snapshot's record, read back: who made each call, when it started, and when
95
+ * it finished if it has. A record that cannot be read is left out; it is somebody's write racing
96
+ * this read, and the next diff reads it whole.
97
+ */
98
+ export declare function callLog(root: string): CallRecord[];
99
+ /**
100
+ * The call is over: its record says when it finished, so a diff still to come can tell it
101
+ * overlapped, and its throwaway indexes go. Records no diff still to come can overlap are dropped.
102
+ */
103
+ export declare function finishSnapshot(root: string, key: string, now?: number): void;
65
104
  /**
66
105
  * The repo the session is standing in — the FOURTH question of §7.
67
106
  *
@@ -110,7 +149,7 @@ export declare function loadRegime(root: string): Promise<Regime>;
110
149
  *
111
150
  * The shape is deliberately flat rather than a hook function per event: the events differ in what
112
151
  * they CARRY (toEvent's job) and in how their answer is rendered (toResult's job), and the middle —
113
- * load, classify, run, log — is identical for all five. The old engine had five files here, and the
152
+ * load, classify, run, log — is identical for all of them. The old engine had five files here, and the
114
153
  * off switch, the session marker and the fault handling were each written out four or five times
115
154
  * with small differences nobody had chosen.
116
155
  */
@@ -157,9 +196,11 @@ export declare function readPayload(): PayloadRead;
157
196
  *
158
197
  * `pre-tool-use` is the rail that GUARDS, so it fails CLOSED — a payload it cannot read is a tool
159
198
  * call it cannot judge, and "I could not look" must never be spelled the same way as "I looked and
160
- * it was fine". The other four annotate, and breaking a session over a hook that only adds a
161
- * breadcrumb would be the cure killing the patient — so they SPEAK on stderr and allow, because
162
- * silence is precisely what let the old swallow go unnoticed.
199
+ * it was fine". The after-call rails JUDGE too — every change a call made reaches the rules there —
200
+ * so a payload they cannot read is a call whose changes nobody judged, and they report it with exit
201
+ * 2, which on those rails reaches the model beside the result and blocks nothing. The rest only add
202
+ * a breadcrumb, and breaking a session over one would be the cure killing the patient, so they SPEAK
203
+ * on stderr and allow. `readFault` next door is where that split is decided.
163
204
  *
164
205
  * Everything is wrapped: a hook that throws is a hook that wedges a session, and an unknown event
165
206
  * is silence rather than a usage message in front of the model.
@@ -18,10 +18,22 @@ export interface HookPayload {
18
18
  readonly tool_input?: Record<string, unknown>;
19
19
  /** SessionStart's reason: `startup` · `resume` · `clear` · `compact` — the compaction announcement. */
20
20
  readonly source?: string;
21
+ /**
22
+ * The host's id for one tool call, the same on its PreToolUse and its PostToolUse. It is the key
23
+ * the snapshot taken before the call is filed under and looked up by after it.
24
+ */
25
+ readonly tool_use_id?: string;
26
+ /**
27
+ * The permission mode the session runs under (`default` · `acceptEdits` · `plan` · `auto` · …),
28
+ * as the host states it on every call. Recorded and reported; never a category key.
29
+ */
30
+ readonly permission_mode?: string;
31
+ /** What the tool answered, on the after-call rails. Read for the host's own changed-file list. */
32
+ readonly tool_response?: unknown;
21
33
  readonly [key: string]: unknown;
22
34
  }
23
35
  /**
24
- * The five events flow registers for, named by the EVENT and never by the job.
36
+ * The events flow answers, named by the EVENT and never by the job.
25
37
  *
26
38
  * Carried from the old engine unchanged, and the naming rule with it: call a hook
27
39
  * `flow hook plan-drift` and the day a second concern wants the same host event you register a
@@ -88,7 +100,7 @@ export declare function configImports(configText: string): readonly string[];
88
100
  * the folder is the honest approximation — and it is exactly the width the old constant had.
89
101
  */
90
102
  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"];
103
+ export declare const HOOK_EVENTS: readonly ["session-start", "pre-tool-use", "post-tool-use", "post-tool-use-failure", "stop", "notification"];
92
104
  export type HookEvent = (typeof HOOK_EVENTS)[number];
93
105
  export declare function isHookEvent(word: string): word is HookEvent;
94
106
  /**
@@ -104,7 +116,9 @@ export declare function isHookEvent(word: string): word is HookEvent;
104
116
  * perfectly. Reporting an entry that goes dark is `flow status`'s job (F6).
105
117
  * · command BRIEFING is delivered, on the same PreToolUse answer the command guardrails use: when
106
118
  * 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.
119
+ * refusal wins — which is the right order, since a blocked command is not about to run. The
120
+ * files a Bash command reads ride that same answer as touches (`bashReads`), so an area's note
121
+ * arrives before the output does.
108
122
  *
109
123
  * It is two lists rather than one because "turn-end" is a word in both vocabularies and the answer
110
124
  * differs between them — a single flat list could only lie about one of them.
@@ -129,6 +143,12 @@ export type AdapterEvent = {
129
143
  readonly command?: string | undefined;
130
144
  readonly staged?: readonly string[] | undefined;
131
145
  readonly turn?: readonly TurnAction[] | undefined;
146
+ /**
147
+ * The change has already LANDED: the file came out of the delta after the call, not out of a
148
+ * tool's input before it. A refusal of an observed write is undone from the snapshot, and it
149
+ * is refused under its own banner.
150
+ */
151
+ readonly observed?: boolean | undefined;
132
152
  } | {
133
153
  readonly rail: "brief";
134
154
  readonly moment: BreadcrumbMoment;
@@ -149,6 +169,13 @@ export interface EventWorld {
149
169
  readonly read: (path: string) => string | null;
150
170
  /** What the actor did this turn. Only `stop` asks, so only `stop` pays for it. */
151
171
  readonly turn?: (() => readonly TurnAction[]) | undefined;
172
+ /** The home directory, for a Bash read through `~`. Absent, such a path is simply not named. */
173
+ readonly home?: string | undefined;
174
+ /**
175
+ * What the call changed in the tree, from the snapshot taken before it. Only the after-call
176
+ * rails carry it; absent, the call changed nothing flow could see.
177
+ */
178
+ readonly changes?: readonly Change[] | undefined;
152
179
  }
153
180
  /** An absolute path inside the repo → the repo-relative spelling every glob is written against. */
154
181
  export declare function relativise(path: string, root: string): string;
@@ -194,11 +221,37 @@ export declare function wouldBeFile(tool: string, input: Record<string, unknown>
194
221
  * aside. The command rail still sees the raw line either way.
195
222
  */
196
223
  export declare function deleteTargets(command: string): string[];
224
+ /** Where a Bash call ran, and the two directories its paths are judged against. */
225
+ export interface BashWhere {
226
+ /** The shell's working directory when the call started. */
227
+ readonly cwd: string;
228
+ /** The repo root. Paths outside it are not an area of this repo and are dropped. */
229
+ readonly root: string;
230
+ /** What `~` and `$HOME` mean; absent, a path through them is not known. */
231
+ readonly home?: string | undefined;
232
+ }
233
+ /** What a Bash command reads in the repo, repo-relative, in order, each once. */
234
+ export interface BashReads {
235
+ /** Files it reads by name — `cat`, `head`, `sed -n`, `grep` on a file, a `<` redirect. */
236
+ readonly reads: readonly string[];
237
+ /**
238
+ * Paths a search walks or lists — `rg`, `find`, `ls`, `grep -r`, a `git` pathspec — which may be
239
+ * directories. They touch an area; they are not a file the session read.
240
+ */
241
+ readonly searched: readonly string[];
242
+ }
243
+ /**
244
+ * The repo paths a Bash command reads or searches — the `READERS` table, a `<` redirect, a `git`
245
+ * pathspec — followed through `cd`, `VAR=`, `for` loops and `$(…)`. The repo root itself is not an
246
+ * area and is left out. Best-effort, failing open: see the section head.
247
+ */
248
+ export declare function bashReads(command: string, where: BashWhere): BashReads;
197
249
  /**
198
250
  * A payload → every event it carries. ZERO OR MORE, and the plural is not hedging.
199
251
  *
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.
252
+ * One Bash call is genuinely several moments — the command about to run, each file an `rm` in it
253
+ * would remove, and each file it reads — and the old engine ran two evaluations off that one hook
254
+ * for exactly this reason.
202
255
  * A signature returning one event would push the second rail back into the shell, where it could
203
256
  * not be tested as a decision.
204
257
  *
@@ -207,6 +260,94 @@ export declare function deleteTargets(command: string): string[];
207
260
  export declare function toEvent(hook: HookEvent, payload: HookPayload, world: EventWorld): AdapterEvent[];
208
261
  /** What a post-tool-use call was about, repo-relative: a path, or nothing this rail can steer on. */
209
262
  export declare function touchedPath(payload: HookPayload, root: string): string | null;
263
+ /**
264
+ * One path a call changed. `before` is null for a file the call created, `after` for one it
265
+ * removed; a path with both is a file it changed.
266
+ */
267
+ export interface Change {
268
+ readonly path: string;
269
+ readonly before: string | null;
270
+ readonly after: string | null;
271
+ }
272
+ /**
273
+ * What the shell could see of one call — the changes, or why it could not see them.
274
+ *
275
+ * A FAULT IS NOT AN EMPTY DELTA, and the difference is the guard's whole doctrine: "I looked and
276
+ * nothing changed" and "I could not look" must never be spelled the same way, or a snapshot that
277
+ * failed would let every write through as clean.
278
+ */
279
+ export type Delta = {
280
+ readonly ok: true;
281
+ readonly changes: readonly Change[];
282
+ } | {
283
+ readonly ok: false;
284
+ readonly fault: string;
285
+ };
286
+ /**
287
+ * Where git should look for the ignored paths `snapshotInclude` names: each glob's literal head,
288
+ * so git walks `config/` for `config/*.local.json` and not the whole repo. A glob with no literal
289
+ * head (`**` + `/.env`) has to be looked for everywhere.
290
+ */
291
+ export declare function snapshotPathspecs(include: readonly string[]): string[];
292
+ /** Of the ignored paths git listed under those heads, the ones a `snapshotInclude` glob matches. */
293
+ export declare function included(listed: readonly string[], include: readonly string[]): string[];
294
+ /**
295
+ * Git's answer as paths, one per line — the plainest shape git has, and the only one flow parses.
296
+ *
297
+ * ONE reader for it in the adapter: `flow status` asks git what it ignores and the realWorld asks
298
+ * for the staged set, and both used to split, trim and drop blank lines under two names. `null` is
299
+ * a command that never ran, and it is no paths.
300
+ */
301
+ export declare function pathLines(stdout: string | null): string[];
302
+ /** The key one call's snapshot is filed under — the host's `tool_use_id` — or null when it sent none. */
303
+ export declare function callKey(payload: HookPayload): string | null;
304
+ /**
305
+ * The paths the PreToolUse rail already judged for this call, repo-relative, BY RAIL.
306
+ *
307
+ * An Edit, a Write or a MultiEdit was judged as a write, on the file it would produce; an `rm` or
308
+ * an `mv` source was judged as a delete, on the file it was about to remove. Both were allowed there
309
+ * or the call would not have run, and judging them again after it would log every such decision
310
+ * twice. The rail matters: a path judged only as a delete says nothing about content written to it
311
+ * afterwards in the same call.
312
+ */
313
+ export declare function preJudged(payload: HookPayload, root: string): {
314
+ readonly writes: string[];
315
+ readonly deletes: string[];
316
+ };
317
+ /**
318
+ * One tool call as flow's own log holds it: who made it, when its snapshot was taken, and when its
319
+ * diff was, once it has been. A record with no `done` is a call still in flight.
320
+ */
321
+ export interface CallRecord {
322
+ /** The host's `tool_use_id`. */
323
+ readonly key: string;
324
+ readonly session: string;
325
+ readonly agent: string;
326
+ readonly tool: string;
327
+ /** When the snapshot before the call was taken, in epoch milliseconds. */
328
+ readonly at: number;
329
+ /** When the diff after it was taken. Absent while the call is in flight. */
330
+ readonly done?: number | undefined;
331
+ }
332
+ /** A call record read back off disk, or null when the file is not one. */
333
+ export declare function callRecord(key: string, raw: unknown): CallRecord | null;
334
+ /**
335
+ * The calls of OTHER actors that overlapped this one in time: started before this diff, and
336
+ * finished after this snapshot or not finished at all.
337
+ *
338
+ * A call that finished inside this one's window counts as well as one still in flight: its write
339
+ * landed between this snapshot and this diff just the same. Calls of the same session and agent
340
+ * never count — the host does not run an agent's writing calls side by side.
341
+ */
342
+ export declare function overlapping(me: CallRecord, log: readonly CallRecord[]): CallRecord[];
343
+ /**
344
+ * The finished calls the log can forget: no call still in flight started before one finished, so no
345
+ * diff still to come can overlap it. Keeping every record for a day would make every diff read a
346
+ * day of records.
347
+ */
348
+ export declare function forgettable(log: readonly CallRecord[]): string[];
349
+ /** How a call is named in a message: the tool, the host's id, and whose it was. */
350
+ export declare function callName(call: CallRecord): string;
210
351
  /** What the binary prints and exits with. The three IO edges as data, so a test can read them. */
211
352
  export interface HookResult {
212
353
  /** stdout — a JSON decision the host reads. Empty means nothing to say. */
@@ -221,7 +362,61 @@ export declare const ALLOW: HookResult;
221
362
  export interface Refused {
222
363
  readonly moment: GuardrailMoment;
223
364
  readonly block: Block;
365
+ /** The path of a change that had already landed when this refused it — the one to put back. */
366
+ readonly observed?: string | undefined;
367
+ }
368
+ /**
369
+ * What putting refused changes back came to — written by the shell, which did the writing.
370
+ *
371
+ * `failed` is the loud half: a file flow could not write back still holds what the call wrote, and
372
+ * the message must say so rather than claim a revert that did not happen.
373
+ */
374
+ export interface Undone {
375
+ /** The changes written back to what they were before the call. */
376
+ readonly restored: readonly Change[];
377
+ /** Paths the call changed that no rule refused — left as the call wrote them. */
378
+ readonly kept: readonly string[];
379
+ readonly failed: readonly {
380
+ readonly path: string;
381
+ readonly why: string;
382
+ }[];
383
+ /** What the refusal was allowed to undo — what the message explains when it was not everything. */
384
+ readonly scope?: RevertScope | undefined;
385
+ }
386
+ /** Which of a call's changes are put back, and which stay as the call left them. */
387
+ export interface Reversal {
388
+ readonly revert: readonly Change[];
389
+ readonly kept: readonly string[];
390
+ }
391
+ /**
392
+ * What a refusal is allowed to undo, beside the refusals themselves: the repo's `revert` setting,
393
+ * and the other calls that overlapped this one.
394
+ *
395
+ * A command started in the background gets nothing here, by ruling: what it writes after its call
396
+ * returns lands in some later call's diff and is judged and put back there like anything else.
397
+ * The harness runs no hook when the process ends, so any mark saying "not this call's" would have
398
+ * to outlive the process by a guess — and a guess that let `sleep 1` buy one unreverted write.
399
+ */
400
+ export interface RevertScope {
401
+ /** The config's `revert` setting: the refused files alone, or every file the call changed. */
402
+ readonly revert: "refused" | "all";
403
+ /** Other actors' calls that overlapped this one. Any of them, and `"all"` is not honoured. */
404
+ readonly overlap: readonly CallRecord[];
224
405
  }
406
+ /** The default scope: the refused files, and no overlap. */
407
+ export declare const REFUSED_ONLY: RevertScope;
408
+ /**
409
+ * THE REVERT SCOPE: the files a rule refused, or every file the call changed when the repo asked
410
+ * for that, and never more than the refused files while another actor's call overlapped this one
411
+ * — a diff taken then can hold that call's writes, and "all" would put back work that was not this
412
+ * call's to lose.
413
+ *
414
+ * Most writing calls touch one file, and the other files a multi-file command wrote are not wrong
415
+ * because one of them is — they are listed as kept, so the agent knows they stand. A refusal that
416
+ * names no landed path (a fault on a pre-emptive rail) reverts nothing: a revert is only ever of a
417
+ * change flow saw land, never a guess.
418
+ */
419
+ export declare function reversal(changes: readonly Change[], refusals: readonly Refused[], scope?: RevertScope): Reversal;
225
420
  /** A note with its prose resolved: a `file:` breadcrumb is read by the shell, never here. */
226
421
  export interface Shown {
227
422
  readonly entry: string | null;
@@ -232,6 +427,8 @@ export interface Shown {
232
427
  export interface Answer {
233
428
  readonly refused: readonly Refused[];
234
429
  readonly shown: readonly Shown[];
430
+ /** What was put back, when a refusal was of changes that had already landed. */
431
+ readonly undone?: Undone | undefined;
235
432
  }
236
433
  /**
237
434
  * Is this event the WRITE that could fix a config that will not load?
@@ -326,7 +523,16 @@ export declare function toResult(hook: HookEvent, answer: Answer): HookResult;
326
523
  * naming an event that never happened. The banner comes off the MOMENT, which every rail has,
327
524
  * rather than off the event, which only the harness ones do.
328
525
  */
329
- export declare function refused(refusals: readonly Refused[]): HookResult | null;
526
+ export declare function refused(refusals: readonly Refused[], undone?: Undone): HookResult | null;
527
+ /**
528
+ * What the after-call rail says when it could not see what the call changed.
529
+ *
530
+ * FAIL LOUD, NEVER FAIL CLEAN. A snapshot that could not be taken, or a diff that could not be
531
+ * read, is not an empty delta: the call is not waved through as having changed nothing, and nothing
532
+ * is put back on a guess. Exit 2 on this rail cannot block — the call has run — so it is a report,
533
+ * and it goes where the model reads the tool's result.
534
+ */
535
+ export declare function deltaFault(fault: string): HookResult;
330
536
  /** Hand the agent some context and allow — the one shape every steering rail emits. */
331
537
  export declare function decision(hook: HookEvent, additionalContext: string): string;
332
538
  /**
@@ -340,6 +546,16 @@ export declare function decision(hook: HookEvent, additionalContext: string): st
340
546
  * decision because it is a fact about what the event is FOR.
341
547
  */
342
548
  export declare function faultText(hook: HookEvent, why: "unreadable" | "unparseable", detail: string): string;
549
+ /**
550
+ * The answer a rail gives when it could not read its payload — and which rails must be LOUD.
551
+ *
552
+ * The three rails that judge a tool call answer exit 2: PreToolUse, where it refuses the call it
553
+ * could not see, and the two after-call rails, where every change the call made is judged — there
554
+ * exit 2 blocks nothing (the call has run) and puts the sentence in front of the model beside the
555
+ * result, so a call whose changes went unjudged is never passed as clean. The rest only add a
556
+ * breadcrumb or end a turn, and say it on stderr with exit 0.
557
+ */
558
+ export declare function readFault(hook: HookEvent, why: "unreadable" | "unparseable", detail: string): HookResult;
343
559
  /** One line of a transcript, parsed. `record` is the raw object — this file reads it, nobody else. */
344
560
  export interface TranscriptLine {
345
561
  /** The record's `type`: `user`, `assistant`, or whatever else the host writes. */
@@ -466,6 +682,36 @@ export declare function sessionFactsFrom(args: {
466
682
  * name, because "the session ran fifty greps here" is a fact about the area.
467
683
  */
468
684
  export declare function toolRow(payload: HookPayload, root?: string): Row | null;
685
+ /**
686
+ * The files a Bash call reads by name, one `read` row each — written beside its tool row, so the
687
+ * record counts a `cat` of a file as a touch of it the way it counts a Read.
688
+ *
689
+ * Files only. A directory a search walks is an area, not a file anybody read, and counted as one it
690
+ * would name a folder as a node of the tree the rows are drawn into.
691
+ */
692
+ export declare function shellReadRows(payload: HookPayload, root: string, home?: string): Row[];
693
+ /**
694
+ * The files one call changed that its own tool row does not already say, one `write` row each —
695
+ * what a heredoc, a `sed -i`, a script or a recipe wrote, and what an Edit's call changed besides
696
+ * the file it named.
697
+ *
698
+ * A SPAWN writes none of its own. The Agent call's diff holds everything its subagent wrote through
699
+ * calls that are logged under their own rows, and counted again here every edit a builder made would
700
+ * be in the record twice.
701
+ */
702
+ export declare function writeRows(payload: HookPayload, changes: readonly Change[], root: string): Row[];
703
+ /**
704
+ * Claude Code's own list of the files a command changed, where the host sent one, set against
705
+ * flow's diff — a `mismatch` row when they disagree, and nothing when they agree or the host said
706
+ * nothing.
707
+ *
708
+ * A CROSS-CHECK, NEVER AN INPUT. The host's list (`bashEditDiff`, a public beta, on by default only
709
+ * in some permission modes) decides nothing here: it is recorded so a reader of `flow facts` can see
710
+ * where the two views of one call differ. A host path outside the repo is not a file flow could
711
+ * have seen, so it is left out rather than reported; and when the host says its list was cut short
712
+ * (`moreFiles`), a file only flow saw is not a disagreement anybody can claim.
713
+ */
714
+ export declare function mismatchRow(payload: HookPayload, changes: readonly Change[], root: string): Row | null;
469
715
  /**
470
716
  * The variables git exports into a hook's environment, which REDIRECT any nested `git` command back
471
717
  * at the parent repo — a `git init` in a temp directory would suddenly write the real index.
@@ -610,6 +856,8 @@ export type TranscriptEvent = {
610
856
  readonly input: Record<string, unknown>;
611
857
  /** The file the call names, repo-relative. Empty when it names none. */
612
858
  readonly path: string;
859
+ /** The repo files a Bash call reads by name (`bashReads`), when it reads any. */
860
+ readonly reads?: readonly string[];
613
861
  };
614
862
  /**
615
863
  * A transcript → the narrative events in it: typed prompts, tool calls, and their results.
@@ -620,7 +868,7 @@ export type TranscriptEvent = {
620
868
  * doing the same over the same files, so "what is an assistant line" had two answers free to
621
869
  * drift. It reads through `transcriptLines` now, and owns only the pairing.
622
870
  */
623
- export declare function parseEvents(jsonl: string, root?: string): TranscriptEvent[];
871
+ export declare function parseEvents(jsonl: string, root?: string, home?: string): TranscriptEvent[];
624
872
  /** Does this prompt read as the human pushing back? */
625
873
  export declare function isCorrection(prose: string): boolean;
626
874
  /** The tools a repo's justfile actually drives — what a direct invocation would be bypassing. */
@@ -662,6 +910,10 @@ export interface Narrative {
662
910
  readonly stats: {
663
911
  readonly edits: number;
664
912
  readonly writes: number;
913
+ /** Files changed round the edit tools — a heredoc, a `sed -i`, a script — from flow's own diff. */
914
+ readonly shellWrites: number;
915
+ /** Files a Bash command read by name. */
916
+ readonly shellReads: number;
665
917
  readonly commits: number;
666
918
  readonly recipes: Readonly<Record<string, number>>;
667
919
  readonly bypasses: Readonly<Record<string, number>>;
@@ -671,6 +923,11 @@ export interface Narrative {
671
923
  readonly bypassSites: readonly Bypass[];
672
924
  readonly loops: readonly EditLoop[];
673
925
  readonly retries: readonly Retry[];
926
+ /** The paths behind the two shell counts, with how often each. */
927
+ readonly shell: {
928
+ readonly writes: Readonly<Record<string, number>>;
929
+ readonly reads: Readonly<Record<string, number>>;
930
+ };
674
931
  /** Every in-repo file the session touched, with how often. The facts layer scores coverage. */
675
932
  readonly touched: Readonly<Record<string, number>>;
676
933
  }
@@ -679,8 +936,14 @@ export interface Narrative {
679
936
  *
680
937
  * `tools` is INJECTED — the shell reads the justfile — so the same call is reproducible from a
681
938
  * fixture, which is the same discipline the guard itself is built on.
939
+ *
940
+ * `rows` is the conversation's own stream, when it has one, and it is where a SHELL WRITE comes
941
+ * from: a transcript says what command ran, never what it changed, and only flow's diff knows that.
942
+ * A conversation with no stream has no shell writes to count, which is why the count reads zero
943
+ * there rather than a guess. Shell READS come from the transcript, like every Read does, so they are
944
+ * counted in every conversation.
682
945
  */
683
- export declare function narrative(events: readonly TranscriptEvent[], tools?: readonly string[]): Narrative;
946
+ export declare function narrative(events: readonly TranscriptEvent[], tools?: readonly string[], rows?: readonly Row[]): Narrative;
684
947
  /**
685
948
  * Every narrative reading merged into one, each pointer tagged with the transcript it came from.
686
949
  *
@@ -832,6 +1095,51 @@ export interface Uncovered {
832
1095
  * know whether the entries it just bound point anywhere near it.
833
1096
  */
834
1097
  export declare function uncoveredAreas(touched: Readonly<Record<string, number>>, entries: readonly Bound[]): Uncovered[];
1098
+ /**
1099
+ * Whether Claude Code's auto mode is steering the session towards shell edits — `bashFirst` on the
1100
+ * host's auto-mode attachment, which tells the model to read with `cat` and change files with `sed`,
1101
+ * heredocs and scripts rather than Read, Edit and Write. `variant` is the host's own word for how hard
1102
+ * (`strict` · `relaxed`), when it gave one.
1103
+ */
1104
+ export interface Steer {
1105
+ readonly shellFirst: boolean;
1106
+ readonly variant: string | null;
1107
+ }
1108
+ /**
1109
+ * The auto-mode steer a conversation is under, from the LAST auto-mode attachment in its transcript
1110
+ * — or null when it has none, or the last one is the host leaving auto mode.
1111
+ *
1112
+ * Read from the END, and only lines that name the attachment are parsed: a transcript is a couple
1113
+ * of megabytes and the answer is one record.
1114
+ */
1115
+ export declare function steerOf(jsonl: string): Steer | null;
1116
+ /** The permission mode the last recorded call was made under, or null when no row states one. */
1117
+ export declare function lastMode(rows: readonly Row[]): string | null;
1118
+ /** The permission modes the read conversations ran under, and how many of them auto mode steered. */
1119
+ export interface Modes {
1120
+ /** Recorded calls per permission mode. */
1121
+ readonly calls: Readonly<Record<string, number>>;
1122
+ /** Conversations whose transcript last stood in auto mode. */
1123
+ readonly auto: number;
1124
+ /** Of those, the ones steered towards shell edits, and by which variant. */
1125
+ readonly steered: number;
1126
+ readonly variants: Readonly<Record<string, number>>;
1127
+ }
1128
+ /** Each conversation's rows and steer, added up. */
1129
+ export declare function modesOf(each: readonly {
1130
+ readonly rows: readonly Row[];
1131
+ readonly steer: Steer | null;
1132
+ }[]): Modes;
1133
+ /** Where Claude Code's changed-file list and flow's diff disagreed, over the read conversations. */
1134
+ export interface Mismatches {
1135
+ readonly calls: number;
1136
+ /** Paths only the host listed, and how often. */
1137
+ readonly hostOnly: Readonly<Record<string, number>>;
1138
+ /** Paths only flow's diff held, and how often. */
1139
+ readonly flowOnly: Readonly<Record<string, number>>;
1140
+ }
1141
+ /** The `mismatch` rows, added up. */
1142
+ export declare function mismatchesIn(rows: readonly Row[]): Mismatches;
835
1143
  export interface Facts {
836
1144
  readonly root: string;
837
1145
  readonly store: {
@@ -864,6 +1172,10 @@ export interface Facts {
864
1172
  readonly actors: readonly SpawnRecord[];
865
1173
  /** A rail refused, and then somebody edited the guard. A pointer, never an accusation. */
866
1174
  readonly weakened: readonly Weakening[];
1175
+ /** The permission modes the read conversations ran under, and the auto-mode steer. */
1176
+ readonly modes: Modes;
1177
+ /** Where the host's changed-file list disagreed with flow's diff. */
1178
+ readonly mismatches: Mismatches;
867
1179
  /** How many conversations were marked read, when asked for. Null when not asked. */
868
1180
  readonly marked: number | null;
869
1181
  }
@@ -943,6 +1255,15 @@ export declare const HOOK_REGISTRATIONS: readonly Registration[];
943
1255
  export declare function ourHookCommand(command: string): boolean;
944
1256
  /** The events this settings file already calls flow for. What makes init idempotent and status honest. */
945
1257
  export declare function registeredEvents(settings: unknown): string[];
1258
+ /**
1259
+ * The events flow IS registered for, but with a matcher narrower than this build asks for.
1260
+ *
1261
+ * An older flow registered PostToolUse for six tools, because an after-call hook could only show a
1262
+ * breadcrumb then. Now it is where every change a tool makes is judged, so a registration on six
1263
+ * tools leaves a shell edit through any other tool unjudged — and it LOOKS registered. It is its own
1264
+ * red line for that reason, rather than disappearing inside "registered".
1265
+ */
1266
+ export declare function staleRegistrations(settings: unknown): string[];
946
1267
  /**
947
1268
  * A settings file with flow's registrations in it — a NEW object, and only ever added to.
948
1269
  *
@@ -1092,13 +1413,52 @@ export interface StatusFacts {
1092
1413
  readonly libraryPath: string;
1093
1414
  readonly present: boolean;
1094
1415
  }[];
1095
- /** Who last worked in this worktree, from the marker the write rail leaves. Null when nobody has. */
1416
+ /**
1417
+ * Who last worked in this worktree, from the marker the write rail leaves. Null when nobody has.
1418
+ * `mode` is the permission mode its last recorded call stated, null when none did; `steer` is auto
1419
+ * mode's steer as its transcript last stated it, asked only under auto mode.
1420
+ */
1096
1421
  readonly session: {
1097
1422
  readonly id: string;
1098
1423
  readonly agent: string | null;
1099
1424
  readonly wearing: readonly string[];
1425
+ readonly mode?: string | null | undefined;
1426
+ readonly steer?: Steer | null | undefined;
1100
1427
  } | null;
1428
+ /**
1429
+ * What git ignores here, one path per entry, a directory with its trailing `/` — plus any literal
1430
+ * path a write or delete rule names that git would ignore though it does not exist yet (`.env`).
1431
+ */
1432
+ readonly ignored: readonly string[];
1433
+ /** The config's `snapshotInclude` globs: the ignored paths the snapshot records anyway. */
1434
+ readonly snapshotInclude: readonly string[];
1101
1435
  }
1436
+ /**
1437
+ * The literal head of a glob, cut back to a whole segment: `src/gen/**` → `src/gen/`, `.env` →
1438
+ * `.env`, `**` + `/*.ts` → nothing. A glob with no wildcard is all head.
1439
+ */
1440
+ export declare function staticPrefix(glob: string): string;
1441
+ /**
1442
+ * The paths those rules name OUTRIGHT — a glob with no wildcard, such as `.env`. A file that does
1443
+ * not exist yet is in no listing of ignored files, so status asks git about these by name.
1444
+ */
1445
+ export declare function literalScopes(load: LoadResult | null): string[];
1446
+ /**
1447
+ * THE SNAPSHOT'S BLIND SPOTS, as red lines: a write or delete rule whose scope reaches a path git
1448
+ * ignores, where `snapshotInclude` does not bring it back.
1449
+ *
1450
+ * The snapshot is git's view, so a shell write to an ignored file is never in the diff and never
1451
+ * judged — and a rule guarding `.env` would load green and watch nothing. So the gap is said where a
1452
+ * person looks, naming the rule and the setting.
1453
+ *
1454
+ * WHAT "REACHES" MEANS, and why it is not "the glob could match something in there". An ignored
1455
+ * FILE is reached when the rule's globs match it. An ignored DIRECTORY is reached when a glob names
1456
+ * it — its literal head lies inside the directory (`dist/**` over an ignored `dist/`), or sits above
1457
+ * it and continues with `**` (`src/**` over an ignored `src/gen/`). A glob that is all wildcard,
1458
+ * `**` + `/*.ts`, does not reach `node_modules/` by this measure, though strictly it matches files in
1459
+ * there: every repo with such a rule would be red over a folder no rule was written about.
1460
+ */
1461
+ export declare function snapshotGaps(entries: readonly Bound[], ignored: readonly string[], include: readonly string[]): Fitting[];
1102
1462
  /** The whole answer. */
1103
1463
  export interface Status {
1104
1464
  readonly root: string;