@jawache/flow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENCE +21 -0
- package/README.md +309 -0
- package/dist/flow.mjs +167 -0
- package/dist/index.mjs +18 -0
- package/dist/packs.mjs +116 -0
- package/dist/types/adapter/archive.d.ts +45 -0
- package/dist/types/adapter/claude.d.ts +157 -0
- package/dist/types/adapter/domain.d.ts +1112 -0
- package/dist/types/adapter/product.d.ts +10 -0
- package/dist/types/checks/domain.d.ts +792 -0
- package/dist/types/engine/domain.d.ts +747 -0
- package/dist/types/engine/state.d.ts +104 -0
- package/dist/types/errors.d.ts +14 -0
- package/dist/types/flow.d.ts +1 -0
- package/dist/types/glob.d.ts +53 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/language/domain.d.ts +641 -0
- package/dist/types/packs/docs.d.ts +49 -0
- package/dist/types/packs/fcis.d.ts +71 -0
- package/dist/types/packs/flow.d.ts +50 -0
- package/dist/types/packs/git.d.ts +107 -0
- package/dist/types/packs/index.d.ts +10 -0
- package/dist/types/packs/justfile.d.ts +83 -0
- package/dist/types/packs/node.d.ts +39 -0
- package/dist/types/packs/secrets.d.ts +117 -0
- package/dist/types/packs/tdd.d.ts +52 -0
- package/dist/types/packs/typescript.d.ts +101 -0
- package/dist/types/packs/work.d.ts +49 -0
- package/dist/types/version.d.ts +2 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1112 @@
|
|
|
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";
|
|
3
|
+
/**
|
|
4
|
+
* One hook payload, as Claude Code writes it on stdin.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately loose, every field optional: it is JSON from another program, so a shape this file
|
|
7
|
+
* DEMANDS is a shape that breaks the day the host adds a field or omits one. The index signature is
|
|
8
|
+
* not laziness either — F5's archival side reads keys this phase has no use for, and a payload
|
|
9
|
+
* narrowed on the way in cannot grow them back.
|
|
10
|
+
*/
|
|
11
|
+
export interface HookPayload {
|
|
12
|
+
readonly session_id?: string;
|
|
13
|
+
readonly agent_id?: string;
|
|
14
|
+
readonly hook_event_name?: string;
|
|
15
|
+
readonly transcript_path?: string;
|
|
16
|
+
readonly stop_hook_active?: boolean;
|
|
17
|
+
readonly tool_name?: string;
|
|
18
|
+
readonly tool_input?: Record<string, unknown>;
|
|
19
|
+
/** SessionStart's reason: `startup` · `resume` · `clear` · `compact` — the compaction announcement. */
|
|
20
|
+
readonly source?: string;
|
|
21
|
+
readonly [key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The five events flow registers for, named by the EVENT and never by the job.
|
|
25
|
+
*
|
|
26
|
+
* Carried from the old engine unchanged, and the naming rule with it: call a hook
|
|
27
|
+
* `flow hook plan-drift` and the day a second concern wants the same host event you register a
|
|
28
|
+
* second hook beside the first, which is the duplicate-firing problem the grammar exists to make
|
|
29
|
+
* impossible. One command per event; a second concern extends the function.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The one file that turns anything on, spelled ONCE.
|
|
33
|
+
*
|
|
34
|
+
* It lives in the pure home rather than in the shell that opens it because three things name it
|
|
35
|
+
* and only one of them is a read: the shell looks for it, the CLI defaults to it, and the list of
|
|
36
|
+
* files that ARE the guard here has to include it. A second spelling in that list would leave a
|
|
37
|
+
* renamed config's old name watched and its new one not.
|
|
38
|
+
*/
|
|
39
|
+
export declare const CONFIG_FILE = "flow.config.ts";
|
|
40
|
+
/**
|
|
41
|
+
* Every relative module the config names, READ OUT OF ITS TEXT.
|
|
42
|
+
*
|
|
43
|
+
* A regex over source, deliberately, and it is the only reader that can work here: this is asked
|
|
44
|
+
* about a file that will not import, so anything that had to resolve, parse or evaluate it would
|
|
45
|
+
* be asking the broken thing to answer for itself. A line like `import { house } from
|
|
46
|
+
* "./guards/house.ts";` survives almost every way a config breaks, because what breaks a config is
|
|
47
|
+
* a missing `.message(…)` or a stray brace two hundred lines below its imports.
|
|
48
|
+
*
|
|
49
|
+
* Only `./…` and `../…` count. A bare specifier is a package — `@jawache/flow`, or a pack somebody
|
|
50
|
+
* published — and a package is not this repo's source to repair. A `..` segment climbs out of the
|
|
51
|
+
* root, so those are dropped too: the config sits at the repo root and the surface is
|
|
52
|
+
* repo-relative or it is nothing. A leading dot in a FOLDER name is ordinary and is kept —
|
|
53
|
+
* `./.guard/house.ts` is a pack like any other.
|
|
54
|
+
*
|
|
55
|
+
* A `from '…'` phrase inside a QUOTED STRING is harvested as well — a rule whose `.message(…)`
|
|
56
|
+
* spells an import line puts that path on the surface though no import named it. ACCEPTED, not
|
|
57
|
+
* overlooked: telling a real import from a sentence quoting one takes a parser, and a parser is
|
|
58
|
+
* the one thing a file that will not parse cannot give. The cost is bounded and one-directional —
|
|
59
|
+
* the surface only ever WIDENS, it widens only while the config is already refusing every other
|
|
60
|
+
* write, and the commit gate goes on refusing until the config loads green.
|
|
61
|
+
*/
|
|
62
|
+
export declare function configImports(configText: string): readonly string[];
|
|
63
|
+
/**
|
|
64
|
+
* THE CONFIG SURFACE — the files a write may target while the guard is broken, and nothing else.
|
|
65
|
+
*
|
|
66
|
+
* Fail-loud says a config that will not load refuses every gated moment, and it is right: a guard
|
|
67
|
+
* that quietly carries on with yesterday's rules is a guard that lies about being there. But taken
|
|
68
|
+
* without exception it also refuses the ONE write that can end the outage, and then the doctrine's
|
|
69
|
+
* own instruction — "adjust the change so it passes, then retry" — is impossible to obey. A human
|
|
70
|
+
* in an editor never meets this (no hooks sit in front of them, so they just undo); an agent that
|
|
71
|
+
* broke the config is locked out of repairing it, and the repo stays broken until a human arrives.
|
|
72
|
+
* Measured twice on the crossing that introduced it, once for real.
|
|
73
|
+
*
|
|
74
|
+
* So the exception is exactly as wide as the repair and no wider: a write whose target is the
|
|
75
|
+
* config or a pack it imports. Everything else still refuses — every other path, every command,
|
|
76
|
+
* and the commit gate, which goes on refusing until the config loads green, so nothing written
|
|
77
|
+
* under the exception can reach a commit unreviewed by a working guard.
|
|
78
|
+
*
|
|
79
|
+
* DERIVED FROM THE CONFIG'S OWN TEXT, and it used to be a two-line constant: the config file, plus
|
|
80
|
+
* one repo's pack folder spelled into the engine. Every other repo got a repair exception over a
|
|
81
|
+
* folder it does not have, and an agent that broke the config there could not reach the pack it
|
|
82
|
+
* broke. The text is readable when the module is not, so nothing is given up by asking it — and the
|
|
83
|
+
* answer now follows whatever a repo actually called the folder.
|
|
84
|
+
*
|
|
85
|
+
* The FOLDER of each imported pack comes with the file, which is the one deliberate widening. A
|
|
86
|
+
* repair is rarely one file: a pack imports its own helpers, and those the config never names. The
|
|
87
|
+
* import graph cannot be walked (walking it means resolving, and resolving is what is broken), so
|
|
88
|
+
* the folder is the honest approximation — and it is exactly the width the old constant had.
|
|
89
|
+
*/
|
|
90
|
+
export declare function configSurface(configText: string | null): readonly string[];
|
|
91
|
+
export declare const HOOK_EVENTS: readonly ["session-start", "pre-tool-use", "post-tool-use", "stop", "notification"];
|
|
92
|
+
export type HookEvent = (typeof HOOK_EVENTS)[number];
|
|
93
|
+
export declare function isHookEvent(word: string): word is HookEvent;
|
|
94
|
+
/**
|
|
95
|
+
* WHAT THIS HARNESS CAN HONESTLY SUPPLY — the seventh question, and the one that keeps the seam
|
|
96
|
+
* from rotting quietly. An adapter never fakes a moment it cannot deliver.
|
|
97
|
+
*
|
|
98
|
+
* Two facts are stated here rather than discovered by somebody whose rule never fires:
|
|
99
|
+
*
|
|
100
|
+
* · `commit` is delivered, but not by the harness — git's pre-commit hook calls `flow commit`,
|
|
101
|
+
* which is why every harness gets the gates for free and why it is in this list at all.
|
|
102
|
+
* · turn-end BRIEFING is not delivered. Stop's decision object carries no context channel, so a
|
|
103
|
+
* `breadcrumb().at(turnEnd)` has nowhere to be shown; the guardrail rail at that moment works
|
|
104
|
+
* perfectly. Reporting an entry that goes dark is `flow status`'s job (F6).
|
|
105
|
+
*
|
|
106
|
+
* It is two lists rather than one because "turn-end" is a word in both vocabularies and the answer
|
|
107
|
+
* differs between them — a single flat list could only lie about one of them.
|
|
108
|
+
*/
|
|
109
|
+
export declare const DELIVERS: {
|
|
110
|
+
guard: readonly GuardrailMoment[];
|
|
111
|
+
brief: readonly BreadcrumbMoment[];
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* One event in the engine's vocabulary, minus the two things the engine's own event types demand
|
|
115
|
+
* and this file cannot know: who the session is (`wearing`, which needs the sidecar and the stored
|
|
116
|
+
* verdict) and how far it has drifted (`tokens`, which needs the transcript). The shell adds both,
|
|
117
|
+
* because both come off disk.
|
|
118
|
+
*/
|
|
119
|
+
export type AdapterEvent = {
|
|
120
|
+
readonly rail: "guard";
|
|
121
|
+
readonly moment: GuardrailMoment;
|
|
122
|
+
readonly file?: {
|
|
123
|
+
readonly path: string;
|
|
124
|
+
readonly content: string;
|
|
125
|
+
} | undefined;
|
|
126
|
+
readonly command?: string | undefined;
|
|
127
|
+
readonly staged?: readonly string[] | undefined;
|
|
128
|
+
readonly turn?: readonly TurnAction[] | undefined;
|
|
129
|
+
} | {
|
|
130
|
+
readonly rail: "brief";
|
|
131
|
+
readonly moment: BreadcrumbMoment;
|
|
132
|
+
readonly path?: string | undefined;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* What a payload needs from the world before it can become an event. Injected, never reached for.
|
|
136
|
+
*
|
|
137
|
+
* Every answer is a FUNCTION rather than a value, and that is not ceremony: a hook fires on every
|
|
138
|
+
* tool call, and the turn's actions cost a whole transcript to work out. Asking only when the
|
|
139
|
+
* moment needs it means the write rail — by far the most frequent — never opens the file at all.
|
|
140
|
+
*/
|
|
141
|
+
export interface EventWorld {
|
|
142
|
+
/** The repo root. Event paths are repo-relative, because that is how every `on` glob is written. */
|
|
143
|
+
readonly root: string;
|
|
144
|
+
/** A file as it stands right now, or null when it is not there — an Edit is a diff against it. */
|
|
145
|
+
readonly read: (path: string) => string | null;
|
|
146
|
+
/** What the actor did this turn. Only `stop` asks, so only `stop` pays for it. */
|
|
147
|
+
readonly turn?: (() => readonly TurnAction[]) | undefined;
|
|
148
|
+
}
|
|
149
|
+
/** An absolute path inside the repo → the repo-relative spelling every glob is written against. */
|
|
150
|
+
export declare function relativise(path: string, root: string): string;
|
|
151
|
+
/** One edit operation, as the Edit tool states it. */
|
|
152
|
+
export interface EditOp {
|
|
153
|
+
readonly old_string?: string;
|
|
154
|
+
readonly new_string?: string;
|
|
155
|
+
readonly replace_all?: boolean;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Fold one Edit into the text it would produce — the delicate half of the pre-emptive rail.
|
|
159
|
+
*
|
|
160
|
+
* The whole capability rests on reconstructing the would-be file EXACTLY as the Edit tool would
|
|
161
|
+
* write it: get this wrong and the guard judges a file that never existed, which is worse than not
|
|
162
|
+
* guarding at all. Null when the anchor is not in the base text — the caller must then stand aside
|
|
163
|
+
* rather than judge a guess.
|
|
164
|
+
*
|
|
165
|
+
* Lifted from the old engine with its behaviour intact, including the empty-anchor case:
|
|
166
|
+
* `old_string: ""` is a Write wearing an Edit's clothes, and the new text is the whole file.
|
|
167
|
+
*/
|
|
168
|
+
export declare function applyEdit(base: string, { old_string, new_string, replace_all }: EditOp): string | null;
|
|
169
|
+
/**
|
|
170
|
+
* The file as it WOULD be on disk after this tool call, disk untouched — J2.2, and the whole reason
|
|
171
|
+
* to guard at PreToolUse rather than after the write.
|
|
172
|
+
*
|
|
173
|
+
* Write carries the content outright; Edit is a diff against what is there; MultiEdit is a fold of
|
|
174
|
+
* several, and one unlocatable anchor abandons the whole reconstruction rather than judging a
|
|
175
|
+
* half-applied file. A tool that names no path is not a write at all.
|
|
176
|
+
*/
|
|
177
|
+
export declare function wouldBeFile(tool: string, input: Record<string, unknown>, world: EventWorld): {
|
|
178
|
+
path: string;
|
|
179
|
+
content: string;
|
|
180
|
+
} | null;
|
|
181
|
+
/**
|
|
182
|
+
* The paths a Bash line would remove — `rm`, `git rm`, and the SOURCE side of `mv` / `git mv`.
|
|
183
|
+
*
|
|
184
|
+
* Deletion has no tool of its own: it is a side effect of a shell command, so the delete moment is
|
|
185
|
+
* assembled here or it does not exist at all. `git mv` was the old engine's blind spot; it is
|
|
186
|
+
* covered, and quoting is respected, so a commit message mentioning `rm` is not a removal.
|
|
187
|
+
*
|
|
188
|
+
* A line that cannot be tokenised yields NOTHING, deliberately: `null` from the tokeniser means "I
|
|
189
|
+
* cannot tell", and a guard that guesses at a line it could not parse is worse than one that stands
|
|
190
|
+
* aside. The command rail still sees the raw line either way.
|
|
191
|
+
*/
|
|
192
|
+
export declare function deleteTargets(command: string): string[];
|
|
193
|
+
/**
|
|
194
|
+
* A payload → every event it carries. ZERO OR MORE, and the plural is not hedging.
|
|
195
|
+
*
|
|
196
|
+
* One Bash call is genuinely two moments — the command about to run, and each file an `rm` in it
|
|
197
|
+
* would remove — and the old engine ran two evaluations off that one hook for exactly this reason.
|
|
198
|
+
* A signature returning one event would push the second rail back into the shell, where it could
|
|
199
|
+
* not be tested as a decision.
|
|
200
|
+
*
|
|
201
|
+
* An event flow was not asked for answers with an empty list, never with an invented moment.
|
|
202
|
+
*/
|
|
203
|
+
export declare function toEvent(hook: HookEvent, payload: HookPayload, world: EventWorld): AdapterEvent[];
|
|
204
|
+
/** What a post-tool-use call was about, repo-relative: a path, or nothing this rail can steer on. */
|
|
205
|
+
export declare function touchedPath(payload: HookPayload, root: string): string | null;
|
|
206
|
+
/** What the binary prints and exits with. The three IO edges as data, so a test can read them. */
|
|
207
|
+
export interface HookResult {
|
|
208
|
+
/** stdout — a JSON decision the host reads. Empty means nothing to say. */
|
|
209
|
+
readonly stdout: string;
|
|
210
|
+
/** stderr — the refusal, shown to the model on exit 2 and to the human otherwise. */
|
|
211
|
+
readonly stderr: string;
|
|
212
|
+
/** 0 allows; 2 blocks. Never anything else — those are the host's two codes. */
|
|
213
|
+
readonly exitCode: 0 | 2;
|
|
214
|
+
}
|
|
215
|
+
export declare const ALLOW: HookResult;
|
|
216
|
+
/** One refusal, with the rail that made it — the banner is the moment's, not the hook's. */
|
|
217
|
+
export interface Refused {
|
|
218
|
+
readonly moment: GuardrailMoment;
|
|
219
|
+
readonly block: Block;
|
|
220
|
+
}
|
|
221
|
+
/** A note with its prose resolved: a `file:` breadcrumb is read by the shell, never here. */
|
|
222
|
+
export interface Shown {
|
|
223
|
+
readonly entry: string | null;
|
|
224
|
+
readonly cause: Cause;
|
|
225
|
+
readonly body: string;
|
|
226
|
+
}
|
|
227
|
+
/** Everything one payload produced, ready to become something the host understands. */
|
|
228
|
+
export interface Answer {
|
|
229
|
+
readonly refused: readonly Refused[];
|
|
230
|
+
readonly shown: readonly Shown[];
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Is this event the WRITE that could fix a config that will not load?
|
|
234
|
+
*
|
|
235
|
+
* The one exception to fail-loud, and it is deliberately the narrowest shape that works: a write
|
|
236
|
+
* (or a delete) whose target is the guard's own source. `configSurface` above says why that surface
|
|
237
|
+
* is read out of the config's own text rather than resolved.
|
|
238
|
+
*
|
|
239
|
+
* A COMMAND is never a repair, however plausible it looks. `sed -i` on the config would qualify by
|
|
240
|
+
* intent and there is no way to tell it from `rm -rf` before it runs — a command rail sees a string,
|
|
241
|
+
* not a target — so the command rail stays fully closed in the broken state, and the agent's route
|
|
242
|
+
* back is the edit tools, which name the file they are about to write.
|
|
243
|
+
*
|
|
244
|
+
* It lives HERE rather than in the shell beside its caller because it decides and reads nothing.
|
|
245
|
+
* The coverage gate over this file is what keeps the exception's exact width asserted.
|
|
246
|
+
*/
|
|
247
|
+
export declare function repairs(event: AdapterEvent, surface: readonly string[]): boolean;
|
|
248
|
+
/**
|
|
249
|
+
* WHAT EVERY RAIL ANSWERS WHILE THE CONFIG WILL NOT LOAD — one decision for both ways it can break.
|
|
250
|
+
*
|
|
251
|
+
* A config has two ways of not loading, and they used to get two different answers. One will not
|
|
252
|
+
* IMPORT — a missing module, a syntax error — which the shell catches as a thrown error. The other
|
|
253
|
+
* imports fine and the GRAMMAR refuses it: a missing `.message(…)`, an override naming an entry the
|
|
254
|
+
* pack does not have, a mandatory parameter nobody supplied. Only the first ever reached the repair
|
|
255
|
+
* exception, because the second was refused a layer deeper inside `guard()` — so the break the
|
|
256
|
+
* README and the guidebook both tell a reader to make was the one break an agent could not repair
|
|
257
|
+
* itself out of. To everyone who meets them the two are the same fact, "the guard is entirely off",
|
|
258
|
+
* and now they are the same answer as well.
|
|
259
|
+
*
|
|
260
|
+
* TURN-END IS TOLD, NOT REFUSED. Holding a turn open on a broken config prevents nothing risky —
|
|
261
|
+
* every write, command and commit is already refused and the commit gate stays shut — while it does
|
|
262
|
+
* stop the agent handing back to the one person who can fix it. So the fault is reported and the
|
|
263
|
+
* turn ends.
|
|
264
|
+
*
|
|
265
|
+
* THE ENGINE HAS ITS OWN PAIR OF `!load.ok` BRANCHES — `guard()` blocks with the fault, `brief()`
|
|
266
|
+
* notices it — and they are NOT this rule written twice. They answer a different question: "there
|
|
267
|
+
* is nothing to run, say so", for the two callers that have no repair to make and no turn to end,
|
|
268
|
+
* namely `replay` and the commit gate. Deleting them was considered at F3.B3 and measured first:
|
|
269
|
+
* `flow replay` over a config the grammar refuses reaches `brief()`'s branch and emits the fault
|
|
270
|
+
* notice, so the branch is live and its deletion would have dropped that notice silently. What IS
|
|
271
|
+
* shared is the shape — `Outage extends Answer`, so the hook rail's answer and every other answer
|
|
272
|
+
* are one type rather than two that happen to match.
|
|
273
|
+
*/
|
|
274
|
+
export interface Outage extends Answer {
|
|
275
|
+
/**
|
|
276
|
+
* The sentence turn-end is TOLD instead of being refused with, or null when no turn ended here.
|
|
277
|
+
*
|
|
278
|
+
* It is not `shown`: the Stop rail has no context channel to inject prose into, which is what
|
|
279
|
+
* `DELIVERS` already says, so the shell writes this one to stderr and answers 0.
|
|
280
|
+
*/
|
|
281
|
+
readonly told: string | null;
|
|
282
|
+
}
|
|
283
|
+
export declare function whileBroken(events: readonly AdapterEvent[], surface: readonly string[], message: string): Outage;
|
|
284
|
+
/**
|
|
285
|
+
* The one code `flow status` answers with.
|
|
286
|
+
*
|
|
287
|
+
* THE 1/2 SPLIT IS THE WHOLE POINT of there being two non-zero codes: "your guard caught something
|
|
288
|
+
* or is not fully wired" and "your guard cannot run at all" are different facts and a setup script,
|
|
289
|
+
* a hook or a CI step has to tell them apart. Status used to break that itself — a config that would
|
|
290
|
+
* not IMPORT answered 2 while a config the grammar REFUSED answered 1, which is the same fact under
|
|
291
|
+
* two codes. A refusal list means the document was refused whole and every rail is off, so it is 2
|
|
292
|
+
* however it broke; anything else that is not green is 1.
|
|
293
|
+
*/
|
|
294
|
+
export declare function statusCode(answer: Status): 0 | 1 | 2;
|
|
295
|
+
/**
|
|
296
|
+
* The block of prose a set of notes is injected as.
|
|
297
|
+
*
|
|
298
|
+
* The header says `breadcrumb` because that is the mechanism's name everywhere else — in the
|
|
299
|
+
* config, in the log, in `flow status`. The injected block was the last place the old system spoke
|
|
300
|
+
* one word to the agent and printed another to the human, and a mechanism you hear one name for and
|
|
301
|
+
* read another about is one you cannot look up.
|
|
302
|
+
*/
|
|
303
|
+
export declare function briefBlock(shown: readonly Shown[]): string;
|
|
304
|
+
/**
|
|
305
|
+
* Effects → the three IO edges. The SECOND and THIRD of the seven questions, answered once.
|
|
306
|
+
*
|
|
307
|
+
* BLOCKING IS EXIT 2 WITH THE TEXT ON STDERR, at every rail, and that is a deliberate narrowing of
|
|
308
|
+
* what the old engine did. It had three spellings — a PreToolUse `permissionDecision: "deny"`
|
|
309
|
+
* object for a write, exit 2 for a command, a `decision: "block"` object at Stop — three shapes for
|
|
310
|
+
* one answer, each with its own way of being subtly wrong, and only one of them works at every
|
|
311
|
+
* moment. Exit 2 is the host's documented refusal channel and the model reads stderr on all three
|
|
312
|
+
* rails, so flow has one refusal and one place to look when it fails to arrive.
|
|
313
|
+
*
|
|
314
|
+
* Injection stays the decision object, because there is no other channel for it.
|
|
315
|
+
*/
|
|
316
|
+
export declare function toResult(hook: HookEvent, answer: Answer): HookResult;
|
|
317
|
+
/**
|
|
318
|
+
* Every refusal as the one thing a refusal is, or null when nothing refused.
|
|
319
|
+
*
|
|
320
|
+
* It takes NO hook, and that is the point of it existing beside `toResult`: the commit gate is not
|
|
321
|
+
* a hook at all — git spawns it — so rendering its refusals through a hook-shaped function meant
|
|
322
|
+
* naming an event that never happened. The banner comes off the MOMENT, which every rail has,
|
|
323
|
+
* rather than off the event, which only the harness ones do.
|
|
324
|
+
*/
|
|
325
|
+
export declare function refused(refusals: readonly Refused[]): HookResult | null;
|
|
326
|
+
/** Hand the agent some context and allow — the one shape every steering rail emits. */
|
|
327
|
+
export declare function decision(hook: HookEvent, additionalContext: string): string;
|
|
328
|
+
/**
|
|
329
|
+
* What the guard says when it could not read its own payload.
|
|
330
|
+
*
|
|
331
|
+
* The three outcomes of a read are not one outcome. "There was nothing to guard" is safe and common
|
|
332
|
+
* — a hand-run, an empty stdin — and silence is the right answer to it. "I could not SEE what I was
|
|
333
|
+
* guarding" is the old engine's invisible swallow: both were folded into `{}`, so a PreToolUse rail
|
|
334
|
+
* holding an unreadable payload found no tool call, allowed, and told nobody. The sentence is the
|
|
335
|
+
* same on every rail; what differs is whether the rail blocks with it, and that is the caller's
|
|
336
|
+
* decision because it is a fact about what the event is FOR.
|
|
337
|
+
*/
|
|
338
|
+
export declare function faultText(hook: HookEvent, why: "unreadable" | "unparseable", detail: string): string;
|
|
339
|
+
/** One line of a transcript, parsed. `record` is the raw object — this file reads it, nobody else. */
|
|
340
|
+
export interface TranscriptLine {
|
|
341
|
+
/** The record's `type`: `user`, `assistant`, or whatever else the host writes. */
|
|
342
|
+
readonly kind: string;
|
|
343
|
+
readonly record: Record<string, unknown>;
|
|
344
|
+
/**
|
|
345
|
+
* 1-based position in the FILE, counting blank and unreadable lines.
|
|
346
|
+
*
|
|
347
|
+
* Ground truth about the file rather than narrative, and the reason it is counted here rather
|
|
348
|
+
* than by a consumer: the facts layer cites a transcript line back to a person, and a number
|
|
349
|
+
* derived from a filtered list points at the wrong line the moment anything is skipped.
|
|
350
|
+
*/
|
|
351
|
+
readonly line: number;
|
|
352
|
+
/** ISO-8601 UTC, exactly as the host wrote it. Null when the line carries none. */
|
|
353
|
+
readonly ts: string | null;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Every readable line of a transcript, in order.
|
|
357
|
+
*
|
|
358
|
+
* The two questions that can stop early do NOT come through here, and the reason is size: a real
|
|
359
|
+
* transcript is a couple of megabytes and a hook fires on every tool call, so parsing twenty
|
|
360
|
+
* thousand records to read one number is a cost paid on every keystroke. They walk the raw lines
|
|
361
|
+
* from whichever end answers first, through the same `parseLine` — one rule about what a line is,
|
|
362
|
+
* three ways of arriving at it.
|
|
363
|
+
*/
|
|
364
|
+
export declare function transcriptLines(jsonl: string): TranscriptLine[];
|
|
365
|
+
/** The token counts one `usage` record carries. */
|
|
366
|
+
export interface Usage {
|
|
367
|
+
readonly input_tokens?: number;
|
|
368
|
+
readonly cache_read_input_tokens?: number;
|
|
369
|
+
readonly cache_creation_input_tokens?: number;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* The context size a `usage` record represents — input plus BOTH cache buckets.
|
|
373
|
+
*
|
|
374
|
+
* Verified on a real transcript, and the reason the sum is spelled out: `input_tokens` alone is
|
|
375
|
+
* about nil, because the context lives in `cache_read_input_tokens`. A counter that summed the
|
|
376
|
+
* obvious field read ~0 forever, which looks exactly like a session that never drifts.
|
|
377
|
+
*/
|
|
378
|
+
export declare function contextTokens(usage: Usage | undefined): number;
|
|
379
|
+
/**
|
|
380
|
+
* How far this session has drifted, in context tokens — the FOURTH question, and drift's one input.
|
|
381
|
+
*
|
|
382
|
+
* The LAST message carrying usage, because that is the current size; an earlier one describes a
|
|
383
|
+
* context that no longer exists. Unreadable or absent is 0, which the engine reads as "no drift
|
|
384
|
+
* yet": a note then shows on first touch and not again, which is the harmless direction.
|
|
385
|
+
*/
|
|
386
|
+
export declare function tokensFromTranscript(jsonl: string): number;
|
|
387
|
+
/**
|
|
388
|
+
* The tools that CHANGE a file — which names count as an edit is harness knowledge, and this is the
|
|
389
|
+
* one set.
|
|
390
|
+
*
|
|
391
|
+
* There were two in the old engine and they had already drifted: the recorder's knew about
|
|
392
|
+
* `NotebookEdit` and the turn-end rule's did not, so a notebook edit was work for the log and not
|
|
393
|
+
* work for the rule watching for work. A fact about the host's tool names belongs in one place.
|
|
394
|
+
*/
|
|
395
|
+
export declare const EDIT_TOOLS: ReadonlySet<string>;
|
|
396
|
+
export declare function isInjected(prose: string): boolean;
|
|
397
|
+
/**
|
|
398
|
+
* What the actor did THIS TURN, in order — the fact a turn-end check is handed.
|
|
399
|
+
*
|
|
400
|
+
* The turn starts at the last thing the human typed. That is a real narrowing of the rule this
|
|
401
|
+
* replaces, which walked the whole file: over one turn the two agree (the check compares the last
|
|
402
|
+
* edit against the last run), but over a resumed session of forty turns the old one was answering
|
|
403
|
+
* about work from hours ago while its message said "this turn".
|
|
404
|
+
*
|
|
405
|
+
* And what comes back is a NARROW, harness-neutral answer — an ordered list of edits and runs —
|
|
406
|
+
* rather than the transcript itself. The rule this replaces read `$WORK_TURN_TRANSCRIPT` from the
|
|
407
|
+
* environment, opened the file and parsed this JSONL inside the check: three reads of the world in
|
|
408
|
+
* the one place the design forbids them, and it made that rule the only core rule no case could
|
|
409
|
+
* drive.
|
|
410
|
+
*/
|
|
411
|
+
export declare function turnActions(jsonl: string, root?: string): TurnAction[];
|
|
412
|
+
/**
|
|
413
|
+
* The brief a session was started with, verbatim — the first thing the human, or the spawning
|
|
414
|
+
* agent, said.
|
|
415
|
+
*
|
|
416
|
+
* It is rung 2 of the settled classification order, and only rung 2: a brief is consulted for
|
|
417
|
+
* generic spawn buckets and never for a specifically-typed one, because a brief QUOTES — four
|
|
418
|
+
* verifier briefs in the 335-transcript probe matched builder patterns, since a verifier is briefed
|
|
419
|
+
* with what the builder claimed to have done. That rule is enforced in the engine's `spawnedAs`;
|
|
420
|
+
* what this owes is the text, honestly and whole, capped only so a runaway paste cannot become the
|
|
421
|
+
* thing every classifier scans.
|
|
422
|
+
*/
|
|
423
|
+
export declare function briefHead(jsonl: string, cap?: number): string;
|
|
424
|
+
/**
|
|
425
|
+
* The branch a worktree has out, from the text of its own `HEAD` — the session marker's key.
|
|
426
|
+
*
|
|
427
|
+
* A ref name, or the short sha when HEAD is detached, or null when there is nothing there. It is
|
|
428
|
+
* read as a FILE rather than asked of `git`, and at this frequency that matters: the marker is
|
|
429
|
+
* written on every tool call, and a subprocess per keystroke is a cost this answer does not justify.
|
|
430
|
+
* It is also the only form that answers on an unborn branch, where `git rev-parse HEAD` fails
|
|
431
|
+
* outright — which is every repo's own first commit.
|
|
432
|
+
*/
|
|
433
|
+
export declare function branchFromHead(head: string): string | null;
|
|
434
|
+
/**
|
|
435
|
+
* The sidecar beside a subagent's transcript: `…/agent-<id>.jsonl` → `…/agent-<id>.meta.json`.
|
|
436
|
+
*
|
|
437
|
+
* The host writes one for every spawn — present 335/335 in the probe — and it is the strongest
|
|
438
|
+
* evidence in the system precisely because the session did not write it. A session could claim to
|
|
439
|
+
* be anything, and permissions resting on the claim would rest on a lie.
|
|
440
|
+
*/
|
|
441
|
+
export declare function sidecarPath(transcriptPath: string): string | null;
|
|
442
|
+
/**
|
|
443
|
+
* Host-written evidence about one session × agent, assembled — the shape a classifier sees.
|
|
444
|
+
*
|
|
445
|
+
* The sidecar's absence is itself the evidence for the top-level session: there is no positive
|
|
446
|
+
* marker for being the parent, only the absence of having been spawned. A sidecar that will not
|
|
447
|
+
* parse is NOT the same as none — the session was spawned, we simply cannot say as what — so
|
|
448
|
+
* `subagent` stays true and `agentType` is left undefined, and a category keyed on a specific type
|
|
449
|
+
* declines rather than the parent category wrongly claiming it.
|
|
450
|
+
*/
|
|
451
|
+
export declare function sessionFactsFrom(args: {
|
|
452
|
+
readonly payload: HookPayload;
|
|
453
|
+
readonly transcript: string | null;
|
|
454
|
+
readonly sidecar: string | null;
|
|
455
|
+
}): SessionFacts;
|
|
456
|
+
/**
|
|
457
|
+
* A tool call → the row that records it, or null when the payload names no tool at all.
|
|
458
|
+
*
|
|
459
|
+
* EVERY call, including the ones no rule watches and no breadcrumb covers. The subject is whatever
|
|
460
|
+
* the call had — a path (repo-relative, so it matches the globs an entry is written with), a
|
|
461
|
+
* command line, a search pattern and its scope — and a call with no subject is still recorded by
|
|
462
|
+
* name, because "the session ran fifty greps here" is a fact about the area.
|
|
463
|
+
*/
|
|
464
|
+
export declare function toolRow(payload: HookPayload, root?: string): Row | null;
|
|
465
|
+
/**
|
|
466
|
+
* The variables git exports into a hook's environment, which REDIRECT any nested `git` command back
|
|
467
|
+
* at the parent repo — a `git init` in a temp directory would suddenly write the real index.
|
|
468
|
+
*
|
|
469
|
+
* The commit gate runs inside git's pre-commit hook and inherits every one of them, so a check that
|
|
470
|
+
* shells out (a test suite, a type-checker, dependency-cruiser writing a temp config) would run
|
|
471
|
+
* inside the in-progress commit unless they are stripped. It was one rule script's own business in
|
|
472
|
+
* the old engine; it is the adapter's now, because `ctx.exec` is the one door and this is a fact
|
|
473
|
+
* about the world on the other side of it.
|
|
474
|
+
*/
|
|
475
|
+
export declare const GIT_HOOK_ENV: readonly string[];
|
|
476
|
+
/** The environment a check's command should see: this one, minus git's hook plumbing. */
|
|
477
|
+
export declare function hermeticEnv(env: Readonly<Record<string, string | undefined>>): Record<string, string | undefined>;
|
|
478
|
+
/**
|
|
479
|
+
* The folder a working directory's transcripts live in.
|
|
480
|
+
*
|
|
481
|
+
* Every character that is not a letter or a digit becomes a dash — verified against all 63 folders
|
|
482
|
+
* on this machine carrying a readable cwd, with zero mismatches, rather than taken from a summary.
|
|
483
|
+
* A dot is not special: `/Users/x/.work/y` yields a doubled dash, and that doubling is the check
|
|
484
|
+
* that this is the real rule rather than a hand-written approximation (the old signposts spelling
|
|
485
|
+
* was `[/.]` and was wrong for an underscore or a space).
|
|
486
|
+
*
|
|
487
|
+
* THE RULE IS NOT INVERTIBLE, and that matters more here than anywhere else it is used: a folder
|
|
488
|
+
* named `…-workbench-workflow-flow` is `workbench.workflow-flow` and `workbench/workflow/flow` and
|
|
489
|
+
* `workbench_workflow_flow`, all three. Which worktree a transcript belongs to is answered by the
|
|
490
|
+
* `cwd` field the host writes INSIDE the records — see `cwdOf` — never by reading the name back.
|
|
491
|
+
*/
|
|
492
|
+
export declare function projectFolderName(cwd: string): string;
|
|
493
|
+
/** One transcript file worth reading, and the sidecar beside it. */
|
|
494
|
+
export interface TranscriptRef {
|
|
495
|
+
/** Relative to the project folder. */
|
|
496
|
+
readonly path: string;
|
|
497
|
+
/** The sidecar's path, only when the listing actually held it — never a probe. */
|
|
498
|
+
readonly meta: string | null;
|
|
499
|
+
readonly session: string;
|
|
500
|
+
/** Null for a top-level session; the `agent-…` stem for a subagent. */
|
|
501
|
+
readonly agent: string | null;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* A flat listing of a project folder, sorted into the transcripts worth reading.
|
|
505
|
+
*
|
|
506
|
+
* Two shapes count and nothing else does: `<session>.jsonl` at the top (a conversation somebody
|
|
507
|
+
* sat in front of) and `<session>/subagents/<agent>.jsonl` (a child it spawned). A worktree holds
|
|
508
|
+
* several sessions — a chat resumed on another day is a second file — so every one is enumerated
|
|
509
|
+
* rather than a newest being picked.
|
|
510
|
+
*
|
|
511
|
+
* Anything else comes back as IGNORED rather than being silently skipped, because "the folder held
|
|
512
|
+
* something I did not understand" is a fact a reader should be able to see. A `.meta.json` is the
|
|
513
|
+
* one exception: it is read through the transcript it belongs to, so naming it would be noise.
|
|
514
|
+
*/
|
|
515
|
+
export declare function classifyStore(paths: readonly string[]): {
|
|
516
|
+
transcripts: TranscriptRef[];
|
|
517
|
+
ignored: string[];
|
|
518
|
+
};
|
|
519
|
+
/**
|
|
520
|
+
* The sidecar the host writes beside every subagent transcript, read.
|
|
521
|
+
*
|
|
522
|
+
* `toolUseId` is the field that makes this more than a label: it is the id of the parent's own
|
|
523
|
+
* `Agent` tool call, so a sidecar and the block that spawned it join exactly. That join is what
|
|
524
|
+
* lets cost attribution and classification agree about who an actor was — the sidecar says what a
|
|
525
|
+
* spawn WAS, the parent's block says when it happened and what it was asked for, and neither
|
|
526
|
+
* alone answers both.
|
|
527
|
+
*/
|
|
528
|
+
export interface SpawnMeta {
|
|
529
|
+
readonly agentType: string | null;
|
|
530
|
+
readonly description: string | null;
|
|
531
|
+
/** The parent's tool_use id. Null on a sidecar written before the field existed. */
|
|
532
|
+
readonly toolUseId: string | null;
|
|
533
|
+
/** The model the spawn asked for — an alias like `opus`, never the served model id. */
|
|
534
|
+
readonly model: string | null;
|
|
535
|
+
readonly spawnDepth: number | null;
|
|
536
|
+
}
|
|
537
|
+
/** A sidecar's text → what it says. Null when it is absent or will not parse. */
|
|
538
|
+
export declare function spawnMeta(raw: string | null): SpawnMeta | null;
|
|
539
|
+
/** The three things knowable about a conversation without reading what was said in it. */
|
|
540
|
+
export interface TranscriptHead {
|
|
541
|
+
/**
|
|
542
|
+
* The working directory it was recorded in — THE ONLY HONEST ANSWER to "which worktree is this".
|
|
543
|
+
*
|
|
544
|
+
* `projectFolderName` collapses dots, slashes and underscores into one character, so two
|
|
545
|
+
* worktrees of one repo — `workbench` and `workbench.workflow-flow` — share a folder name and a
|
|
546
|
+
* reader that inverted it would attribute one checkout's conversations to the other.
|
|
547
|
+
*/
|
|
548
|
+
readonly cwd: string | null;
|
|
549
|
+
readonly branch: string | null;
|
|
550
|
+
/** When it started, as the host stamped it. */
|
|
551
|
+
readonly started: string | null;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* A conversation's opening facts, in ONE pass, stopping as soon as all three are known.
|
|
555
|
+
*
|
|
556
|
+
* It was three functions and therefore three parses of the same multi-megabyte file, once per
|
|
557
|
+
* conversation in the store — a cost paid on every `flow facts` run for an answer that is in the
|
|
558
|
+
* first few records. The file opens with stub records (last-prompt, mode, permission-mode) that
|
|
559
|
+
* carry no fields at all, so a literal first line is never enough and the walk has to be able to
|
|
560
|
+
* continue; what it must not do is start over twice.
|
|
561
|
+
*/
|
|
562
|
+
export declare function transcriptHead(jsonl: string): TranscriptHead;
|
|
563
|
+
/** One spawn as the PARENT recorded it: the tool call, joined to the sidecar by its id. */
|
|
564
|
+
export interface Spawn {
|
|
565
|
+
readonly toolUseId: string;
|
|
566
|
+
/** What the parent asked for — its own claim, which the sidecar's `agentType` outranks. */
|
|
567
|
+
readonly asked: string | null;
|
|
568
|
+
readonly description: string | null;
|
|
569
|
+
readonly ts: string | null;
|
|
570
|
+
readonly line: number;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Every spawn a parent transcript made, by tool_use id — the parent's half of the join.
|
|
574
|
+
*
|
|
575
|
+
* It is the WEAKER half and is read as such: what the parent asked for is a claim by the session
|
|
576
|
+
* being classified, while the sidecar is host-written and unforgeable. The classification order
|
|
577
|
+
* (engine, `spawnedAs`) reads the sidecar and never this. What this is for is everything the
|
|
578
|
+
* sidecar cannot say — when the spawn happened, and where in the parent's own transcript to look.
|
|
579
|
+
*/
|
|
580
|
+
export declare function spawnsIn(jsonl: string): Map<string, Spawn>;
|
|
581
|
+
/** Text with terminal colour codes taken out — a recorded command line is full of them. */
|
|
582
|
+
export declare function strip(value: unknown): string;
|
|
583
|
+
/**
|
|
584
|
+
* ONE definition of "make this printable and short": strip ANSI, collapse whitespace, cut to `n`
|
|
585
|
+
* — and MARK the cut, because a truncated command with no ellipsis reads as a whole one, and every
|
|
586
|
+
* surface that shows these strings claims to be showing what really happened.
|
|
587
|
+
*/
|
|
588
|
+
export declare function snip(value: unknown, n?: number): string;
|
|
589
|
+
/** One thing that happened in a conversation, with the line it happened on. */
|
|
590
|
+
export type TranscriptEvent = {
|
|
591
|
+
readonly line: number;
|
|
592
|
+
readonly kind: "prompt";
|
|
593
|
+
readonly text: string;
|
|
594
|
+
} | {
|
|
595
|
+
readonly line: number;
|
|
596
|
+
readonly kind: "result";
|
|
597
|
+
readonly id: string;
|
|
598
|
+
readonly content: string;
|
|
599
|
+
readonly failed: boolean;
|
|
600
|
+
} | {
|
|
601
|
+
readonly line: number;
|
|
602
|
+
readonly kind: "use";
|
|
603
|
+
readonly ts: string | null;
|
|
604
|
+
readonly id: string;
|
|
605
|
+
readonly name: string;
|
|
606
|
+
readonly input: Record<string, unknown>;
|
|
607
|
+
/** The file the call names, repo-relative. Empty when it names none. */
|
|
608
|
+
readonly path: string;
|
|
609
|
+
};
|
|
610
|
+
/**
|
|
611
|
+
* A transcript → the narrative events in it: typed prompts, tool calls, and their results.
|
|
612
|
+
*
|
|
613
|
+
* The PAIRING is this function's own — a `tool_use` block carries the id its `tool_result` answers
|
|
614
|
+
* to, and matching them is what turns a flat file into "the agent tried X and got back Y". The
|
|
615
|
+
* old engine did this over its own `JSON.parse` and its own type switch, beside the corpus reader
|
|
616
|
+
* doing the same over the same files, so "what is an assistant line" had two answers free to
|
|
617
|
+
* drift. It reads through `transcriptLines` now, and owns only the pairing.
|
|
618
|
+
*/
|
|
619
|
+
export declare function parseEvents(jsonl: string, root?: string): TranscriptEvent[];
|
|
620
|
+
/** Does this prompt read as the human pushing back? */
|
|
621
|
+
export declare function isCorrection(prose: string): boolean;
|
|
622
|
+
/** The tools a repo's justfile actually drives — what a direct invocation would be bypassing. */
|
|
623
|
+
export declare function recipeTools(justfile: string): string[];
|
|
624
|
+
/** What one Bash line was: the recipes it drove, the tools it went round, and whether it committed. */
|
|
625
|
+
export interface BashClass {
|
|
626
|
+
readonly recipes: readonly string[];
|
|
627
|
+
readonly bypasses: readonly string[];
|
|
628
|
+
readonly commits: boolean;
|
|
629
|
+
}
|
|
630
|
+
/** Read one command line against the repo's own recipe tools. HEURISTIC, like everything below. */
|
|
631
|
+
export declare function classifyBash(command: unknown, tools: readonly string[]): BashClass;
|
|
632
|
+
/** A pointer into a transcript — the shape every heuristic finding takes. */
|
|
633
|
+
export interface Pointer {
|
|
634
|
+
readonly line: number;
|
|
635
|
+
readonly text: string;
|
|
636
|
+
/** Which transcript, once several are merged into one report. */
|
|
637
|
+
readonly session?: string;
|
|
638
|
+
}
|
|
639
|
+
export interface Bypass extends Pointer {
|
|
640
|
+
readonly tool: string;
|
|
641
|
+
}
|
|
642
|
+
/** The same file edited over and over — a loop somebody was stuck in. */
|
|
643
|
+
export interface EditLoop {
|
|
644
|
+
readonly path: string;
|
|
645
|
+
readonly count: number;
|
|
646
|
+
readonly from: number;
|
|
647
|
+
readonly to: number;
|
|
648
|
+
readonly session?: string;
|
|
649
|
+
}
|
|
650
|
+
/** The same command run again and again — something that would not come right. */
|
|
651
|
+
export interface Retry {
|
|
652
|
+
readonly command: string;
|
|
653
|
+
readonly count: number;
|
|
654
|
+
readonly lines: readonly number[];
|
|
655
|
+
readonly session?: string;
|
|
656
|
+
}
|
|
657
|
+
export interface Narrative {
|
|
658
|
+
readonly stats: {
|
|
659
|
+
readonly edits: number;
|
|
660
|
+
readonly writes: number;
|
|
661
|
+
readonly commits: number;
|
|
662
|
+
readonly recipes: Readonly<Record<string, number>>;
|
|
663
|
+
readonly bypasses: Readonly<Record<string, number>>;
|
|
664
|
+
};
|
|
665
|
+
readonly prompts: readonly Pointer[];
|
|
666
|
+
readonly corrections: readonly Pointer[];
|
|
667
|
+
readonly bypassSites: readonly Bypass[];
|
|
668
|
+
readonly loops: readonly EditLoop[];
|
|
669
|
+
readonly retries: readonly Retry[];
|
|
670
|
+
/** Every in-repo file the session touched, with how often. The facts layer scores coverage. */
|
|
671
|
+
readonly touched: Readonly<Record<string, number>>;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* The narrative reading of one conversation. Everything here is a guess with a citation.
|
|
675
|
+
*
|
|
676
|
+
* `tools` is INJECTED — the shell reads the justfile — so the same call is reproducible from a
|
|
677
|
+
* fixture, which is the same discipline the guard itself is built on.
|
|
678
|
+
*/
|
|
679
|
+
export declare function narrative(events: readonly TranscriptEvent[], tools?: readonly string[]): Narrative;
|
|
680
|
+
/**
|
|
681
|
+
* Every narrative reading merged into one, each pointer tagged with the transcript it came from.
|
|
682
|
+
*
|
|
683
|
+
* The report is ONE aggregate whether it read one conversation or a hundred — the flags only
|
|
684
|
+
* re-scope which sessions it covers, never its shape — so this is where the per-session readings
|
|
685
|
+
* stop being chapters and become one list somebody can work down.
|
|
686
|
+
*/
|
|
687
|
+
export declare function mergeNarratives(each: readonly {
|
|
688
|
+
session: string;
|
|
689
|
+
read: Narrative;
|
|
690
|
+
}[]): Narrative;
|
|
691
|
+
/**
|
|
692
|
+
* The HOST's half of the guard: where the hooks that invoke any of it are registered.
|
|
693
|
+
*
|
|
694
|
+
* Fixed, because these are the harness's own file names and not a repo's choice — unlike the
|
|
695
|
+
* repo's half, which is `configSurface` reading the config's imports. The old list spelled the two
|
|
696
|
+
* halves as one constant, with one repo's pack folder standing in for every repo's.
|
|
697
|
+
*
|
|
698
|
+
* THE GUARD PACK KEEPS ITS OWN, WIDER LIST, and the difference is deliberate — this is the one
|
|
699
|
+
* place that says so, because two lists that look like copies are read as drift until somebody
|
|
700
|
+
* checks. Two things separate them:
|
|
701
|
+
*
|
|
702
|
+
* · They answer different questions. This list feeds `weakenedAfterBlock`, which reports "a rail
|
|
703
|
+
* refused, and then somebody edited the guard" — so it holds only files that can WEAKEN a
|
|
704
|
+
* rule the engine enforces. The pack's `editingTheGuardrails` is a nudge before you type, and
|
|
705
|
+
* it adds `.claude/skills/**`: a skill is prose an agent reads, so editing one is worth a
|
|
706
|
+
* word of caution and is not evidence that a guardrail was loosened. Merged, every skill edit
|
|
707
|
+
* after any block would be reported as a possible weakening, and the report earns its keep
|
|
708
|
+
* only while every line in it is worth reading.
|
|
709
|
+
* · Neither may import the other, and the shared home would be worse than the duplication.
|
|
710
|
+
* `flow-packs → flow-adapter` is fenced (a pack must not be able to tell which harness is
|
|
711
|
+
* running it) and so is the way back, so a shared constant could only live in the public
|
|
712
|
+
* door — which would put one harness's file names into the GRAMMAR every config is written
|
|
713
|
+
* in, and that is the coupling the fence exists to prevent.
|
|
714
|
+
*/
|
|
715
|
+
export declare const HOST_SURFACE: readonly string[];
|
|
716
|
+
/** Every file that decides what the guard does here — the config and its packs, plus the host's. */
|
|
717
|
+
export declare function guardPaths(configText: string | null): readonly string[];
|
|
718
|
+
/** Is this one of the files that decides what the guard does? */
|
|
719
|
+
export declare function isGuardPath(path: string, paths: readonly string[]): boolean;
|
|
720
|
+
/** A block, and the guardrail edit that followed it. A POINTER, never an accusation. */
|
|
721
|
+
export interface Weakening {
|
|
722
|
+
readonly entry: string | null;
|
|
723
|
+
readonly line: number;
|
|
724
|
+
readonly path: string;
|
|
725
|
+
readonly gapSeconds: number;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* A rail refused, and then somebody edited the guard — flagged, with both ends cited.
|
|
729
|
+
*
|
|
730
|
+
* The edit may be perfectly legitimate authoring, which is exactly why this reports a POINTER and
|
|
731
|
+
* lets a person read the two spots. What it cannot do is stay quiet: "the rule blocked me, so I
|
|
732
|
+
* changed the rule" is the one failure mode a guard cannot catch itself.
|
|
733
|
+
*/
|
|
734
|
+
export declare function weakenedAfterBlock(rows: readonly Row[], events: readonly TranscriptEvent[], paths: readonly string[]): Weakening[];
|
|
735
|
+
/** One transcript, as the selector needs it: enough to order by, and nothing it has to open. */
|
|
736
|
+
export interface Candidate {
|
|
737
|
+
readonly id: string;
|
|
738
|
+
readonly bytes: number;
|
|
739
|
+
readonly mtimeMs: number;
|
|
740
|
+
/** When the conversation began, as the host stamped it. Preferred over the file's mtime. */
|
|
741
|
+
readonly started: string | null;
|
|
742
|
+
readonly branch: string | null;
|
|
743
|
+
}
|
|
744
|
+
export interface SelectOpts {
|
|
745
|
+
/** Analyse only the first N in order. The rest resurface next time. */
|
|
746
|
+
readonly limit?: number | null | undefined;
|
|
747
|
+
/** Ignore the limit. */
|
|
748
|
+
readonly all?: boolean | undefined;
|
|
749
|
+
/** Order heaviest-first — wasted effort concentrates in the token-heavy conversations. */
|
|
750
|
+
readonly largest?: boolean | undefined;
|
|
751
|
+
/** A date floor, in ms. */
|
|
752
|
+
readonly since?: number | null | undefined;
|
|
753
|
+
}
|
|
754
|
+
export interface Selection {
|
|
755
|
+
readonly analyse: readonly Candidate[];
|
|
756
|
+
readonly excluded: readonly {
|
|
757
|
+
readonly id: string;
|
|
758
|
+
readonly reason: "limit" | "since";
|
|
759
|
+
}[];
|
|
760
|
+
readonly counts: {
|
|
761
|
+
readonly backlog: number;
|
|
762
|
+
readonly analysed: number;
|
|
763
|
+
readonly excluded: number;
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
/** The backlog, ordered and narrowed — and an honest account of what was left out, and why. */
|
|
767
|
+
export declare function selectSessions(candidates: readonly Candidate[], done: ReadonlySet<string>, opts?: SelectOpts): Selection;
|
|
768
|
+
export interface Health {
|
|
769
|
+
/** The record cannot be believed at all, and this says why. Nothing below it should be read. */
|
|
770
|
+
readonly blocked: string | null;
|
|
771
|
+
/** It can be believed with a caveat, and here they are. */
|
|
772
|
+
readonly warn: readonly string[];
|
|
773
|
+
}
|
|
774
|
+
export interface HealthArgs {
|
|
775
|
+
/** Does this repo have a recorded history at all? */
|
|
776
|
+
readonly armed: boolean;
|
|
777
|
+
/** Did the sessions being read record anything? */
|
|
778
|
+
readonly rows: number;
|
|
779
|
+
/** How many of the conversations being read had a stream of their own. */
|
|
780
|
+
readonly withRecord: number;
|
|
781
|
+
readonly analysed: number;
|
|
782
|
+
}
|
|
783
|
+
/** What a reader must know before believing anything under it. */
|
|
784
|
+
export declare function health({ armed, rows, withRecord, analysed }: HealthArgs): Health;
|
|
785
|
+
/**
|
|
786
|
+
* One spawned actor, as the two host-written records agree on it — THE join, at its consumer.
|
|
787
|
+
*
|
|
788
|
+
* The sidecar says what a spawn WAS (`agentType`, unforgeable, written by the host) and the
|
|
789
|
+
* parent's own `Agent` block says when it happened and what was asked for. Joining them on
|
|
790
|
+
* `toolUseId` is what lets classification and cost attribution name the same actor: a builder
|
|
791
|
+
* spawned under a generic bucket is `general-purpose` in the parent's claim and `builder` in the
|
|
792
|
+
* host's record, and only one of those is evidence.
|
|
793
|
+
*/
|
|
794
|
+
export interface SpawnRecord {
|
|
795
|
+
/** The conversation that spawned it. */
|
|
796
|
+
readonly session: string;
|
|
797
|
+
/** The `agent-…` stem — its own transcript's name. */
|
|
798
|
+
readonly agent: string;
|
|
799
|
+
/** The host's answer. Null when the sidecar was absent or would not parse: spawned, as what unknown. */
|
|
800
|
+
readonly agentType: string | null;
|
|
801
|
+
readonly description: string | null;
|
|
802
|
+
readonly model: string | null;
|
|
803
|
+
/** What the PARENT asked for. Null when the two records could not be joined. */
|
|
804
|
+
readonly asked: string | null;
|
|
805
|
+
readonly at: string | null;
|
|
806
|
+
/** Where in the parent's transcript to look. Null when unjoined. */
|
|
807
|
+
readonly line: number | null;
|
|
808
|
+
}
|
|
809
|
+
/** One subagent's two records, joined. An unjoinable sidecar still yields what it alone knows. */
|
|
810
|
+
export declare function joinSpawn(args: {
|
|
811
|
+
readonly session: string;
|
|
812
|
+
readonly agent: string;
|
|
813
|
+
readonly meta: SpawnMeta | null;
|
|
814
|
+
readonly spawns: ReadonlyMap<string, Spawn>;
|
|
815
|
+
}): SpawnRecord;
|
|
816
|
+
/** An area the conversations worked in that no entry's globs reach. */
|
|
817
|
+
export interface Uncovered {
|
|
818
|
+
readonly path: string;
|
|
819
|
+
readonly touches: number;
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* The files a conversation touched that nothing was watching — coverage read from the TRANSCRIPT.
|
|
823
|
+
*
|
|
824
|
+
* It is not a duplicate of the record's own gap list and the difference is the whole point: the
|
|
825
|
+
* rows can only answer for sessions flow was installed for, and the store holds every conversation
|
|
826
|
+
* this repo has ever had. A session with no record at all still says where the work went, which is
|
|
827
|
+
* exactly the reading a repo wants on the day it adopts flow — and the day after, when it wants to
|
|
828
|
+
* know whether the entries it just bound point anywhere near it.
|
|
829
|
+
*/
|
|
830
|
+
export declare function uncoveredAreas(touched: Readonly<Record<string, number>>, entries: readonly Bound[]): Uncovered[];
|
|
831
|
+
export interface Facts {
|
|
832
|
+
readonly root: string;
|
|
833
|
+
readonly store: {
|
|
834
|
+
readonly dir: string;
|
|
835
|
+
readonly exists: boolean;
|
|
836
|
+
readonly ignored: readonly string[];
|
|
837
|
+
};
|
|
838
|
+
/** Which conversations this run covers, and which it deliberately deferred. */
|
|
839
|
+
readonly selection: Selection;
|
|
840
|
+
readonly coverage: {
|
|
841
|
+
readonly analysed: number;
|
|
842
|
+
readonly withRecord: number;
|
|
843
|
+
readonly narrativeOnly: number;
|
|
844
|
+
};
|
|
845
|
+
/** The fail-loud header. Read it before believing anything else here. */
|
|
846
|
+
readonly health: Health;
|
|
847
|
+
/**
|
|
848
|
+
* The numbers, over the repo's WHOLE recorded history — deliberately not narrowed to the
|
|
849
|
+
* selection. A dead scope and a lead distribution are lifetime questions, and scoping them to
|
|
850
|
+
* one backlog would make a quiet week look like a retirement case.
|
|
851
|
+
*/
|
|
852
|
+
readonly metrics: Metrics;
|
|
853
|
+
readonly moments: MomentsView;
|
|
854
|
+
readonly terrain: readonly TerrainNode[];
|
|
855
|
+
/** The heuristic pointers, one list, each row tagged with the conversation it came from. */
|
|
856
|
+
readonly narrative: Narrative;
|
|
857
|
+
/** Where the conversations worked that nothing watches — the transcript's own coverage answer. */
|
|
858
|
+
readonly uncovered: readonly Uncovered[];
|
|
859
|
+
/** Every actor the read conversations spawned, both host-written records joined. */
|
|
860
|
+
readonly actors: readonly SpawnRecord[];
|
|
861
|
+
/** A rail refused, and then somebody edited the guard. A pointer, never an accusation. */
|
|
862
|
+
readonly weakened: readonly Weakening[];
|
|
863
|
+
/** How many conversations were marked read, when asked for. Null when not asked. */
|
|
864
|
+
readonly marked: number | null;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* The reading, as the lines a person reads. The whole of what `flow facts` prints.
|
|
868
|
+
*
|
|
869
|
+
* It is here rather than in the CLI shell on this repo's own standing precedent (927ca62, the
|
|
870
|
+
* guard status report): a report's text is a pile of branches — is it armed, is the history ample,
|
|
871
|
+
* did anything go quiet, was the guard edited after it refused — and every one of those branches
|
|
872
|
+
* is a judgement about what a reader most needs to know. In a shell they are untestable, and the
|
|
873
|
+
* one that matters most is the one that fires least.
|
|
874
|
+
*/
|
|
875
|
+
export declare function formatFacts(facts: Facts): string[];
|
|
876
|
+
/**
|
|
877
|
+
* The node version flow's config loader needs, and why it is a floor rather than a preference.
|
|
878
|
+
*
|
|
879
|
+
* A `flow.config.ts` is TypeScript and is loaded by node's own type stripping — there is no
|
|
880
|
+
* transpile step and there will not be one, because a guard that compiles its own config is a guard
|
|
881
|
+
* with a build to go wrong. Stripping is flag-free from node 24 (the Active LTS), so that is the
|
|
882
|
+
* floor `engines` states and the number this sentence names.
|
|
883
|
+
*/
|
|
884
|
+
export declare const NODE_FLOOR = 24;
|
|
885
|
+
/**
|
|
886
|
+
* Is this module one of the GUARDED REPO'S OWN TypeScript files — the config, and whatever it
|
|
887
|
+
* imports — which flow has to evaluate itself?
|
|
888
|
+
*
|
|
889
|
+
* Node decides a `.ts` file's module system from the nearest `package.json`: `type: "module"` makes
|
|
890
|
+
* it ESM, anything else makes it CommonJS, and there is no syntax detection for `.ts` the way there
|
|
891
|
+
* is for an extensionless-type `.js`. A config is written in `import` statements, so in a repo whose
|
|
892
|
+
* package.json says `commonjs` — or says nothing, which is what `npm init` writes, and is the
|
|
893
|
+
* ordinary JS repo — the config flow's own `init` just scaffolded cannot be loaded at all. Measured
|
|
894
|
+
* at 0.1.0's pre-flight over all four shapes: absent green (no package.json, so node falls back to
|
|
895
|
+
* syntax detection), `module` green, NO TYPE FIELD red, `commonjs` red. Every suite had missed it
|
|
896
|
+
* because the harness's throwaway repos carry no package.json.
|
|
897
|
+
*
|
|
898
|
+
* So flow loads its own config the way eslint and vitest load theirs: it reads the file, strips the
|
|
899
|
+
* types and hands node ESM, and the repo's package.json never enters into it. This is the pure half
|
|
900
|
+
* — WHICH urls that treatment is for — and `loadConsumerTypeScript` in the shell is the doing.
|
|
901
|
+
*
|
|
902
|
+
* THREE EXCLUSIONS, each a case that must keep node's own answer:
|
|
903
|
+
*
|
|
904
|
+
* · not a `file:` url — an http or data specifier is nothing flow put there.
|
|
905
|
+
* · `.d.ts` — declarations, which are types all the way down and evaluate to nothing.
|
|
906
|
+
* · anything under `node_modules` — node REFUSES to strip types there
|
|
907
|
+
* (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), on the grounds that a dependency ships built
|
|
908
|
+
* files. Stripping them here because a config happened to import one would quietly overrule that,
|
|
909
|
+
* for every package in the tree, from a guard. flow's own bundles are `.mjs` and never reach here.
|
|
910
|
+
*/
|
|
911
|
+
export declare function loadsAsStrippedModule(url: string): boolean;
|
|
912
|
+
/**
|
|
913
|
+
* The one sentence for "this config would not load", and the one place a version floor is named.
|
|
914
|
+
*
|
|
915
|
+
* A node too old to strip types fails on the IMPORT, with a syntax error about a colon. That is a
|
|
916
|
+
* machine fact arriving as a parse error — the least actionable shape a true statement can take —
|
|
917
|
+
* so it is rewritten here, once, for both readers: the `flow test` verb and the live hook rail,
|
|
918
|
+
* which would otherwise describe one breakage two ways.
|
|
919
|
+
*/
|
|
920
|
+
export declare function configLoadFault(file: string, message: string, nodeVersion: string): string;
|
|
921
|
+
/** One entry in a settings file's hook array: the command, and what it fires on. */
|
|
922
|
+
export interface Registration {
|
|
923
|
+
/** The host's own name for the event — `SessionStart`, `PreToolUse`, … */
|
|
924
|
+
readonly event: string;
|
|
925
|
+
readonly command: string;
|
|
926
|
+
/** The host's `matcher`. Absent means every occurrence of the event. */
|
|
927
|
+
readonly matcher?: string;
|
|
928
|
+
}
|
|
929
|
+
export declare const HOOK_REGISTRATIONS: readonly Registration[];
|
|
930
|
+
/**
|
|
931
|
+
* Is this registration OURS?
|
|
932
|
+
*
|
|
933
|
+
* The rule that says "this is ours" is the rule that would say "this may be removed", so it is
|
|
934
|
+
* narrow on purpose: the flow binary — by either name it legitimately goes by, with any path in
|
|
935
|
+
* front — invoking `hook <event>` for an event this build actually answers. A `work hook stop` from
|
|
936
|
+
* the engine flow replaces is NOT ours; sweeping those is the crossing's job, with that engine's own
|
|
937
|
+
* recogniser.
|
|
938
|
+
*/
|
|
939
|
+
export declare function ourHookCommand(command: string): boolean;
|
|
940
|
+
/** The events this settings file already calls flow for. What makes init idempotent and status honest. */
|
|
941
|
+
export declare function registeredEvents(settings: unknown): string[];
|
|
942
|
+
/**
|
|
943
|
+
* A settings file with flow's registrations in it — a NEW object, and only ever added to.
|
|
944
|
+
*
|
|
945
|
+
* It is somebody's file and holds far more than us, so nothing is rewritten and nothing is removed:
|
|
946
|
+
* a registration on our event that belongs to another tool keeps its place and flow stands beside
|
|
947
|
+
* it. Adding what is already there is skipped, which is the whole of what makes `flow init`
|
|
948
|
+
* idempotent — a second run has nothing to say.
|
|
949
|
+
*/
|
|
950
|
+
export declare function withRegistrations(settings: unknown): {
|
|
951
|
+
settings: unknown;
|
|
952
|
+
added: string[];
|
|
953
|
+
};
|
|
954
|
+
/**
|
|
955
|
+
* WHERE THE GATE LIVES, spelled once for everyone who names it.
|
|
956
|
+
*
|
|
957
|
+
* Four readers say these two paths — the hook `init` writes, the report that names the write, the
|
|
958
|
+
* fitting `status` checks, and the shell next door that stats the file. Spelled four times, a rename
|
|
959
|
+
* would leave one reader writing a path another never looks at, and the failure is the silent kind:
|
|
960
|
+
* a green report over a gate git is not running.
|
|
961
|
+
*/
|
|
962
|
+
export declare const HOOKS_DIR = ".githooks";
|
|
963
|
+
export declare const GATE_PATH = ".githooks/pre-commit";
|
|
964
|
+
/** What arms git, said the way a person types it — the report's line and the fitting's fix. */
|
|
965
|
+
export declare const SET_HOOKS_PATH = "git config core.hooksPath .githooks";
|
|
966
|
+
/**
|
|
967
|
+
* How to arm a gate flow will not write over, in the ONE wording both readers use.
|
|
968
|
+
*
|
|
969
|
+
* `init` says it about the hook it just declined to overwrite; `status` says it about the hook it
|
|
970
|
+
* found unarmed. The same instruction, at the two moments a person can meet it — and two spellings
|
|
971
|
+
* of an instruction are two instructions the day one of them is edited.
|
|
972
|
+
*/
|
|
973
|
+
export declare const ADD_THE_GATE_LINE = "add `flow commit $(git diff --cached --name-only)` to it";
|
|
974
|
+
/**
|
|
975
|
+
* The pre-commit hook, verbatim.
|
|
976
|
+
*
|
|
977
|
+
* `flow commit <files…>` rather than a sixth hook, because the commit moment is not delivered by any
|
|
978
|
+
* harness — git's own hook is what fires it, which is why every harness gets the gates for free. The
|
|
979
|
+
* staged set is computed here rather than inside flow: it is git's own question, and this file is
|
|
980
|
+
* already a git hook.
|
|
981
|
+
*/
|
|
982
|
+
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";
|
|
983
|
+
/** Does this pre-commit hook call flow? Asked by `init` before writing, and by `status` after. */
|
|
984
|
+
export declare function armsFlow(text: string | null): boolean;
|
|
985
|
+
/** The config file init writes: the demo, or the bare one `--empty` asks for. */
|
|
986
|
+
export declare function scaffold(empty: boolean): string;
|
|
987
|
+
/** What the shell found before anything was written. Every one is a stat, not a judgement. */
|
|
988
|
+
export interface InitFacts {
|
|
989
|
+
readonly root: string;
|
|
990
|
+
/** `--empty`: the wiring, with no demo. */
|
|
991
|
+
readonly empty: boolean;
|
|
992
|
+
readonly isGit: boolean;
|
|
993
|
+
readonly hasConfig: boolean;
|
|
994
|
+
/** The gate as it stands, or null when there is none. */
|
|
995
|
+
readonly gateText: string | null;
|
|
996
|
+
readonly hasFlowDir: boolean;
|
|
997
|
+
/** `git config core.hooksPath` as it stands. */
|
|
998
|
+
readonly hooksPath: string | null;
|
|
999
|
+
readonly settingsPath: string;
|
|
1000
|
+
/** The parsed host settings file. */
|
|
1001
|
+
readonly settings: unknown;
|
|
1002
|
+
/**
|
|
1003
|
+
* The settings file is THERE and would not parse.
|
|
1004
|
+
*
|
|
1005
|
+
* Distinct from `settings: null`, which means there is none — and the distinction is the whole
|
|
1006
|
+
* reason this field exists. Both read as "no registrations found", so an init that acted on the
|
|
1007
|
+
* absence would answer an unreadable file by writing a NEW one over it, taking the reader's
|
|
1008
|
+
* permissions, model and every other key with it. Measured at the crossing, against a real
|
|
1009
|
+
* settings.json holding JSONC comments: the write was one branch away.
|
|
1010
|
+
*/
|
|
1011
|
+
readonly settingsUnreadable: boolean;
|
|
1012
|
+
/** Does `@jawache/flow` already resolve from this repo? */
|
|
1013
|
+
readonly resolves: boolean;
|
|
1014
|
+
/**
|
|
1015
|
+
* Where the running binary's own package lives — what a link would point at, and NULL when the
|
|
1016
|
+
* walk up never found it. Null rather than an empty string on purpose: an empty string is a path
|
|
1017
|
+
* as far as the shell is concerned, and it would go on to attempt a symlink to nowhere and report
|
|
1018
|
+
* the failure of a step that should never have been planned.
|
|
1019
|
+
*/
|
|
1020
|
+
readonly packageRoot: string | null;
|
|
1021
|
+
}
|
|
1022
|
+
/** One file to write, repo-relative so the report reads the way a person would say it. */
|
|
1023
|
+
export interface Write {
|
|
1024
|
+
readonly path: string;
|
|
1025
|
+
readonly body: string;
|
|
1026
|
+
/** Set on the gate, which git will not run unless it is executable. */
|
|
1027
|
+
readonly mode?: number;
|
|
1028
|
+
}
|
|
1029
|
+
/** Everything `flow init` will do, decided before a byte is written. A null means "already there". */
|
|
1030
|
+
export interface InitPlan {
|
|
1031
|
+
readonly config: Write | null;
|
|
1032
|
+
readonly gate: Write | null;
|
|
1033
|
+
/** A pre-commit hook that is somebody else's. Never overwritten — reported, with the line to add. */
|
|
1034
|
+
readonly gateKept: boolean;
|
|
1035
|
+
readonly notGit: boolean;
|
|
1036
|
+
readonly flowDir: boolean;
|
|
1037
|
+
/** The package to link into `node_modules/@jawache/flow`, or null when flow already resolves. */
|
|
1038
|
+
readonly link: string | null;
|
|
1039
|
+
readonly hooksPath: boolean;
|
|
1040
|
+
/** The rewritten settings, or null when they already call flow — or could not be read. */
|
|
1041
|
+
readonly settings: {
|
|
1042
|
+
readonly path: string;
|
|
1043
|
+
readonly value: unknown;
|
|
1044
|
+
} | null;
|
|
1045
|
+
readonly registered: readonly string[];
|
|
1046
|
+
/** The settings file was there and would not parse, so nothing was registered. Said out loud. */
|
|
1047
|
+
readonly settingsUnreadable: string | null;
|
|
1048
|
+
}
|
|
1049
|
+
/** What init will do to this repo. Pure: every branch reads a fact the shell already gathered. */
|
|
1050
|
+
export declare function planInit(facts: InitFacts): InitPlan;
|
|
1051
|
+
/**
|
|
1052
|
+
* The report — every write named, and everything NOT done said out loud with what to do instead.
|
|
1053
|
+
*
|
|
1054
|
+
* A setup command that prints "done" is a command nobody can check. Failures are handed in rather
|
|
1055
|
+
* than thrown because a PARTIAL init is the interesting case: the config landed, the settings file
|
|
1056
|
+
* was read-only, and the person needs to know exactly that.
|
|
1057
|
+
*/
|
|
1058
|
+
export declare function initLines(plan: InitPlan, failures: readonly string[]): string[];
|
|
1059
|
+
/** One thing the world around the config has to be, and whether it is. A red line carries its fix. */
|
|
1060
|
+
export interface Fitting {
|
|
1061
|
+
readonly id: string;
|
|
1062
|
+
readonly ok: boolean;
|
|
1063
|
+
readonly detail: string;
|
|
1064
|
+
}
|
|
1065
|
+
/** Everything status was able to read. Gathering it is the shell's job; judging it is not. */
|
|
1066
|
+
export interface StatusFacts {
|
|
1067
|
+
readonly root: string;
|
|
1068
|
+
readonly version: string;
|
|
1069
|
+
readonly off: boolean;
|
|
1070
|
+
readonly configPath: string;
|
|
1071
|
+
readonly hasConfig: boolean;
|
|
1072
|
+
/** The load, or null when there is no config to load. */
|
|
1073
|
+
readonly load: LoadResult | null;
|
|
1074
|
+
readonly gateText: string | null;
|
|
1075
|
+
readonly hooksPath: string | null;
|
|
1076
|
+
readonly settingsPath: string;
|
|
1077
|
+
readonly settings: unknown;
|
|
1078
|
+
/** Who last worked in this worktree, from the marker the write rail leaves. Null when nobody has. */
|
|
1079
|
+
readonly session: {
|
|
1080
|
+
readonly id: string;
|
|
1081
|
+
readonly agent: string | null;
|
|
1082
|
+
readonly wearing: readonly string[];
|
|
1083
|
+
} | null;
|
|
1084
|
+
}
|
|
1085
|
+
/** The whole answer. */
|
|
1086
|
+
export interface Status {
|
|
1087
|
+
readonly root: string;
|
|
1088
|
+
readonly version: string;
|
|
1089
|
+
readonly armed: boolean;
|
|
1090
|
+
readonly configPath: string;
|
|
1091
|
+
readonly moments: MomentsView;
|
|
1092
|
+
/** Every category a bound entry names — the vocabulary this repo actually classifies by. */
|
|
1093
|
+
readonly categories: readonly string[];
|
|
1094
|
+
readonly session: StatusFacts["session"];
|
|
1095
|
+
/** Entries bound only at moments this harness cannot deliver. Loaded, counted, and dark. */
|
|
1096
|
+
readonly dark: readonly string[];
|
|
1097
|
+
readonly fittings: readonly Fitting[];
|
|
1098
|
+
readonly refusals: readonly Refusal[];
|
|
1099
|
+
readonly green: boolean;
|
|
1100
|
+
}
|
|
1101
|
+
/** Does this adapter carry that moment, for that KIND of entry? */
|
|
1102
|
+
export declare function delivers(kind: EntrySpec["kind"], moment: Moment): boolean;
|
|
1103
|
+
/** The gathered facts, judged. Never throws: a broken config is a red line, not an exception. */
|
|
1104
|
+
export declare function status(facts: StatusFacts): Status;
|
|
1105
|
+
/**
|
|
1106
|
+
* The whole answer as lines — the headline, what meets you and when, then everything not true yet.
|
|
1107
|
+
*
|
|
1108
|
+
* The branching is here rather than in the CLI shell for the reason every report in this package is:
|
|
1109
|
+
* which headline, whether a moment table is printable at all, when the close goes red — each of
|
|
1110
|
+
* those is a judgement about what a reader most needs to know, and in a shell not one has a test.
|
|
1111
|
+
*/
|
|
1112
|
+
export declare function statusLines(s: Status): string[];
|