@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.
- package/README.md +15 -9
- package/dist/flow.mjs +94 -73
- package/dist/index.mjs +22 -16
- package/dist/packs.mjs +103 -68
- package/dist/types/adapter/archive.d.ts +8 -1
- package/dist/types/adapter/claude.d.ts +46 -5
- package/dist/types/adapter/domain.d.ts +369 -9
- package/dist/types/checks/domain.d.ts +111 -16
- package/dist/types/engine/domain.d.ts +10 -2
- package/dist/types/glob.d.ts +17 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/language/domain.d.ts +42 -1
- package/dist/types/packs/docs.d.ts +3 -4
- package/dist/types/packs/work.d.ts +18 -2
- package/package.json +1 -1
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
161
|
-
*
|
|
162
|
-
*
|
|
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
|
|
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
|
|
201
|
-
* would remove — and the old engine ran two evaluations off that one hook
|
|
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
|
-
/**
|
|
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;
|