@jawache/flow 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -53,54 +59,25 @@ export declare const CONFIG_FILE = "flow.config.ts";
53
59
  * Every relative module the config names, READ OUT OF ITS TEXT.
54
60
  *
55
61
  * A regex over source, deliberately, and it is the only reader that can work here: this is asked
56
- * 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
57
63
  * be asking the broken thing to answer for itself. A line like `import { house } from
58
64
  * "./guards/house.ts";` survives almost every way a config breaks, because what breaks a config is
59
65
  * a missing `.message(…)` or a stray brace two hundred lines below its imports.
60
66
  *
61
67
  * Only `./…` and `../…` count. A bare specifier is a package — `@jawache/flow`, or a pack somebody
62
- * published — and a package is not this repo's source to repair. A `..` segment climbs out of the
63
- * root, so those are dropped too: the config sits at the repo root and the surface is
64
- * repo-relative or it is nothing. A leading dot in a FOLDER name is ordinary and is kept —
65
- * `./.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.
66
72
  *
67
73
  * A `from '…'` phrase inside a QUOTED STRING is harvested as well — a rule whose `.message(…)`
68
- * 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
69
75
  * overlooked: telling a real import from a sentence quoting one takes a parser, and a parser is
70
76
  * the one thing a file that will not parse cannot give. The cost is bounded and one-directional —
71
- * the surface only ever WIDENS, it widens only while the config is already refusing every other
72
- * 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.
73
78
  */
74
79
  export declare function configImports(configText: string): readonly string[];
75
- /**
76
- * THE CONFIG SURFACE — the files a write may target while the guard is broken, and nothing else.
77
- *
78
- * Fail-loud says a config that will not load refuses every gated moment, and it is right: a guard
79
- * that quietly carries on with yesterday's rules is a guard that lies about being there. But taken
80
- * without exception it also refuses the ONE write that can end the outage, and then the doctrine's
81
- * own instruction — "adjust the change so it passes, then retry" — is impossible to obey. A human
82
- * in an editor never meets this (no hooks sit in front of them, so they just undo); an agent that
83
- * broke the config is locked out of repairing it, and the repo stays broken until a human arrives.
84
- * Measured twice on the crossing that introduced it, once for real.
85
- *
86
- * So the exception is exactly as wide as the repair and no wider: a write whose target is the
87
- * config or a pack it imports. Everything else still refuses — every other path, every command,
88
- * and the commit gate, which goes on refusing until the config loads green, so nothing written
89
- * under the exception can reach a commit unreviewed by a working guard.
90
- *
91
- * DERIVED FROM THE CONFIG'S OWN TEXT, and it used to be a two-line constant: the config file, plus
92
- * one repo's pack folder spelled into the engine. Every other repo got a repair exception over a
93
- * folder it does not have, and an agent that broke the config there could not reach the pack it
94
- * broke. The text is readable when the module is not, so nothing is given up by asking it — and the
95
- * answer now follows whatever a repo actually called the folder.
96
- *
97
- * The FOLDER of each imported pack comes with the file, which is the one deliberate widening. A
98
- * repair is rarely one file: a pack imports its own helpers, and those the config never names. The
99
- * import graph cannot be walked (walking it means resolving, and resolving is what is broken), so
100
- * the folder is the honest approximation — and it is exactly the width the old constant had.
101
- */
102
- export declare function configSurface(configText: string | null): readonly string[];
103
- export declare const HOOK_EVENTS: readonly ["session-start", "pre-tool-use", "post-tool-use", "post-tool-use-failure", "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"];
104
81
  export type HookEvent = (typeof HOOK_EVENTS)[number];
105
82
  export declare function isHookEvent(word: string): word is HookEvent;
106
83
  /**
@@ -109,38 +86,48 @@ export declare function isHookEvent(word: string): word is HookEvent;
109
86
  *
110
87
  * Two facts are stated here rather than discovered by somebody whose rule never fires:
111
88
  *
112
- * · `commit` is delivered, but not by the harness — git's pre-commit hook calls `flow commit`,
113
- * which is why every harness gets the gates for free and why it is in this list at all.
114
- * · turn-end BRIEFING is not delivered. Stop's decision object carries no context channel, so a
115
- * `breadcrumb().at(turnEnd)` has nowhere to be shown; the guardrail rail at that moment works
116
- * 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.
117
95
  * · command BRIEFING is delivered, on the same PreToolUse answer the command guardrails use: when
118
96
  * nothing blocks, the hook's `additionalContext` carries the note, and when something does, the
119
97
  * refusal wins — which is the right order, since a blocked command is not about to run. The
120
98
  * files a Bash command reads ride that same answer as touches (`bashReads`), so an area's note
121
99
  * arrives before the output does.
122
100
  *
123
- * It is two lists rather than one because "turn-end" is a word in both vocabularies and the answer
124
- * 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.
125
104
  */
126
105
  export declare const DELIVERS: {
127
106
  guard: readonly GuardrailMoment[];
128
107
  brief: readonly BreadcrumbMoment[];
129
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;
130
118
  /**
131
119
  * One event in the engine's vocabulary, minus the two things the engine's own event types demand
132
- * 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
133
121
  * verdict) and how far it has drifted (`tokens`, which needs the transcript). The shell adds both,
134
122
  * because both come off disk.
135
123
  */
136
124
  export type AdapterEvent = {
137
125
  readonly rail: "guard";
138
126
  readonly moment: GuardrailMoment;
139
- readonly file?: {
140
- readonly path: string;
141
- readonly content: string;
142
- } | undefined;
127
+ readonly file?: Ctx["file"];
143
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"];
144
131
  readonly staged?: readonly string[] | undefined;
145
132
  readonly turn?: readonly TurnAction[] | undefined;
146
133
  /**
@@ -167,8 +154,14 @@ export interface EventWorld {
167
154
  readonly root: string;
168
155
  /** A file as it stands right now, or null when it is not there — an Edit is a diff against it. */
169
156
  readonly read: (path: string) => string | null;
170
- /** 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. */
171
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;
172
165
  /** The home directory, for a Bash read through `~`. Absent, such a path is simply not named. */
173
166
  readonly home?: string | undefined;
174
167
  /**
@@ -210,17 +203,19 @@ export declare function wouldBeFile(tool: string, input: Record<string, unknown>
210
203
  content: string;
211
204
  } | null;
212
205
  /**
213
- * 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.
214
208
  *
215
209
  * Deletion has no tool of its own: it is a side effect of a shell command, so the delete moment is
216
- * assembled here or it does not exist at all. `git mv` was the old engine's blind spot; it is
217
- * 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.
218
213
  *
219
- * A line that cannot be tokenised yields NOTHING, deliberately: `null` from the tokeniser means "I
220
- * cannot tell", and a guard that guesses at a line it could not parse is worse than one that stands
221
- * 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.
222
217
  */
223
- export declare function deleteTargets(command: string): string[];
218
+ export declare function deleteTargets(commands: readonly ShellCommand[] | null): string[];
224
219
  /** Where a Bash call ran, and the two directories its paths are judged against. */
225
220
  export interface BashWhere {
226
221
  /** The shell's working directory when the call started. */
@@ -242,10 +237,11 @@ export interface BashReads {
242
237
  }
243
238
  /**
244
239
  * 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.
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.
247
243
  */
248
- export declare function bashReads(command: string, where: BashWhere): BashReads;
244
+ export declare function bashReads(steps: readonly ShellStep[] | null, where: BashWhere): BashReads;
249
245
  /**
250
246
  * A payload → every event it carries. ZERO OR MORE, and the plural is not hedging.
251
247
  *
@@ -258,6 +254,19 @@ export declare function bashReads(command: string, where: BashWhere): BashReads;
258
254
  * An event flow was not asked for answers with an empty list, never with an invented moment.
259
255
  */
260
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;
261
270
  /** What a post-tool-use call was about, repo-relative: a path, or nothing this rail can steer on. */
262
271
  export declare function touchedPath(payload: HookPayload, root: string): string | null;
263
272
  /**
@@ -291,6 +300,26 @@ export type Delta = {
291
300
  export declare function snapshotPathspecs(include: readonly string[]): string[];
292
301
  /** Of the ignored paths git listed under those heads, the ones a `snapshotInclude` glob matches. */
293
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[];
294
323
  /**
295
324
  * Git's answer as paths, one per line — the plainest shape git has, and the only one flow parses.
296
325
  *
@@ -299,6 +328,37 @@ export declare function included(listed: readonly string[], include: readonly st
299
328
  * a command that never ran, and it is no paths.
300
329
  */
301
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;
302
362
  /** The key one call's snapshot is filed under — the host's `tool_use_id` — or null when it sent none. */
303
363
  export declare function callKey(payload: HookPayload): string | null;
304
364
  /**
@@ -431,57 +491,56 @@ export interface Answer {
431
491
  readonly undone?: Undone | undefined;
432
492
  }
433
493
  /**
434
- * Is this event the WRITE that could fix a config that will not load?
494
+ * What the agent is told beside the load error, on every answer it rides: what that error means.
435
495
  *
436
- * The one exception to fail-loud, and it is deliberately the narrowest shape that works: a write
437
- * (or a delete) whose target is the guard's own source. `configSurface` above says why that surface
438
- * is read out of the config's own text rather than resolved.
439
- *
440
- * A COMMAND is never a repair, however plausible it looks. `sed -i` on the config would qualify by
441
- * intent and there is no way to tell it from `rm -rf` before it runs — a command rail sees a string,
442
- * not a target — so the command rail stays fully closed in the broken state, and the agent's route
443
- * back is the edit tools, which name the file they are about to write.
444
- *
445
- * It lives HERE rather than in the shell beside its caller because it decides and reads nothing.
446
- * 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.
447
498
  */
448
- 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.";
449
500
  /**
450
501
  * WHAT EVERY RAIL ANSWERS WHILE THE CONFIG WILL NOT LOAD — one decision for both ways it can break.
451
502
  *
452
- * A config has two ways of not loading, and they used to get two different answers. One will not
453
- * IMPORT — a missing module, a syntax error — which the shell catches as a thrown error. The other
454
- * imports fine and the GRAMMAR refuses it: a missing `.message(…)`, an override naming an entry the
455
- * pack does not have, a mandatory parameter nobody supplied. Only the first ever reached the repair
456
- * exception, because the second was refused a layer deeper inside `guard()` — so the break the
457
- * README and the guidebook both tell a reader to make was the one break an agent could not repair
458
- * itself out of. To everyone who meets them the two are the same fact, "the guard is entirely off",
459
- * and now they are the same answer as well.
460
- *
461
- * TURN-END IS TOLD, NOT REFUSED. Holding a turn open on a broken config prevents nothing risky —
462
- * every write, command and commit is already refused and the commit gate stays shut — while it does
463
- * stop the agent handing back to the one person who can fix it. So the fault is reported and the
464
- * 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.
465
526
  *
466
527
  * THE ENGINE HAS ITS OWN PAIR OF `!load.ok` BRANCHES — `guard()` blocks with the fault, `brief()`
467
528
  * notices it — and they are NOT this rule written twice. They answer a different question: "there
468
- * is nothing to run, say so", for the two callers that have no repair to make and no turn to end,
469
- * namely `replay` and the commit gate. Deleting them was considered at F3.B3 and measured first:
470
- * `flow replay` over a config the grammar refuses reaches `brief()`'s branch and emits the fault
471
- * notice, so the branch is live and its deletion would have dropped that notice silently. What IS
472
- * shared is the shape — `Outage extends Answer`, so the hook rail's answer and every other answer
473
- * 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.
474
533
  */
475
534
  export interface Outage extends Answer {
476
535
  /**
477
- * 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.
478
537
  *
479
- * It is not `shown`: the Stop rail has no context channel to inject prose into, which is what
480
- * `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.
481
540
  */
482
541
  readonly told: string | null;
483
542
  }
484
- export declare function whileBroken(events: readonly AdapterEvent[], surface: readonly string[], message: string): Outage;
543
+ export declare function whileBroken(events: readonly AdapterEvent[], message: string): Outage;
485
544
  /**
486
545
  * The one code `flow status` answers with.
487
546
  *
@@ -512,7 +571,11 @@ export declare function briefBlock(shown: readonly Shown[]): string;
512
571
  * moment. Exit 2 is the host's documented refusal channel and the model reads stderr on all three
513
572
  * rails, so flow has one refusal and one place to look when it fails to arrive.
514
573
  *
515
- * 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`).
516
579
  */
517
580
  export declare function toResult(hook: HookEvent, answer: Answer): HookResult;
518
581
  /**
@@ -535,25 +598,29 @@ export declare function refused(refusals: readonly Refused[], undone?: Undone):
535
598
  export declare function deltaFault(fault: string): HookResult;
536
599
  /** Hand the agent some context and allow — the one shape every steering rail emits. */
537
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>;
538
603
  /**
539
- * 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.
540
605
  *
541
606
  * The three outcomes of a read are not one outcome. "There was nothing to guard" is safe and common
542
607
  * — a hand-run, an empty stdin — and silence is the right answer to it. "I could not SEE what I was
543
608
  * guarding" is the old engine's invisible swallow: both were folded into `{}`, so a PreToolUse rail
544
- * holding an unreadable payload found no tool call, allowed, and told nobody. The sentence is the
545
- * same on every rail; what differs is whether the rail blocks with it, and that is the caller's
546
- * decision because it is a fact about what the event is FOR.
547
- */
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.
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).
551
613
  *
552
614
  * The three rails that judge a tool call answer exit 2: PreToolUse, where it refuses the call it
553
615
  * 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
616
+ * exit 2 blocks nothing (the call has run) and puts the line in front of the model beside the
555
617
  * result, so a call whose changes went unjudged is never passed as clean. The rest only add a
556
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).
557
624
  */
558
625
  export declare function readFault(hook: HookEvent, why: "unreadable" | "unparseable", detail: string): HookResult;
559
626
  /** One line of a transcript, parsed. `record` is the raw object — this file reads it, nobody else. */
@@ -651,28 +718,31 @@ export declare function briefHead(jsonl: string, cap?: number): string;
651
718
  * outright — which is every repo's own first commit.
652
719
  */
653
720
  export declare function branchFromHead(head: string): string | null;
654
- /**
655
- * The sidecar beside a subagent's transcript: `…/agent-<id>.jsonl` → `…/agent-<id>.meta.json`.
656
- *
657
- * The host writes one for every spawn — present 335/335 in the probe — and it is the strongest
658
- * evidence in the system precisely because the session did not write it. A session could claim to
659
- * be anything, and permissions resting on the claim would rest on a lie.
660
- */
661
- export declare function sidecarPath(transcriptPath: string): string | null;
662
721
  /**
663
722
  * Host-written evidence about one session × agent, assembled — the shape a classifier sees.
664
723
  *
665
- * The sidecar's absence is itself the evidence for the top-level session: there is no positive
666
- * marker for being the parent, only the absence of having been spawned. A sidecar that will not
667
- * parse is NOT the same as none — the session was spawned, we simply cannot say as what — so
668
- * `subagent` stays true and `agentType` is left undefined, and a category keyed on a specific type
669
- * 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.
670
733
  */
671
734
  export declare function sessionFactsFrom(args: {
672
735
  readonly payload: HookPayload;
673
736
  readonly transcript: string | null;
674
- readonly sidecar: string | null;
675
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;
676
746
  /**
677
747
  * A tool call → the row that records it, or null when the payload names no tool at all.
678
748
  *
@@ -766,14 +836,24 @@ export declare function classifyStore(paths: readonly string[]): {
766
836
  transcripts: TranscriptRef[];
767
837
  ignored: string[];
768
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;
769
849
  /**
770
850
  * The sidecar the host writes beside every subagent transcript, read.
771
851
  *
772
852
  * `toolUseId` is the field that makes this more than a label: it is the id of the parent's own
773
- * `Agent` tool call, so a sidecar and the block that spawned it join exactly. That join is what
774
- * lets cost attribution and classification agree about who an actor was — the sidecar says what a
775
- * spawn WAS, the parent's block says when it happened and what it was asked for, and neither
776
- * 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.
777
857
  */
778
858
  export interface SpawnMeta {
779
859
  readonly agentType: string | null;
@@ -823,9 +903,10 @@ export interface Spawn {
823
903
  * Every spawn a parent transcript made, by tool_use id — the parent's half of the join.
824
904
  *
825
905
  * It is the WEAKER half and is read as such: what the parent asked for is a claim by the session
826
- * being classified, while the sidecar is host-written and unforgeable. The classification order
827
- * (engine, `spawnedAs`) reads the sidecar and never this. What this is for is everything the
828
- * 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.
829
910
  */
830
911
  export declare function spawnsIn(jsonl: string): Map<string, Spawn>;
831
912
  /** Text with terminal colour codes taken out — a recorded command line is full of them. */
@@ -871,7 +952,13 @@ export type TranscriptEvent = {
871
952
  export declare function parseEvents(jsonl: string, root?: string, home?: string): TranscriptEvent[];
872
953
  /** Does this prompt read as the human pushing back? */
873
954
  export declare function isCorrection(prose: string): boolean;
874
- /** 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
+ */
875
962
  export declare function recipeTools(justfile: string): string[];
876
963
  /** What one Bash line was: the recipes it drove, the tools it went round, and whether it committed. */
877
964
  export interface BashClass {
@@ -879,7 +966,12 @@ export interface BashClass {
879
966
  readonly bypasses: readonly string[];
880
967
  readonly commits: boolean;
881
968
  }
882
- /** 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
+ */
883
975
  export declare function classifyBash(command: unknown, tools: readonly string[]): BashClass;
884
976
  /** A pointer into a transcript — the shape every heuristic finding takes. */
885
977
  export interface Pointer {
@@ -955,12 +1047,16 @@ export declare function mergeNarratives(each: readonly {
955
1047
  session: string;
956
1048
  read: Narrative;
957
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";
958
1055
  /**
959
1056
  * The HOST's half of the guard: where the hooks that invoke any of it are registered.
960
1057
  *
961
1058
  * Fixed, because these are the harness's own file names and not a repo's choice — unlike the
962
- * repo's half, which is `configSurface` reading the config's imports. The old list spelled the two
963
- * 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.
964
1060
  *
965
1061
  * THE GUARD PACK KEEPS ITS OWN, WIDER LIST, and the difference is deliberate — this is the one
966
1062
  * place that says so, because two lists that look like copies are read as drift until somebody
@@ -980,7 +1076,18 @@ export declare function mergeNarratives(each: readonly {
980
1076
  * in, and that is the coupling the fence exists to prevent.
981
1077
  */
982
1078
  export declare const HOST_SURFACE: readonly string[];
983
- /** 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
+ */
984
1091
  export declare function guardPaths(configText: string | null): readonly string[];
985
1092
  /** Is this one of the files that decides what the guard does? */
986
1093
  export declare function isGuardPath(path: string, paths: readonly string[]): boolean;
@@ -1054,9 +1161,9 @@ export declare function health({ armed, rows, withRecord, analysed }: HealthArgs
1054
1161
  *
1055
1162
  * The sidecar says what a spawn WAS (`agentType`, unforgeable, written by the host) and the
1056
1163
  * parent's own `Agent` block says when it happened and what was asked for. Joining them on
1057
- * `toolUseId` is what lets classification and cost attribution name the same actor: a builder
1058
- * spawned under a generic bucket is `general-purpose` in the parent's claim and `builder` in the
1059
- * 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.
1060
1167
  */
1061
1168
  export interface SpawnRecord {
1062
1169
  /** The conversation that spawned it. */
@@ -1113,6 +1220,14 @@ export interface Steer {
1113
1220
  * of megabytes and the answer is one record.
1114
1221
  */
1115
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;
1116
1231
  /** The permission mode the last recorded call was made under, or null when no row states one. */
1117
1232
  export declare function lastMode(rows: readonly Row[]): string | null;
1118
1233
  /** The permission modes the read conversations ran under, and how many of them auto mode steered. */
@@ -1242,7 +1357,39 @@ export interface Registration {
1242
1357
  /** The host's `matcher`. Absent means every occurrence of the event. */
1243
1358
  readonly matcher?: string;
1244
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;
1245
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;
1246
1393
  /**
1247
1394
  * Is this registration OURS?
1248
1395
  *
@@ -1265,17 +1412,54 @@ export declare function registeredEvents(settings: unknown): string[];
1265
1412
  */
1266
1413
  export declare function staleRegistrations(settings: unknown): string[];
1267
1414
  /**
1268
- * A settings file with flow's registrations in it — a NEW object, and only ever added to.
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.
1269
1440
  *
1270
- * It is somebody's file and holds far more than us, so nothing is rewritten and nothing is removed:
1271
- * a registration on our event that belongs to another tool keeps its place and flow stands beside
1272
- * it. Adding what is already there is skipped, which is the whole of what makes `flow init`
1273
- * 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.
1274
1445
  */
1275
1446
  export declare function withRegistrations(settings: unknown): {
1276
1447
  settings: unknown;
1277
1448
  added: string[];
1278
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
+ };
1279
1463
  /**
1280
1464
  * WHERE THE GATE LIVES, spelled once for everyone who names it.
1281
1465
  *
@@ -1289,24 +1473,139 @@ export declare const GATE_PATH = ".githooks/pre-commit";
1289
1473
  /** What arms git, said the way a person types it — the report's line and the fitting's fix. */
1290
1474
  export declare const SET_HOOKS_PATH = "git config core.hooksPath .githooks";
1291
1475
  /**
1292
- * 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.
1293
1486
  *
1294
1487
  * `init` says it about the hook it just declined to overwrite; `status` says it about the hook it
1295
1488
  * found unarmed. The same instruction, at the two moments a person can meet it — and two spellings
1296
1489
  * of an instruction are two instructions the day one of them is edited.
1297
1490
  */
1298
- 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;
1299
1500
  /**
1300
1501
  * The pre-commit hook, verbatim.
1301
1502
  *
1302
- * `flow commit <files…>` rather than a sixth hook, because the commit moment is not delivered by any
1303
- * harness — git's own hook is what fires it, which is why every harness gets the gates for free. The
1304
- * staged set is computed here rather than inside flow: it is git's own question, and this file is
1305
- * 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.
1306
1506
  */
1307
- 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";
1308
- /** 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. */
1309
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[];
1310
1609
  /** The config file init writes: the demo, or the bare one `--empty` asks for. */
1311
1610
  export declare function scaffold(empty: boolean): string;
1312
1611
  /** What the shell found before anything was written. Every one is a stat, not a judgement. */
@@ -1321,8 +1620,11 @@ export interface InitFacts {
1321
1620
  readonly hasFlowDir: boolean;
1322
1621
  /** `git config core.hooksPath` as it stands. */
1323
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. */
1324
1626
  readonly settingsPath: string;
1325
- /** The parsed host settings file. */
1627
+ /** That file, parsed. */
1326
1628
  readonly settings: unknown;
1327
1629
  /**
1328
1630
  * The settings file is THERE and would not parse.
@@ -1334,6 +1636,15 @@ export interface InitFacts {
1334
1636
  * settings.json holding JSONC comments: the write was one branch away.
1335
1637
  */
1336
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;
1337
1648
  /** Does `@jawache/flow` already resolve from this repo? */
1338
1649
  readonly resolves: boolean;
1339
1650
  /**
@@ -1357,17 +1668,36 @@ export interface InitPlan {
1357
1668
  readonly gate: Write | null;
1358
1669
  /** A pre-commit hook that is somebody else's. Never overwritten — reported, with the line to add. */
1359
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;
1360
1673
  readonly notGit: boolean;
1361
1674
  readonly flowDir: boolean;
1362
1675
  /** The package to link into `node_modules/@jawache/flow`, or null when flow already resolves. */
1363
1676
  readonly link: string | null;
1677
+ /** Point git at `.githooks`: a git repo whose git runs no other hooks, and not already there. */
1364
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;
1365
1686
  /** The rewritten settings, or null when they already call flow — or could not be read. */
1366
1687
  readonly settings: {
1367
1688
  readonly path: string;
1368
1689
  readonly value: unknown;
1369
1690
  } | null;
1370
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;
1371
1701
  /** The settings file was there and would not parse, so nothing was registered. Said out loud. */
1372
1702
  readonly settingsUnreadable: string | null;
1373
1703
  }
@@ -1378,9 +1708,125 @@ export declare function planInit(facts: InitFacts): InitPlan;
1378
1708
  *
1379
1709
  * A setup command that prints "done" is a command nobody can check. Failures are handed in rather
1380
1710
  * than thrown because a PARTIAL init is the interesting case: the config landed, the settings file
1381
- * 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.
1713
+ */
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.
1382
1719
  */
1383
- export declare function initLines(plan: InitPlan, failures: readonly string[]): string[];
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[];
1384
1830
  /** One thing the world around the config has to be, and whether it is. A red line carries its fix. */
1385
1831
  export interface Fitting {
1386
1832
  readonly id: string;
@@ -1398,8 +1844,18 @@ export interface StatusFacts {
1398
1844
  readonly load: LoadResult | null;
1399
1845
  readonly gateText: string | null;
1400
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;
1401
1849
  readonly settingsPath: string;
1402
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;
1403
1859
  /**
1404
1860
  * The grammars the config declared, each with its library path made absolute and a straight
1405
1861
  * answer about whether that file is there.
@@ -1432,6 +1888,18 @@ export interface StatusFacts {
1432
1888
  readonly ignored: readonly string[];
1433
1889
  /** The config's `snapshotInclude` globs: the ignored paths the snapshot records anyway. */
1434
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
+ };
1435
1903
  }
1436
1904
  /**
1437
1905
  * The literal head of a glob, cut back to a whole segment: `src/gen/**` → `src/gen/`, `.env` →
@@ -1475,6 +1943,15 @@ export interface Status {
1475
1943
  readonly refusals: readonly Refusal[];
1476
1944
  readonly green: boolean;
1477
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[];
1478
1955
  /** Does this adapter carry that moment, for that KIND of entry? */
1479
1956
  export declare function delivers(kind: EntrySpec["kind"], moment: Moment): boolean;
1480
1957
  /** The gathered facts, judged. Never throws: a broken config is a red line, not an exception. */