@jawache/flow 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,57 +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`.
36
37
  *
37
- * WITHOUT ITS HEREDOC BODIES (`elideHeredocs`), by default. A heredoc is text fed to a program,
38
- * and a python script or a note that names a banned command is not a use of it: matched whole,
39
- * `python3 - <<'PY'` scripts were refused for naming a recipe in a string. Only the bodies go —
40
- * the newlines the patterns above span are kept — and a body a shell reads stays, because it is
41
- * commands.
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
42
  *
43
- * `matchHeredocs: true` matches the whole command instead. A rule about what a command is FED —
44
- * the text of a commit message given on stdin, `git commit -F - <<'EOF'` — has to see the body,
45
- * and would be blind to that message without it.
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.
46
53
  */
47
54
  export declare const banCommands: (options: {
48
55
  ban: readonly string[];
49
- matchHeredocs?: boolean;
56
+ wholeLine?: boolean;
50
57
  }) => Check;
51
58
  /**
52
- * A `banCommands` pattern for: a command that writes permanent prose, with a BACKTICK inside a
53
- * double-quoted argument.
54
- *
55
- * Reads as: <one of `heads`> … <an OPEN double quote> … <a backtick>, where the quote-pair walk
56
- * skips arguments that are already closed, and both scans refuse to cross a QUOTED heredoc opener
57
- * — inside one the shell substitutes nothing, so it is the sanctioned way to carry backticks and
58
- * must not be blocked. An unquoted `<<EOF` is deliberately still caught: there, substitution
59
- * really does happen. This is the one pattern that has to span a newline, and the reason
60
- * `banCommands` matches a command whole rather than line by line.
61
- *
62
- * A BUILDER over a `heads` list rather than one frozen expression, because the commands that
63
- * record prose belong to different packs: `git commit` and `gh pr|issue` are `git`'s, which every
64
- * repo binds, and the `work …` verbs are `work`'s, which only a repo running the work lifecycle
65
- * binds. One tail, two heads, and neither pack carries a copy of the other's — a second copy of
66
- * this expression is how the two would drift while both still read as armed.
67
- *
68
- * WHY IT IS CORE and not a helper beside the packs: TWO PACKS
69
- * SHARE IT, and that is the whole rule the fold settled — anything two packs share is a core
70
- * check. A shared file sitting beside the packs is a library with two users and no owner, and this
71
- * one proved it by growing four unrelated sections around this pair. Here it sits next to the
72
- * check it builds a pattern for, and the door that exports one exports both.
73
- */
74
- 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;
75
88
  /**
76
89
  * What a blocked prose-writing command is told, and it is the same sentence whichever pack caught
77
- * 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.
78
91
  *
79
92
  * ONE constant rather than a message per pack. The sentence never named the verb that was blocked
80
93
  * — it names the shape — so two copies would be two places for the same advice to drift, and the
@@ -226,7 +239,12 @@ export declare const jsonInvariant: (options: {
226
239
  /** Of some exported names, which are never mentioned in the sibling test's text? */
227
240
  export declare function unreferenced(names: readonly string[], siblingText: string): string[];
228
241
  /**
229
- * 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.
230
248
  *
231
249
  * A real parser rather than a regex, because `export const x = …` inside a string or a comment is
232
250
  * exactly the false positive that teaches people to ignore a rule. Parsing is not reaching the
@@ -235,7 +253,11 @@ export declare function unreferenced(names: readonly string[], siblingText: stri
235
253
  * milliseconds to load and no other check needs it.
236
254
  */
237
255
  export declare function exportedNames(content: string): Promise<string[]>;
238
- /** 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
+ */
239
261
  export declare const symbolsInSibling: (options: {
240
262
  sibling: string;
241
263
  }) => Check;
@@ -270,69 +292,35 @@ export declare const changeTogether: (options: {
270
292
  * aimed at a directory called HEAD.
271
293
  */
272
294
  export declare const GIT_VALUE_OPTS: ReadonlySet<string>;
273
- /** A heredoc's opener: its delimiter, whether `<<-` strips leading tabs, and where the opener ends. */
274
- export interface HeredocOpen {
275
- readonly delim: string;
276
- readonly tabs: boolean;
277
- readonly end: number;
278
- }
279
- /**
280
- * The first heredoc opener anywhere in a line, quoted context or not — the one definition of "this
281
- * line carries a heredoc". A here-string (`<<<`) is stepped over whole, so its tail is not read as
282
- * one.
283
- */
284
- export declare function firstHeredoc(s: string): HeredocOpen | null;
295
+ /** What a command reader is handed: the line, and its parse when the engine made one. */
296
+ type CommandFacts = Pick<Ctx, "command" | "commands">;
285
297
  /**
286
- * Where a `$(` closes, honouring quotes, nesting and any heredoc inside it — a body is text, so an
287
- * apostrophe or a `)` in it closes nothing. -1 when it never closes.
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".
288
301
  */
289
- export declare function closeParen(s: string, from: number): number;
290
- /** Where a double-quoted run closes — a `"` inside a `$(…)` in it does not close it. -1 when it never does. */
291
- export declare function dquoteEnd(s: string, from: number): number;
292
- /**
293
- * THE COMMAND LINE WITHOUT ITS HEREDOC BODIES — what a command ban matches unless the rule sets
294
- * `matchHeredocs`.
295
- *
296
- * A heredoc is text fed to a program on its standard input: a python script, a file being written,
297
- * a note. The words of a banned command inside it are not a use of that command, and matching
298
- * them refused `python3 - <<'PY'` scripts for naming a recipe in a string. Every body is removed
299
- * (its terminator line stays, so the opener still ends), whether the delimiter is quoted or not,
300
- * and wherever the heredoc sits —
301
- * inside a `$(…)` or a quoted argument too. EVERYTHING ELSE IS LEFT AS WRITTEN — newlines
302
- * included, because the patterns that police a commit message span them.
303
- *
304
- * One exception keeps a body: a heredoc fed to a SHELL (`bash`, `sh`, `zsh`, `eval`, or a pipeline
305
- * into one), whose body is commands, and a banned one in it will run.
306
- *
307
- * Best-effort, and it errs toward matching: from a word whose quote never closes, the line is
308
- * returned as written, so a ban still sees everything it saw before.
309
- */
310
- export declare function elideHeredocs(command: string): string;
311
- /**
312
- * A shell-ish tokeniser: quotes respected (so a commit message mentioning `git commit` is not a
313
- * second command), operators emitted as their own tokens.
314
- *
315
- * Null on unparseable input — an unclosed quote — and null means "cannot tell", which every caller
316
- * must read as "do not judge". A guard that guesses at a line it cannot parse is worse than one
317
- * that stands aside.
318
- */
319
- export declare function tokenizeCommand(command: string): string[] | null;
320
- /** One `git …` in a command line, split where git stops and its subcommand starts. */
321
- 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 {
322
305
  /** git's OWN options, before the subcommand — where `-C` counts and only there. */
323
306
  readonly globals: readonly string[];
324
- /** 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. */
325
308
  readonly subcommand: string | null;
326
309
  /** Everything after the subcommand — its own flags and arguments. */
327
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;
328
315
  }
329
316
  /**
330
- * Every git invocation in a command line, in order. Null when the line cannot be parsed at all.
331
- *
332
- * A token only starts an invocation at a COMMAND position — the start of the line or just after an
333
- * operator — so a `git` inside a quoted message or as an argument to something else is not one.
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.
334
320
  */
335
- export declare function gitInvocations(command: string): GitInvocation[] | 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[];
336
324
  /**
337
325
  * The message a `git commit` in this command line carries, or null when there is no judgeable
338
326
  * commit in it at all. Null is the "not our business" answer and every caller passes on it.
@@ -350,7 +338,12 @@ export declare function gitInvocations(command: string): GitInvocation[] | null;
350
338
  *
351
339
  * git commit -m MSG · -mMSG · --message MSG · --message=MSG judged
352
340
  * git commit -m SUBJECT -m BODY (repeated, any mix of forms) judged, joined as git joins them
353
- * git commit -F - <<'EOF' … EOF (the commit reads stdin) 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
354
347
  * python3 - <<'PY' … PY; git commit -F msg.txt NOT judged — the heredoc is
355
348
  * another command's script, and
356
349
  * the message is on disk
@@ -364,7 +357,7 @@ export declare function gitInvocations(command: string): GitInvocation[] | null;
364
357
  * The boundary is on the record: this rail polices AGENT-typed commits, which are its audience. A
365
358
  * human committing from an editor is not its subject and never was.
366
359
  */
367
- export declare function commitMessage(command: string): string | null;
360
+ export declare function commitMessage(facts: CommandFacts): string | null;
368
361
  /** One shell argument, single-quoted so nothing in it can be expanded or re-parsed. */
369
362
  export declare function quoteArg(value: string): string;
370
363
  /**
@@ -392,15 +385,16 @@ export declare function quoteArg(value: string): string;
392
385
  * options exactly as a shell chains directory changes: `cd /a && git -C b status` runs in `/a/b`,
393
386
  * and `-C '/a' -C 'b'` says precisely that. Nothing has to decide which of the two wins.
394
387
  *
395
- * ONLY A LEADING `cd`, and only when an operator follows its path. A `cd` in the middle of a chain
396
- * can be preceded by another that already moved, and a command line that walks two trees is beyond
397
- * 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.
398
392
  *
399
393
  * A path the SHELL would have expanded — `~`, a variable — is quoted here and so reaches git
400
394
  * literally, which makes the read fail rather than lie. Every caller treats a failed read as "not
401
395
  * a repo, git owns that error" and passes: a false negative, which is this pack's stated trade.
402
396
  */
403
- export declare function gitDirPrefix(command: string): string;
397
+ export declare function gitDirPrefix(facts: CommandFacts): string;
404
398
  /**
405
399
  * Does the message carry the token, on a LINE OF ITS OWN?
406
400
  *
@@ -655,7 +649,7 @@ export interface Layer {
655
649
  readonly not?: readonly string[];
656
650
  }
657
651
  export interface Dialect {
658
- /** 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. */
659
653
  readonly scan: string;
660
654
  /** Each layer, as globs — a bare list, or `{ path, not }` when it has holes in it. */
661
655
  readonly layers: Readonly<Record<string, readonly string[] | Layer>>;
@@ -667,8 +661,6 @@ export interface Dialect {
667
661
  readonly require?: readonly RequireEdge[];
668
662
  /** Carve-outs — `type-only` above all, since a type import is erased at compile time. */
669
663
  readonly except?: readonly string[];
670
- /** Informational rules that never block. `sdp` = the Stable Dependencies Principle. */
671
- readonly warn?: readonly string[];
672
664
  /** dependency-cruiser's own options, passed through. */
673
665
  readonly options?: Readonly<Record<string, unknown>>;
674
666
  }
@@ -755,6 +747,15 @@ export declare function nonGlobLayers(layers: Readonly<Record<string, readonly s
755
747
  * at load: use plain globs for a layer that an `only` names, or express the exclusion as a `forbid`.
756
748
  */
757
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[];
758
759
  /**
759
760
  * The generic import-fence runner.
760
761
  *
@@ -847,6 +848,10 @@ export declare function readCase(c: Case): {
847
848
  readonly dialect: CaseDialect;
848
849
  readonly facts: Partial<Ctx>;
849
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;
850
855
  };
851
856
  /** Which moment's dialect a case speaks. */
852
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
  /**
@@ -359,15 +383,42 @@ export interface SessionMarker {
359
383
  * delete. Absent on a marker written before this field existed, which reads as "unknown actor".
360
384
  */
361
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;
362
388
  readonly ts?: unknown;
363
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;
364
415
  /** A commit long after the last edit is not this session's. */
365
416
  export declare const MARKER_MAX_AGE_MS: number;
366
417
  /** Who a commit-gate run belongs to: the live session, and which of its agents was working. */
367
418
  export interface Attribution {
368
419
  readonly session: string;
369
420
  /** Null when the marker named no agent — an old marker, or a run with nobody to attribute to. */
370
- readonly agent: string | null;
421
+ readonly who: AgentOf | null;
371
422
  }
372
423
  /**
373
424
  * Which session × agent a commit-gate run belongs to, given the marker it found.
@@ -647,12 +698,11 @@ export declare const RECORDING_VERSION = 1;
647
698
  export type RecordedStep = {
648
699
  readonly rail: "guard";
649
700
  readonly moment: GuardrailMoment;
650
- readonly file?: {
651
- readonly path: string;
652
- readonly content: string;
653
- } | undefined;
701
+ readonly file?: Ctx["file"];
654
702
  readonly command?: string | undefined;
655
703
  readonly staged?: readonly string[] | undefined;
704
+ readonly added?: readonly string[] | undefined;
705
+ readonly deleted?: GuardEvent["deleted"];
656
706
  readonly turn?: readonly TurnAction[] | undefined;
657
707
  readonly wearing: readonly string[];
658
708
  readonly world?: CaseWorld | undefined;
@@ -736,8 +786,8 @@ export interface RowDiff {
736
786
  * Only the two MECHANISM rows are compared, and that is not a softening: a replay has no tool
737
787
  * calls to record and no session to stamp a meta row for, so `tool`, `run` and `meta` are facts
738
788
  * about the live process rather than about the guard's judgement. What the recording claims is
739
- * that the same rules refuse the same subjects and the same notes show for the same causes, and
740
- * 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.
741
791
  */
742
792
  export declare function diffRows(replayed: readonly Row[], live: readonly Row[]): RowDiff[];
743
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
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";