@jawache/flow 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,5 +6,10 @@ export interface VerbResult {
6
6
  }
7
7
  /** `flow init [--empty]` — make this repo drivable, and say exactly what was created. */
8
8
  export declare function runInit(cwd: string, args: readonly string[]): VerbResult;
9
+ /**
10
+ * `flow uninstall [--machine]` — remove what `flow init` wrote in this repo, but the config, and say
11
+ * exactly what went. `--machine` takes flow's hooks out of the machine-wide settings file too.
12
+ */
13
+ export declare function runUninstall(cwd: string, args: readonly string[]): VerbResult;
9
14
  /** `flow status` — is flow working here, and what will meet me. Green means guarded. */
10
15
  export declare function runStatus(cwd: string, args: readonly string[]): Promise<VerbResult>;
@@ -1,4 +1,4 @@
1
- import type { Case, CaseWorld, Check, Ctx, ExecResult, Grammar, LoadedEntry, Moment, Unanswered } from "../language/domain.ts";
1
+ import type { Case, CaseWorld, Check, Ctx, ExecResult, Grammar, LoadedEntry, Moment, ShellCommand, Unanswered } from "../language/domain.ts";
2
2
  /** One pattern that matched, and the 1-based line it matched on. */
3
3
  export interface PatternHit {
4
4
  readonly pattern: string;
@@ -24,46 +24,70 @@ export declare const textBan: (options: {
24
24
  /**
25
25
  * Ban a dangerous command before it runs.
26
26
  *
27
- * A DETERRENT: the patterns ARE its scope, so matching nothing in a repo is the rule working, not
28
- * a rule watching the wrong place.
27
+ * A DETERRENT: the commands it names ARE its scope, so matching nothing in a repo is the rule
28
+ * working, not a rule watching the wrong place.
29
29
  *
30
- * MATCHED WHOLE, and that is the one difference from `textBan` above. A command is ONE subject
31
- * however many lines it occupies — `git commit -m "subject\n\nCo-Authored-By: …"` is a single
32
- * invocation, and the patterns that police it span the newline on purpose. Swept line by line
33
- * (which is right for a file, where a hit must be able to name its line) every one of those
34
- * patterns matches nothing, silently: the rule loads, counts, and cannot catch the thing it
35
- * names.
30
+ * EACH ENTRY IS A COMMAND IN WORDS — `"git push --force"` — read by the same parse as the line: a
31
+ * program, then the arguments that make it dangerous. A command in `ctx.commands` matches when its
32
+ * program, less its path, is the entry's, and its arguments include every one of the entry's, in
33
+ * any order. Arguments are compared whole, so `"work task"` is not `work task-list` and a quoted
34
+ * `'we tick later'` is not the word `tick`; a bundle of short flags counts letter by letter on both
35
+ * sides, so `"rm -rf"` is `rm -fr` and `rm -r -f`. A regex over a command's text could say none of
36
+ * that: it read `/usr/local/bin/work plan tick` as no tick and `rm -fr` as no `-rf`.
37
+ *
38
+ * One command at a time, so `git push origin main; ls -f` is no force push, and a heredoc's body
39
+ * is text fed to a program, not arguments — unless a shell reads it, when the parse has already
40
+ * made its lines commands of their own. A line that does not parse is refused when an entry's words
41
+ * all appear in it as whole words: a deterrent that cannot read a line errs toward refusing it.
42
+ *
43
+ * `wholeLine: true` is the escape hatch for a pattern that spans two commands on purpose
44
+ * (`curl … | sh`): each entry is then a regex, compiled when the rule loads and tried anywhere in
45
+ * the line as written, less its heredoc bodies. One that does not compile refuses to load.
46
+ *
47
+ * An entry that is not one command in words — a line that does not parse, two commands, a
48
+ * backslash, the mark of a regex written before words, or a wrapper such as `sudo` or a `FOO=1`
49
+ * assignment in front — refuses to load, rather than arming a ban that matches nothing, or every
50
+ * use of its program.
51
+ *
52
+ * docs/user/checks/ban-commands.md is the promise, case by case.
36
53
  */
37
54
  export declare const banCommands: (options: {
38
55
  ban: readonly string[];
56
+ wholeLine?: boolean;
39
57
  }) => Check;
40
58
  /**
41
- * A `banCommands` pattern for: a command that writes permanent prose, with a BACKTICK inside a
42
- * double-quoted argument.
43
- *
44
- * Reads as: <one of `heads`> … <an OPEN double quote> … <a backtick>, where the quote-pair walk
45
- * skips arguments that are already closed, and both scans refuse to cross a QUOTED heredoc opener
46
- * — inside one the shell substitutes nothing, so it is the sanctioned way to carry backticks and
47
- * must not be blocked. An unquoted `<<EOF` is deliberately still caught: there, substitution
48
- * really does happen. This is the one pattern that has to span a newline, and the reason
49
- * `banCommands` matches a command whole rather than line by line.
50
- *
51
- * A BUILDER over a `heads` list rather than one frozen expression, because the commands that
52
- * record prose belong to different packs: `git commit` and `gh pr|issue` are `git`'s, which every
53
- * repo binds, and the `work …` verbs are `work`'s, which only a repo running the work lifecycle
54
- * binds. One tail, two heads, and neither pack carries a copy of the other's — a second copy of
55
- * this expression is how the two would drift while both still read as armed.
56
- *
57
- * WHY IT IS CORE and not a helper beside the packs: TWO PACKS
58
- * SHARE IT, and that is the whole rule the fold settled — anything two packs share is a core
59
- * check. A shared file sitting beside the packs is a library with two users and no owner, and this
60
- * one proved it by growing four unrelated sections around this pair. Here it sits next to the
61
- * check it builds a pattern for, and the door that exports one exports both.
62
- */
63
- export declare function substitutionInProse(heads: readonly string[]): string;
59
+ * Refuse a command that records permanent prose when the shell would run a BACKTICK substitution
60
+ * while building it — a commit message, a PR or issue body, a journal entry.
61
+ *
62
+ * `heads` are the commands that record prose, each in words, `"gh pr"`, matched as `banCommands`
63
+ * matches an entry: the same program, less its path, and every argument of the head among the
64
+ * command's, compared whole. A head's words are found anywhere among the arguments, so a bare word
65
+ * passed as an option's value counts too: `"gh pr"` refuses `gh run list --branch pr`. A head that
66
+ * is not one command in words refuses to load, as an entry does. `gitSubcommands` names git's own,
67
+ * `commit`, and is read through `gitCommand`: git's options sit between `git` and its subcommand,
68
+ * so `-C '/a b' commit` is `commit` there, where the containment of `"git commit"` would also take
69
+ * `git log --grep commit`.
70
+ *
71
+ * The verdict reads the parse, never the line: a backtick the shell runs is one the parse lists as
72
+ * the command's substitution — in a double-quoted or bare argument, or in an unquoted heredoc the
73
+ * command is fed. A single-quoted argument, an escaped backtick and a QUOTED heredoc run nothing,
74
+ * so none is refused, and neither is a backtick in another command on the same line. A `$( … )`
75
+ * substitution is the sanctioned way to feed a heredoc into an argument, so it is not refused
76
+ * itself; what it runs is, when that holds a backtick — the unquoted `"$(cat <<EOF … EOF)"`
77
+ * message. A line that does not parse is not judged.
78
+ *
79
+ * A CORE CHECK because TWO PACKS SHARE IT: `git` polices `git commit` and the `gh` verbs, which
80
+ * every repo binds, and `work` the lifecycle verbs, which only a repo running the work lifecycle
81
+ * binds. Anything two packs share is a core check; a copy in each is how the two would drift
82
+ * while both still read as armed.
83
+ */
84
+ export declare const substitutionInProse: (options: {
85
+ heads: readonly string[];
86
+ gitSubcommands?: readonly string[];
87
+ }) => Check;
64
88
  /**
65
89
  * What a blocked prose-writing command is told, and it is the same sentence whichever pack caught
66
- * it: the mechanism, the absence of an undo, the fix, and the one false positive the rule owns.
90
+ * it: the mechanism, the absence of an undo, and the fix.
67
91
  *
68
92
  * ONE constant rather than a message per pack. The sentence never named the verb that was blocked
69
93
  * — it names the shape — so two copies would be two places for the same advice to drift, and the
@@ -86,6 +110,24 @@ export declare const SUBSTITUTION_MESSAGE: string;
86
110
  export declare const protectedPath: (options: {
87
111
  existingOnly?: boolean;
88
112
  }) => Check;
113
+ /**
114
+ * Only the listed categories may change a file: a write or a delete by any other actor is refused.
115
+ *
116
+ * It reads `ctx.actor` — the categories the acting session wears, recognised from what the host
117
+ * wrote about it, never from a claim it made — and nothing about the file. A session wearing none
118
+ * of `writers` is refused whatever it wrote, and one wearing none at all is refused too: "no
119
+ * category" is not "every category".
120
+ *
121
+ * `.for(…)` on an entry says which actors a RULE applies to; this says which actors may WRITE. Both
122
+ * read the same categories, and they answer different questions — a rule for a checker says what a
123
+ * checker may not do, this says that everybody but a builder may not write.
124
+ *
125
+ * An empty `writers` is refused at load: it would refuse every change by everyone, which is a
126
+ * frozen worktree, not a rule about who writes it.
127
+ */
128
+ export declare const oneWriter: (options: {
129
+ writers: readonly string[];
130
+ }) => Check;
89
131
  /**
90
132
  * A file must have a required companion on disk — every `src/domain/**` needs its `.test.ts`.
91
133
  *
@@ -197,7 +239,12 @@ export declare const jsonInvariant: (options: {
197
239
  /** Of some exported names, which are never mentioned in the sibling test's text? */
198
240
  export declare function unreferenced(names: readonly string[], siblingText: string): string[];
199
241
  /**
200
- * The names a TypeScript source exports — functions, classes and consts.
242
+ * The names a TypeScript source exports — functions, classes and consts, in source order.
243
+ *
244
+ * An `export const` is read as every name it binds: each declarator of `export const a = 1, b = 2`,
245
+ * and each name a destructuring pattern binds. A declarator with a type annotation is left out,
246
+ * destructured or not, and so is an ambient `export declare const`: the declaration sits under an
247
+ * `ambient_declaration`, not directly under the export, so it is never matched.
201
248
  *
202
249
  * A real parser rather than a regex, because `export const x = …` inside a string or a comment is
203
250
  * exactly the false positive that teaches people to ignore a rule. Parsing is not reaching the
@@ -206,7 +253,11 @@ export declare function unreferenced(names: readonly string[], siblingText: stri
206
253
  * milliseconds to load and no other check needs it.
207
254
  */
208
255
  export declare function exportedNames(content: string): Promise<string[]>;
209
- /** Every exported symbol is referenced in its sibling test. */
256
+ /**
257
+ * Every exported symbol is referenced in its sibling test.
258
+ *
259
+ * A refusal is one line: the sibling test once, then every export it never mentions.
260
+ */
210
261
  export declare const symbolsInSibling: (options: {
211
262
  sibling: string;
212
263
  }) => Check;
@@ -241,44 +292,35 @@ export declare const changeTogether: (options: {
241
292
  * aimed at a directory called HEAD.
242
293
  */
243
294
  export declare const GIT_VALUE_OPTS: ReadonlySet<string>;
295
+ /** What a command reader is handed: the line, and its parse when the engine made one. */
296
+ type CommandFacts = Pick<Ctx, "command" | "commands">;
244
297
  /**
245
- * A shell-ish tokeniser: quotes respected (so a commit message mentioning `git commit` is not a
246
- * second command), operators emitted as their own tokens.
247
- *
248
- * Null on unparseable input — an unclosed quote — and null means "cannot tell", which every caller
249
- * must read as "do not judge". A guard that guesses at a line it cannot parse is worse than one
250
- * that stands aside.
298
+ * The command line parsed as bash: `ctx.commands`, or the line parsed here when the ctx carries no
299
+ * parse at all — which is what an engine older than the parse hands these checks. Null when the
300
+ * line does not parse, and every reader takes null as "cannot tell".
251
301
  */
252
- export declare function tokenizeCommand(command: string): string[] | null;
253
- /** One `git …` in a command line, split where git stops and its subcommand starts. */
254
- export interface GitInvocation {
302
+ export declare function shellCommands(facts: CommandFacts): readonly ShellCommand[] | null;
303
+ /** One `git` command in the parse, split where git's own options stop and its subcommand starts. */
304
+ export interface GitCommand {
255
305
  /** git's OWN options, before the subcommand — where `-C` counts and only there. */
256
306
  readonly globals: readonly string[];
257
- /** The subcommand word (`commit`, `status`), or null when the invocation carries none. */
307
+ /** The subcommand word (`commit`, `status`), or null when the command carries none. */
258
308
  readonly subcommand: string | null;
259
309
  /** Everything after the subcommand — its own flags and arguments. */
260
310
  readonly args: readonly string[];
311
+ /** Where `args` starts in `command.args`: `args[i]` is `command.args[argsAt + i]`. */
312
+ readonly argsAt: number;
313
+ /** The parsed command it was read from: its heredocs are what `git commit -F -` reads. */
314
+ readonly command: ShellCommand;
261
315
  }
262
316
  /**
263
- * Every git invocation in a command line, in order. Null when the line cannot be parsed at all.
264
- *
265
- * A token only starts an invocation at a COMMAND position — the start of the line or just after an
266
- * operator — so a `git` inside a quoted message or as an argument to something else is not one.
267
- */
268
- export declare function gitInvocations(command: string): GitInvocation[] | null;
269
- /**
270
- * The body of the first heredoc in the raw command, or null.
271
- *
272
- * `git commit -F -` reads its message on stdin, and a QUOTED heredoc is the safe way to write one
273
- * containing backticks — a form this codebase's own rules push commits toward. The argument scan
274
- * cannot see it: there is no `-m` token, so a message written that way goes unjudged, and every
275
- * rule that reads a commit message is inert on it.
276
- *
277
- * Deliberately last-resort and deliberately dumb — it runs only when no inline message was found,
278
- * and reads the FIRST heredoc, because a commit command carrying two is not a shape anyone writes
279
- * and guessing between them would be worse than the gap.
317
+ * One parsed command read as git, or null when its program is not git. The parse has already
318
+ * found the program, so a `git` inside a quoted message, a heredoc body or another command's
319
+ * arguments is never one, and a `git` inside `$( … )` or a `bash -c` string is.
280
320
  */
281
- export declare function heredocBody(command: string): string | null;
321
+ export declare function gitCommand(command: ShellCommand): GitCommand | null;
322
+ /** Every git command the line runs, in the order the parse lists them; none when it does not parse. */
323
+ export declare function gitCommands(facts: CommandFacts): GitCommand[];
282
324
  /**
283
325
  * The message a `git commit` in this command line carries, or null when there is no judgeable
284
326
  * commit in it at all. Null is the "not our business" answer and every caller passes on it.
@@ -296,7 +338,15 @@ export declare function heredocBody(command: string): string | null;
296
338
  *
297
339
  * git commit -m MSG · -mMSG · --message MSG · --message=MSG judged
298
340
  * git commit -m SUBJECT -m BODY (repeated, any mix of forms) judged, joined as git joins them
299
- * git commit -F - <<'EOF' … EOF (any heredoc on the line) judged, via the heredoc body
341
+ * git commit -m "$(cat <<'EOF' … EOF)" judged, via that heredoc's body,
342
+ * which is what the shell passes
343
+ * git commit -F - <<'EOF' … EOF (the commit reads stdin) judged, via ITS heredoc's body
344
+ * python3 - <<'PY' … PY; git commit -F - <<'EOF' … EOF judged, via the commit's own
345
+ * heredoc — the first one is the
346
+ * python's script
347
+ * python3 - <<'PY' … PY; git commit -F msg.txt NOT judged — the heredoc is
348
+ * another command's script, and
349
+ * the message is on disk
300
350
  * git commit -F path/to/file NOT judged — the message is on
301
351
  * disk, and reading it is a read
302
352
  * of the world this layer does not
@@ -307,7 +357,7 @@ export declare function heredocBody(command: string): string | null;
307
357
  * The boundary is on the record: this rail polices AGENT-typed commits, which are its audience. A
308
358
  * human committing from an editor is not its subject and never was.
309
359
  */
310
- export declare function commitMessage(command: string): string | null;
360
+ export declare function commitMessage(facts: CommandFacts): string | null;
311
361
  /** One shell argument, single-quoted so nothing in it can be expanded or re-parsed. */
312
362
  export declare function quoteArg(value: string): string;
313
363
  /**
@@ -335,15 +385,16 @@ export declare function quoteArg(value: string): string;
335
385
  * options exactly as a shell chains directory changes: `cd /a && git -C b status` runs in `/a/b`,
336
386
  * and `-C '/a' -C 'b'` says precisely that. Nothing has to decide which of the two wins.
337
387
  *
338
- * ONLY A LEADING `cd`, and only when an operator follows its path. A `cd` in the middle of a chain
339
- * can be preceded by another that already moved, and a command line that walks two trees is beyond
340
- * what one answer can honestly describe — the same reason the FIRST git invocation decides below.
388
+ * ONLY A LEADING `cd`, and only when a command follows it. A `cd` in the middle of a chain can be
389
+ * preceded by another that already moved, and a command line that walks two trees is beyond what
390
+ * one answer can honestly describe — the same reason the FIRST git command decides below. Both are
391
+ * read from the parse, so a `cd` or a `git` inside a quoted argument or a heredoc body is neither.
341
392
  *
342
393
  * A path the SHELL would have expanded — `~`, a variable — is quoted here and so reaches git
343
394
  * literally, which makes the read fail rather than lie. Every caller treats a failed read as "not
344
395
  * a repo, git owns that error" and passes: a false negative, which is this pack's stated trade.
345
396
  */
346
- export declare function gitDirPrefix(command: string): string;
397
+ export declare function gitDirPrefix(facts: CommandFacts): string;
347
398
  /**
348
399
  * Does the message carry the token, on a LINE OF ITS OWN?
349
400
  *
@@ -598,7 +649,7 @@ export interface Layer {
598
649
  readonly not?: readonly string[];
599
650
  }
600
651
  export interface Dialect {
601
- /** What to cruise. A GLOB, because a bare directory makes depcruise cruise zero modules. */
652
+ /** What to cruise: a directory or a glob. `src` and `src/**` cruise the same modules. */
602
653
  readonly scan: string;
603
654
  /** Each layer, as globs — a bare list, or `{ path, not }` when it has holes in it. */
604
655
  readonly layers: Readonly<Record<string, readonly string[] | Layer>>;
@@ -610,8 +661,6 @@ export interface Dialect {
610
661
  readonly require?: readonly RequireEdge[];
611
662
  /** Carve-outs — `type-only` above all, since a type import is erased at compile time. */
612
663
  readonly except?: readonly string[];
613
- /** Informational rules that never block. `sdp` = the Stable Dependencies Principle. */
614
- readonly warn?: readonly string[];
615
664
  /** dependency-cruiser's own options, passed through. */
616
665
  readonly options?: Readonly<Record<string, unknown>>;
617
666
  }
@@ -698,6 +747,15 @@ export declare function nonGlobLayers(layers: Readonly<Record<string, readonly s
698
747
  * at load: use plain globs for a layer that an `only` names, or express the exclusion as a `forbid`.
699
748
  */
700
749
  export declare function holesInAnOnlyList(opts: Dialect): string[];
750
+ /**
751
+ * A `warn` option — refused, because it is gone and its rule would silently go with it.
752
+ *
753
+ * `warn: ["sdp"]` compiled a Stable Dependencies rule that nothing bound, and it is removed with no
754
+ * replacement. The type no longer has the key, so a literal config fails to compile; but options
755
+ * built in a variable carry it past the compiler, and dropped quietly the rule would report armed
756
+ * and fence nothing. It is read off the raw object because the type cannot name it.
757
+ */
758
+ export declare function removedWarnOption(opts: Dialect): string[];
701
759
  /**
702
760
  * The generic import-fence runner.
703
761
  *
@@ -790,6 +848,10 @@ export declare function readCase(c: Case): {
790
848
  readonly dialect: CaseDialect;
791
849
  readonly facts: Partial<Ctx>;
792
850
  readonly world: CaseWorld;
851
+ /** commit: the staged paths the last commit does not have, when the case says. */
852
+ readonly added?: readonly string[] | undefined;
853
+ /** commit: the staged deletions, by path, with their content as the last commit has it. */
854
+ readonly deleted?: Readonly<Record<string, string>> | undefined;
793
855
  };
794
856
  /** Which moment's dialect a case speaks. */
795
857
  export declare function caseMoment(c: Case): Moment;
@@ -1,4 +1,4 @@
1
- import { inScope, type Scope, type BreadcrumbMoment, type CaseWorld, type Category, type EntrySpec, type GuardrailMoment, type LoadResult, type LoadedEntry, type Moment, type SessionFacts, type Settings, type TurnAction, type Unanswered, type World } from "../language/domain.ts";
1
+ import { inScope, type Scope, type BreadcrumbMoment, type CaseWorld, type Category, type Ctx, type EntrySpec, type GuardrailMoment, type LoadResult, type LoadedEntry, type Moment, type SessionFacts, type Settings, type TurnAction, type Unanswered, type World } from "../language/domain.ts";
2
2
  /**
3
3
  * How a category recognises its members from host-written evidence.
4
4
  *
@@ -9,7 +9,7 @@ import { inScope, type Scope, type BreadcrumbMoment, type CaseWorld, type Catego
9
9
  */
10
10
  export interface SpawnRecipe {
11
11
  /**
12
- * Sidecar agent types that ARE this category, decisively. Brief text is never consulted for a
12
+ * Host-sent agent types that ARE this category, decisively. Brief text is never consulted for a
13
13
  * session whose type is named here — nor for one whose type is named anywhere but `generic`.
14
14
  */
15
15
  readonly types?: readonly string[];
@@ -21,7 +21,7 @@ export interface SpawnRecipe {
21
21
  /** Substrings that recover this category from a generic spawn's brief. */
22
22
  readonly brief?: readonly string[];
23
23
  /**
24
- * True for the top-level session only — no sidecar, no agent type. Structural, and exclusive: a
24
+ * True for the top-level session only — not spawned, no agent type. Structural, and exclusive: a
25
25
  * recipe is either about spawns or about the parent, never both.
26
26
  */
27
27
  readonly parent?: boolean;
@@ -64,21 +64,33 @@ export declare function categoriesIn(entries: readonly LoadedEntry[]): Category[
64
64
  * accident — a transcript since compacted, a brief scrolled away — and a rule that fired for the
65
65
  * first half of a session and not the second is worse than either outcome alone. The unit is
66
66
  * session × agent because that is what the evidence is about: each subagent has its own transcript
67
- * and its own sidecar.
67
+ * and its own agent id.
68
68
  */
69
69
  export declare function identify(stored: readonly string[] | undefined, facts: SessionFacts, categories: readonly Category[]): Identity;
70
70
  /** One event the guardrail rails carry. Facts only — who it happened to is `wearing`. */
71
71
  export interface GuardEvent {
72
72
  readonly moment: GuardrailMoment;
73
73
  /** write/delete: the would-be file, in memory, before disk. */
74
- readonly file?: {
75
- readonly path: string;
76
- readonly content: string;
77
- } | undefined;
74
+ readonly file?: Ctx["file"];
78
75
  /** command: the line about to run. */
79
76
  readonly command?: string | undefined;
77
+ /** command: the line's parse, when the caller already made it. Absent, the engine parses the line. */
78
+ readonly commands?: Ctx["commands"];
80
79
  /** commit: the staged paths. */
81
80
  readonly staged?: readonly string[] | undefined;
81
+ /**
82
+ * commit: the staged paths the last commit does not have, which become each file's `existed`.
83
+ * Absent when the shell could not ask git, and then `existed` is left unset.
84
+ */
85
+ readonly added?: readonly string[] | undefined;
86
+ /**
87
+ * commit: the staged deletions an entry will judge (`judgedDeletions`), each with its content as
88
+ * the last commit has it. A rename is a deletion of its old path plus an add of its new one.
89
+ */
90
+ readonly deleted?: readonly {
91
+ readonly path: string;
92
+ readonly content: string;
93
+ }[] | undefined;
82
94
  /** turn-end: what the actor did this turn, in order. */
83
95
  readonly turn?: readonly TurnAction[] | undefined;
84
96
  /** Every category this session × agent wears. Empty means it wears none, not "all of them". */
@@ -123,6 +135,12 @@ export declare function covers(scope: Scope, path: string): boolean;
123
135
  * guarded on one page and abandoned on the next.
124
136
  */
125
137
  export declare function watching(entries: readonly Bound[]): Bound[];
138
+ /**
139
+ * The staged deletions some entry will judge — the ones whose content is worth reading from the
140
+ * last commit. A commit that removes a vendored folder is thousands of deletions, and none of them
141
+ * costs a read unless a rule is about it.
142
+ */
143
+ export declare function judgedDeletions(entries: readonly LoadedEntry[], deleted: readonly string[]): string[];
126
144
  /** A rail refusing. `entry` is null when it is the guard itself that is broken, not a rule. */
127
145
  export interface Block {
128
146
  readonly do: "block";
@@ -146,6 +164,12 @@ export interface Notice {
146
164
  /** The file the prose lives in, when it did not. The shell resolves it; a pure home cannot. */
147
165
  readonly file: string | undefined;
148
166
  readonly cause: Cause;
167
+ /**
168
+ * What the event that showed it was about: the repo-relative path, or the command line, as a
169
+ * guardrail row's subject is. Absent when the event carries no file or command, such as a
170
+ * session note or a fault.
171
+ */
172
+ readonly subject: string | undefined;
149
173
  }
150
174
  export type Effect = Block | Notice;
151
175
  /**
@@ -266,8 +290,16 @@ export declare const FLOW_DIR = ".flow";
266
290
  export declare const FLOW_GITIGNORE = "# flow's own state \u2014 telemetry and session marks, never committed.\n*\n";
267
291
  /** The log schema this build writes, stamped into every session file's first row. */
268
292
  export declare const LOG_VERSION = 1;
269
- /** Every kind a log row may be. Closed — a reader that meets another word has met corruption. */
270
- export declare const ROW_KINDS: readonly ["meta", "tool", "breadcrumb", "guardrail", "compaction", "run"];
293
+ /**
294
+ * Every kind a log row may be. Closed — a reader that meets another word has met corruption.
295
+ *
296
+ * `read` and `write` are what went ROUND the tool rows: a file a shell command read by name, and a
297
+ * file a call changed that the call did not name — a heredoc, a `sed -i`, a script. A `tool` row
298
+ * says a call happened; these say what it did to the tree, so the coverage and lead numbers count
299
+ * work done through the shell as well as through Edit. `mismatch` is the host's own list of the
300
+ * files a command changed disagreeing with flow's diff: a fact to read, never an input to a rule.
301
+ */
302
+ export declare const ROW_KINDS: readonly ["meta", "tool", "breadcrumb", "guardrail", "compaction", "run", "read", "write", "mismatch"];
271
303
  export type RowKind = (typeof ROW_KINDS)[number];
272
304
  export declare function flowDir(root: string): string;
273
305
  /** A session id becomes a filename, so it is held to a filename's charset. */
@@ -351,15 +383,42 @@ export interface SessionMarker {
351
383
  * delete. Absent on a marker written before this field existed, which reads as "unknown actor".
352
384
  */
353
385
  readonly agent?: unknown;
386
+ /** The type that agent was started as, for the commit gate's log rows. Absent on `main`. */
387
+ readonly agentType?: unknown;
354
388
  readonly ts?: unknown;
355
389
  }
390
+ /**
391
+ * The agent one row, marker or call came from: the key its state is stored under, and its type.
392
+ *
393
+ * One value rather than two loose ones, because the type means nothing without the id it belongs to.
394
+ */
395
+ export interface AgentOf {
396
+ /** `main` for the session itself, a subagent's `agent_id`, or `commit`: see `UNNAMED_COMMIT`. */
397
+ readonly agent: string;
398
+ /** The type the subagent was started as, when the host said. Never beside `main` or `commit`. */
399
+ readonly agentType?: string;
400
+ }
401
+ /**
402
+ * The agent a commit-gate run's rows name when no agent can be named: no marker, a stale one, or
403
+ * one written before markers held the agent. `commit` is also the stream such a commit's rows land
404
+ * in when no session claims it.
405
+ */
406
+ export declare const UNNAMED_COMMIT: AgentOf;
407
+ /**
408
+ * An agent named by an id and, maybe, a type — the one reading of the pair, wherever it is read
409
+ * from: a hook message's `agent_id` and `agent_type`, or a marker's `agent` and `agentType`.
410
+ *
411
+ * Null when the id is not a non-empty string, and the type counts only beside such an id, because a
412
+ * type means nothing without the agent it belongs to.
413
+ */
414
+ export declare function namedAgent(id: unknown, type: unknown): AgentOf | null;
356
415
  /** A commit long after the last edit is not this session's. */
357
416
  export declare const MARKER_MAX_AGE_MS: number;
358
417
  /** Who a commit-gate run belongs to: the live session, and which of its agents was working. */
359
418
  export interface Attribution {
360
419
  readonly session: string;
361
420
  /** Null when the marker named no agent — an old marker, or a run with nobody to attribute to. */
362
- readonly agent: string | null;
421
+ readonly who: AgentOf | null;
363
422
  }
364
423
  /**
365
424
  * Which session × agent a commit-gate run belongs to, given the marker it found.
@@ -639,12 +698,11 @@ export declare const RECORDING_VERSION = 1;
639
698
  export type RecordedStep = {
640
699
  readonly rail: "guard";
641
700
  readonly moment: GuardrailMoment;
642
- readonly file?: {
643
- readonly path: string;
644
- readonly content: string;
645
- } | undefined;
701
+ readonly file?: Ctx["file"];
646
702
  readonly command?: string | undefined;
647
703
  readonly staged?: readonly string[] | undefined;
704
+ readonly added?: readonly string[] | undefined;
705
+ readonly deleted?: GuardEvent["deleted"];
648
706
  readonly turn?: readonly TurnAction[] | undefined;
649
707
  readonly wearing: readonly string[];
650
708
  readonly world?: CaseWorld | undefined;
@@ -728,8 +786,8 @@ export interface RowDiff {
728
786
  * Only the two MECHANISM rows are compared, and that is not a softening: a replay has no tool
729
787
  * calls to record and no session to stamp a meta row for, so `tool`, `run` and `meta` are facts
730
788
  * about the live process rather than about the guard's judgement. What the recording claims is
731
- * that the same rules refuse the same subjects and the same notes show for the same causes, and
732
- * those are exactly the rows named here.
789
+ * that the same rules refuse the same subjects and the same notes show for the same causes and
790
+ * files, and those are exactly the rows named here.
733
791
  */
734
792
  export declare function diffRows(replayed: readonly Row[], live: readonly Row[]): RowDiff[];
735
793
  /**
@@ -1,5 +1,5 @@
1
1
  import type { Category, SessionFacts } from "../language/domain.ts";
2
- import { type Attribution, type Identity, type RecordedStep, type Recording, type Row, type SessionRows, type SessionState } from "./domain.ts";
2
+ import { type AgentOf, type Attribution, type Identity, type RecordedStep, type Recording, type Row, type SessionRows, type SessionState } from "./domain.ts";
3
3
  /**
4
4
  * Create `.flow/`, self-ignored, and answer whether it is there.
5
5
  *
@@ -35,11 +35,14 @@ export interface LogContext {
35
35
  readonly session: string;
36
36
  /** The branch this worktree has out, for the stream's meta row. The adapter knows it; this does not. */
37
37
  readonly branch: string | null;
38
+ /** The agent every row came from, stamped beside the time. */
39
+ readonly who: AgentOf;
38
40
  /** Injected, because a clock inside a decision is a decision nobody can test. */
39
41
  readonly now?: () => Date;
40
42
  }
41
43
  /**
42
- * Append rows to this session's stream, stamping each with the time and the next event index.
44
+ * Append rows to this session's stream, stamping each with the time, the next event index and the
45
+ * agent it came from.
43
46
  *
44
47
  * The FIRST write creates the file with its meta row, which is the fail-loud "armed" marker — a
45
48
  * present-but-quiet log and a missing one are different facts, and a reader that cannot tell them
@@ -99,6 +102,6 @@ export declare function loadRecording(file: string): Recording;
99
102
  * marker tracks the session's latest activity and its timestamp is what `attribution` judges
100
103
  * freshness by.
101
104
  */
102
- export declare function writeMarker(root: string, session: string, agent: string, branch: string | null, at?: Date): boolean;
103
- /** Who this worktree's commit belongs to — the fresh marker's session × agent, or `commit`. */
105
+ export declare function writeMarker(root: string, session: string, who: AgentOf, branch: string | null, at?: Date): boolean;
106
+ /** Who this worktree's commit belongs to — the fresh marker's session × agent and its type, or `commit`. */
104
107
  export declare function commitAttribution(root: string, branch: string | null, nowMs?: number): Attribution;
@@ -1,10 +1,11 @@
1
1
  export { guardrail, breadcrumb, type Sentence, type EntrySpec, type GuardrailSpec, type BreadcrumbSpec, type GuardrailSentence, type BreadcrumbSentence, type EntryGroup, type DisabledMark, } from "./language/domain.ts";
2
2
  export { definePack, pack, override, defineConfig, type Pack, type PackDefinition, type EntryRef, type RefTarget, type FlowConfig, type Binding, type PackBinding, type OverrideBinding, type ConfigSentence, } from "./language/domain.ts";
3
- export { defineCheck, withFaults, faultsOf, type Check, type Ctx, type World, type Verdict, type ExecResult, type TurnAction, type Case, type CaseWorld, type Cases, } from "./language/domain.ts";
3
+ export { defineCheck, withFaults, faultsOf, type Check, type Ctx, type World, type Verdict, type ExecResult, type TurnAction, type ShellCommand, type ShellHeredoc, type ShellRedirect, type ShellSubstitution, type Case, type CaseWorld, type Cases, } from "./language/domain.ts";
4
4
  export { defineCategory, type Category, type Classifier, type SessionFacts } from "./language/domain.ts";
5
- export { astGrep, banCommands, canonicalFiles, changeTogether, commitReason, depcruise, execPasses, jsonInvariant, protectedPath, ranSinceEdit, siblingExists, symbolsInSibling, textBan, type CanonOptions, type ChangeGroup, type Dialect, type Layer, type DiffAdds, type ForbidEdge, type JsonAssert, type RequireEdge, } from "./checks/domain.ts";
5
+ export { astGrep, banCommands, canonicalFiles, changeTogether, commitReason, depcruise, execPasses, jsonInvariant, oneWriter, protectedPath, ranSinceEdit, siblingExists, symbolsInSibling, textBan, type CanonOptions, type ChangeGroup, type Dialect, type Layer, type DiffAdds, type ForbidEdge, type JsonAssert, type RequireEdge, } from "./checks/domain.ts";
6
6
  export { substitutionInProse, SUBSTITUTION_MESSAGE } from "./checks/domain.ts";
7
- export { commitMessage, gitDirPrefix, gitInvocations, givesReason, quoteArg, tokenizeCommand, type GitInvocation, } from "./checks/domain.ts";
7
+ export { programName } from "./language/domain.ts";
8
+ export { commitMessage, gitCommand, gitCommands, gitDirPrefix, givesReason, quoteArg, shellCommands, type GitCommand, } from "./checks/domain.ts";
8
9
  export { spawnedAs, type SpawnRecipe } from "./engine/domain.ts";
9
10
  export { write, command, commit, deletion, turnEnd, session, touch, type Moment, type GuardrailMoment, type BreadcrumbMoment, } from "./language/domain.ts";
10
11
  export { loadConfig, refusalText, REFUSAL_CODES, type Refusal, type RefusalCode, type LoadResult, type LoadedEntry, type Settings, } from "./language/domain.ts";