@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.
@@ -58,17 +58,21 @@ export declare function isScopable(kind: EntrySpec["kind"], m: string): boolean;
58
58
  * The host-written record of one session × agent, as a classifier sees it.
59
59
  *
60
60
  * Every field is something a HARNESS wrote, not something the session said. `agentType` is the
61
- * strongest of them — the sidecar the host writes beside every subagent transcript — and it is
62
- * what a classifier should key on when it exists.
61
+ * strongest of them — the agent type the host puts in the hook message for every call a subagent
62
+ * makes — and it is what a classifier should key on when it exists.
63
63
  */
64
64
  export interface SessionFacts {
65
- /** The head of this session's transcript: the brief it was started with, verbatim. */
65
+ /** The head of this session's own transcript: the brief it was started with, verbatim. */
66
66
  head: string;
67
67
  /** True when this is a spawned subagent rather than the parent session. */
68
68
  subagent: boolean;
69
- /** The host's own agent type for this spawn, from the sidecar it wrote. Absent on a parent. */
69
+ /** The agent type the host started this spawn as, from its hook message. Absent on a parent. */
70
70
  agentType?: string | undefined;
71
- /** The human-readable label the spawning agent gave this subagent. Absent on a parent. */
71
+ /**
72
+ * @deprecated Always undefined: a category that reads it never matches. It came from the sidecar
73
+ * file, which is no longer read, and the hook message carries no description. Kept so a config
74
+ * that names it still compiles.
75
+ */
72
76
  description?: string | undefined;
73
77
  }
74
78
  /** A category's recognizer: host-written facts in, a yes or no out. */
@@ -130,17 +134,37 @@ export type TurnAction = {
130
134
  */
131
135
  export interface Ctx {
132
136
  readonly moment: Moment;
133
- /** write/touch: the would-be file. */
137
+ /**
138
+ * write/touch: the would-be file.
139
+ *
140
+ * `existed` says whether a file was at `path` before this change, and it is set only where the
141
+ * tree can no longer say so. A shell command's change is judged AFTER it has landed, when
142
+ * `ctx.fs.exists(path)` answers yes to every file it created; at commit every staged file is on
143
+ * disk, and "before" is the last commit. Absent, ask `ctx.fs`: at write the tree is still the
144
+ * one from before the change.
145
+ */
134
146
  readonly file?: {
135
147
  readonly path: string;
136
148
  readonly content: string;
149
+ readonly existed?: boolean | undefined;
137
150
  } | undefined;
138
151
  /** command: the line about to run. */
139
152
  readonly command?: string | undefined;
153
+ /**
154
+ * command: that line parsed as bash — one entry per simple command, nested ones included, in
155
+ * the order they appear. `null` when the line does not parse: read it as "cannot tell".
156
+ */
157
+ readonly commands?: readonly ShellCommand[] | null | undefined;
140
158
  /** commit: the staged paths. */
141
159
  readonly staged?: readonly string[] | undefined;
142
160
  /** turn-end: what the actor did this turn, in order. */
143
161
  readonly turn?: readonly TurnAction[] | undefined;
162
+ /**
163
+ * Every moment: the category names that match the session making this change — the categories
164
+ * some bound entry names, whose recognizer said yes to the host-written evidence about it. Empty
165
+ * when it matches none, which is not "every category".
166
+ */
167
+ readonly actor: readonly string[];
144
168
  exec(cmd: string): Promise<ExecResult>;
145
169
  readonly fs: {
146
170
  read(path: string): Promise<string>;
@@ -160,7 +184,7 @@ export type Check = (ctx: Ctx) => Verdict | Promise<Verdict>;
160
184
  * The two verdict constructors — INTERNAL to flow, and deliberately not part of its public surface.
161
185
  *
162
186
  * A check has exactly one way to answer and it is `ctx.ok()` / `ctx.fail(detail)`. That is the
163
- * spelling the fourteen core checks are written in, the spelling a repo's own check is written in,
187
+ * spelling the fifteen core checks are written in, the spelling a repo's own check is written in,
164
188
  * and the only one flow/index.ts exports. These exist because something has to BUILD the ctx that
165
189
  * carries them — the canned one a `.test()` case runs through, and the live one the adapter makes
166
190
  * — and that is flow's own job, not a config author's. Exporting both would put two spellings of
@@ -177,6 +201,140 @@ export declare const verdict: {
177
201
  * no way to grow a fourth capability that a `.test()` case has no means of recording.
178
202
  */
179
203
  export type World = Pick<Ctx, "exec" | "fs" | "git">;
204
+ /** One heredoc a command is fed. `quoted` is a delimiter in quotes or after a backslash. */
205
+ export interface ShellHeredoc {
206
+ readonly body: string;
207
+ readonly quoted: boolean;
208
+ }
209
+ /** One `$( … )` or backtick substitution the shell runs: the command inside, and which spelling. */
210
+ export interface ShellSubstitution {
211
+ readonly text: string;
212
+ readonly backtick: boolean;
213
+ /** The commands it runs, nested ones included, as `ctx.commands` lists them. */
214
+ readonly commands: readonly ShellCommand[];
215
+ /**
216
+ * The index in its command's `args` of the argument this substitution ends, when only plain text
217
+ * comes before it there — `-m "$( … )"`, `-m"$( … )"`, `--message="$( … )"` — or null.
218
+ */
219
+ readonly argument: number | null;
220
+ /** That plain text: `""` for an argument that is the substitution alone, `-m` for `-m"$( … )"`. Null with `argument`. */
221
+ readonly prefix: string | null;
222
+ }
223
+ /** One redirect on a command: its operator (`>`, `>>`, `<`, `2>&1`'s `>&` …) and its target word. */
224
+ export interface ShellRedirect {
225
+ readonly operator: string;
226
+ readonly target: string;
227
+ }
228
+ /** One simple command in a parsed line — the fields docs/user/reference/ctx.md promises. */
229
+ export interface ShellCommand {
230
+ /** The program that runs, after any `FOO=1` assignment and any wrapper. */
231
+ readonly program: string;
232
+ /** Its arguments as the program receives them, quotes removed. */
233
+ readonly args: readonly string[];
234
+ /** `program` and `args` joined by single spaces. */
235
+ readonly text: string;
236
+ readonly heredocs: readonly ShellHeredoc[];
237
+ /** What the shell runs while building this command's arguments and unquoted heredocs. */
238
+ readonly substitutions: readonly ShellSubstitution[];
239
+ /** Its redirects, and those of every group or loop it sits in. A heredoc or here-string is not one. */
240
+ readonly redirects: readonly ShellRedirect[];
241
+ /** Inside a substitution, a `<( … )`, a `bash -c` or `eval` string, or a heredoc or here-string fed to a shell. */
242
+ readonly nested: boolean;
243
+ }
244
+ /**
245
+ * One piece of a word as the line writes it: text the shell passes as is (quoted, escaped or plain),
246
+ * a variable it expands (`$NAME`, `${NAME}`), a leading unquoted `~`, or something only running
247
+ * the line could tell — a substitution, a special parameter, a `${…}` with an operation in it.
248
+ */
249
+ export type ShellWordPiece = {
250
+ readonly kind: "text";
251
+ readonly text: string;
252
+ } | {
253
+ readonly kind: "variable";
254
+ readonly name: string;
255
+ } | {
256
+ readonly kind: "tilde";
257
+ } | {
258
+ readonly kind: "unknown";
259
+ };
260
+ /** A word as the line writes it, piece by piece, so a reader expands `$F` and leaves `'$F'` alone. */
261
+ export type ShellWord = readonly ShellWordPiece[];
262
+ /**
263
+ * THE SAME PARSE, IN THE ORDER IT RUNS — for a reader that follows the line the way the shell does:
264
+ * where a `cd` leaves it, what a variable the line set holds, what a `for` loop's body is given.
265
+ * Not on ctx, and not public: `ctx.commands` is exactly these steps' commands (`stepCommands`), so
266
+ * the two can never disagree about what a line runs.
267
+ */
268
+ export type ShellStep = {
269
+ readonly kind: "command";
270
+ readonly command: ShellCommand;
271
+ /**
272
+ * Its words as written — wrappers first, no here-string — and where the program starts among
273
+ * them: `words.slice(0, at)` are the wrappers stepped past (`xargs -a list …`), and the rest
274
+ * are the program and the arguments it is given. A here-string's text is stdin, not a word.
275
+ */
276
+ readonly words: readonly ShellWord[];
277
+ readonly at: number;
278
+ /** Whether a `NAME=value` assignment stands in front of it, as its env: `FOO=1 rm`. Not one of its words. */
279
+ readonly assigned: boolean;
280
+ /** `command.redirects` with each target as written, so a reader expands `< $F` and leaves `< '$F'` alone. */
281
+ readonly redirects: readonly StepRedirect[];
282
+ /** The substitutions that build its words, which run before it does. */
283
+ readonly before: readonly ShellStep[];
284
+ }
285
+ /**
286
+ * `NAME=value` on its own, or as an argument of `export`, `declare`, `local`, `readonly` or
287
+ * `typeset`, which sets a shell variable. In front of a command it is that command's env, and not a step.
288
+ */
289
+ | {
290
+ readonly kind: "assign";
291
+ readonly name: string;
292
+ readonly value: ShellWord;
293
+ }
294
+ /** A subshell, a substitution, or a script handed to another shell: a `cd` or a variable set inside stays inside. */
295
+ | {
296
+ readonly kind: "scope";
297
+ readonly body: readonly ShellStep[];
298
+ }
299
+ /** `for name in values; do body; done`: the body runs once per value. `values` is empty when the line lists none. */
300
+ | {
301
+ readonly kind: "for";
302
+ readonly name: string;
303
+ readonly values: readonly ShellWord[];
304
+ readonly body: readonly ShellStep[];
305
+ };
306
+ /**
307
+ * Every command in the steps, nested ones included, in the order they appear in the line: a
308
+ * command before the commands its own substitutions run, which is the order `ctx.commands` keeps.
309
+ */
310
+ export declare function stepCommands(steps: readonly ShellStep[]): ShellCommand[];
311
+ /** A redirect on a step: its operator, and its target as written. A redirect with no target has one unknown piece. */
312
+ export interface StepRedirect {
313
+ readonly operator: string;
314
+ readonly target: ShellWord;
315
+ }
316
+ /** Whether the program, without its path, is a wrapper the parse steps past to the program that runs. */
317
+ export declare function isWrapper(program: string): boolean;
318
+ /**
319
+ * A program's name without its path, the ONE way flow reads it: `/usr/bin/git` and `git` are both
320
+ * `git`. A word ending in `/` names a directory, and gives "".
321
+ */
322
+ export declare function programName(program: string): string;
323
+ /**
324
+ * Parse one shell command line as bash: every simple command in it, nested ones included, in the
325
+ * order they appear — or `null` when the line, or any script nested inside it, does not parse.
326
+ */
327
+ export declare function parseCommandLine(line: string): ShellCommand[] | null;
328
+ /** The same parse as its steps, in the order the shell runs them — or `null` when it does not parse. */
329
+ export declare function parseShellSteps(line: string): readonly ShellStep[] | null;
330
+ /**
331
+ * The line as written, less the body of every heredoc in it — the subject a `banCommands` rule
332
+ * with `wholeLine` matches. The opener and the terminator line stay, and so does everything else,
333
+ * newlines included. A heredoc a shell reads, directly or through a pipeline, holds commands, and
334
+ * its body stays; so does a body inside a string the parse reads again (`bash -c`, `eval`), which
335
+ * is part of that string. `null` when the line does not parse.
336
+ */
337
+ export declare function elideHeredocBodies(line: string): string | null;
180
338
  /**
181
339
  * THE ctx builder — the one place a check's world is assembled, and there must only ever be one.
182
340
  *
@@ -197,7 +355,8 @@ export declare function makeCtx(moment: Moment, facts: Partial<Ctx>, world: Worl
197
355
  * WHY IT IS A VALUE AND NOT A THROW: a check factory runs while the config module is evaluating, so
198
356
  * a throw there is a module that will not import, and the reader gets one fault with a stack trace
199
357
  * instead of every fault with its entry named. Marked instead, the load collects it beside the
200
- * grammar's own refusals and the engine blocks every gated moment with the same text — which is the
358
+ * grammar's own refusals, and the same text answers every gated moment — the engine's `guard`
359
+ * refuses with it, and the live hook rail passes it to the agent with each call — which is the
201
360
  * shape every other load failure already has.
202
361
  *
203
362
  * WHY IT EXISTS AT ALL: an option can be well-TYPED and still be nonsense — the class that bit here
@@ -263,24 +422,35 @@ export type Case =
263
422
  string | {
264
423
  readonly command: string;
265
424
  readonly world?: CaseWorld;
425
+ readonly actor?: readonly string[];
266
426
  } | {
267
427
  readonly path: string;
268
428
  readonly content: string;
429
+ /** The change landed before it was judged, as a shell command's does: `ctx.file.existed`. */
430
+ readonly existed?: boolean;
269
431
  readonly world?: CaseWorld;
432
+ readonly actor?: readonly string[];
270
433
  } | {
271
434
  readonly staged: readonly string[];
435
+ /** The staged paths the last commit does not have. Each file's `ctx.file.existed` says so. */
436
+ readonly added?: readonly string[];
437
+ /**
438
+ * Staged deletions, by path, each with its content as the last commit has it. An entry that
439
+ * fires at `delete` as well as `commit` is asked about each one, as a delete.
440
+ */
441
+ readonly deleted?: Readonly<Record<string, string>>;
272
442
  readonly world?: CaseWorld;
443
+ readonly actor?: readonly string[];
273
444
  } | {
274
445
  readonly actions: readonly TurnAction[];
275
446
  readonly world?: CaseWorld;
447
+ readonly actor?: readonly string[];
276
448
  };
277
449
  /** The cases an entry carries: what must pass it, and what must be blocked by it. */
278
450
  export interface Cases {
279
451
  readonly pass?: readonly Case[];
280
452
  readonly block?: readonly Case[];
281
453
  }
282
- /** Does this cases block prove anything? An entry whose answer is no does not load. */
283
- export declare function hasCases(cases: Cases | undefined): boolean;
284
454
  /** What a recorded world was reached for and could not answer. */
285
455
  export interface Unanswered {
286
456
  readonly kind: "exec" | "read";
@@ -391,6 +561,29 @@ type DeadScope = "flow: every moment this entry fires at carries no path, so .on
391
561
  * the other order, and the load's `dead-scope` refusal is what catches it. Both halves, always.
392
562
  */
393
563
  type Scoping<At extends Moment, Verb, Scopable extends Moment = PathMoment> = [Extract<At, Scopable>] extends [never] ? (dead: DeadScope) => never : Verb;
564
+ /**
565
+ * `.on(…)` / `.ignore(…)` as a call that takes at least one glob — the compile-time half of the
566
+ * empty-scope rule.
567
+ *
568
+ * Not `(glob: string, ...more: string[])`, and that is the whole design: a shipped pack scopes with
569
+ * `.on(...repo.files)`, a list whose length is the repo's to choose, and a spread of `string[]`
570
+ * into a first-required parameter is refused whether the list is empty or not. So the call infers
571
+ * the arguments it was given, and only the arguments KNOWN to be none — a literal `.on()` — land
572
+ * on a required parameter that was not passed: tsc's arity error, whose "an argument for
573
+ * 'atLeastOneGlob' was not provided" note is the advice. A spread list that turns out empty is the
574
+ * load's `empty-scope` refusal to catch. Both halves, always.
575
+ */
576
+ type Globs<Next> = <G extends readonly string[]>(...globs: G extends readonly [] ? [atLeastOneGlob: string] : G) => Next;
577
+ /** What the second form of a breadcrumb's prose becomes once the first was said. The TEXT is the diagnostic. */
578
+ type DoubleProse<Said extends string> = `flow: this breadcrumb already said .${Said}(…) — its note is exactly one of .text(…) and .file(…)`;
579
+ /**
580
+ * `.text(…)` / `.file(…)`, or the refusal once the OTHER form was said — the compile-time half of
581
+ * the double-prose rule. The same form said twice is `Once`'s job, not this one.
582
+ *
583
+ * A wrong-argument refusal rather than a `never`, for the reason `Scoping` gives: the sentence lands
584
+ * in the diagnostic, on the line of the second form.
585
+ */
586
+ type Prose<Spoken extends string, Other extends "text" | "file", Verb> = Other extends Spoken ? (both: DoubleProse<Other>) => never : Verb;
394
587
  /** Anything a pack may file under an entry name. The one shape `definePack` walks. */
395
588
  export interface Sentence<S extends EntrySpec = EntrySpec> {
396
589
  readonly spec: S;
@@ -402,8 +595,8 @@ export interface EntryGroup {
402
595
  export interface GuardrailSentence<Spoken extends GuardrailKey = never, At extends GuardrailMoment = GuardrailMoment> extends Sentence<GuardrailSpec> {
403
596
  readonly at: Once<Spoken, "at", <M extends GuardrailMoment[]>(...moments: M) => GuardrailSentence<Spoken | "at", M[number]>>;
404
597
  readonly for: Once<Spoken, "for", (...categories: Category[]) => GuardrailSentence<Spoken | "for", At>>;
405
- readonly on: Once<Spoken, "on", Scoping<At, (...globs: string[]) => GuardrailSentence<Spoken | "on", At>>>;
406
- readonly ignore: Once<Spoken, "ignore", Scoping<At, (...globs: string[]) => GuardrailSentence<Spoken | "ignore", At>>>;
598
+ readonly on: Once<Spoken, "on", Scoping<At, Globs<GuardrailSentence<Spoken | "on", At>>>>;
599
+ readonly ignore: Once<Spoken, "ignore", Scoping<At, Globs<GuardrailSentence<Spoken | "ignore", At>>>>;
407
600
  readonly check: Once<Spoken, "check", (check: Check) => GuardrailSentence<Spoken | "check", At>>;
408
601
  readonly message: Once<Spoken, "message", (message: string) => GuardrailSentence<Spoken | "message", At>>;
409
602
  readonly disabled: Once<Spoken, "disabled", (reason?: string) => GuardrailSentence<Spoken | "disabled", At>>;
@@ -413,10 +606,10 @@ export interface GuardrailSentence<Spoken extends GuardrailKey = never, At exten
413
606
  export interface BreadcrumbSentence<Spoken extends BreadcrumbKey = never, At extends BreadcrumbMoment = BreadcrumbMoment> extends Sentence<BreadcrumbSpec> {
414
607
  readonly at: Once<Spoken, "at", <M extends BreadcrumbMoment[]>(...moments: M) => BreadcrumbSentence<Spoken | "at", M[number]>>;
415
608
  readonly for: Once<Spoken, "for", (...categories: Category[]) => BreadcrumbSentence<Spoken | "for", At>>;
416
- readonly on: Once<Spoken, "on", Scoping<At, (...globs: string[]) => BreadcrumbSentence<Spoken | "on", At>, BriefScopeMoment>>;
417
- readonly ignore: Once<Spoken, "ignore", Scoping<At, (...globs: string[]) => BreadcrumbSentence<Spoken | "ignore", At>, BriefScopeMoment>>;
418
- readonly text: Once<Spoken, "text", (text: string) => BreadcrumbSentence<Spoken | "text", At>>;
419
- readonly file: Once<Spoken, "file", (path: string) => BreadcrumbSentence<Spoken | "file", At>>;
609
+ readonly on: Once<Spoken, "on", Scoping<At, Globs<BreadcrumbSentence<Spoken | "on", At>>, BriefScopeMoment>>;
610
+ readonly ignore: Once<Spoken, "ignore", Scoping<At, Globs<BreadcrumbSentence<Spoken | "ignore", At>>, BriefScopeMoment>>;
611
+ readonly text: Once<Spoken, "text", Prose<Spoken, "file", (text: string) => BreadcrumbSentence<Spoken | "text", At>>>;
612
+ readonly file: Once<Spoken, "file", Prose<Spoken, "text", (path: string) => BreadcrumbSentence<Spoken | "file", At>>>;
420
613
  readonly disabled: Once<Spoken, "disabled", (reason?: string) => BreadcrumbSentence<Spoken | "disabled", At>>;
421
614
  readonly description: Once<Spoken, "description", (description: string) => BreadcrumbSentence<Spoken | "description", At>>;
422
615
  }
@@ -538,6 +731,19 @@ export interface Settings {
538
731
  * it two ways.
539
732
  */
540
733
  readonly grammars?: readonly Grammar[];
734
+ /**
735
+ * Globs for the IGNORED paths the snapshot records as well. The snapshot is git's view of the
736
+ * tree — tracked files, and untracked ones git does not ignore — so a shell write to an ignored
737
+ * file is never seen unless a glob here brings it back. `.env` is the usual one.
738
+ */
739
+ readonly snapshotInclude?: readonly string[];
740
+ /**
741
+ * What is put back when a rule refuses a write that has already landed: `"refused"` (the default)
742
+ * writes back only the files a rule refused, `"all"` every file that call changed. While another
743
+ * agent's call overlaps it, only the refused files are put back whatever this says — that call's
744
+ * writes can be in the same diff.
745
+ */
746
+ readonly revert?: "refused" | "all";
541
747
  }
542
748
  /**
543
749
  * One hand-built tree-sitter grammar, as `registerDynamicLanguage` wants it.
@@ -598,11 +804,11 @@ interface OverrideChain<Kind extends EntrySpec["kind"], Spoken extends OverrideK
598
804
  readonly binding: OverrideBinding;
599
805
  readonly at: Once<Spoken, "at", (...moments: (Kind extends "guardrail" ? GuardrailMoment : BreadcrumbMoment)[]) => OverrideChain<Kind, Spoken | "at">>;
600
806
  readonly for: Once<Spoken, "for", (...categories: Category[]) => OverrideChain<Kind, Spoken | "for">>;
601
- readonly on: Once<Spoken, "on", (...globs: string[]) => OverrideChain<Kind, Spoken | "on">>;
602
- readonly ignore: Once<Spoken, "ignore", (...globs: string[]) => OverrideChain<Kind, Spoken | "ignore">>;
807
+ readonly on: Once<Spoken, "on", Globs<OverrideChain<Kind, Spoken | "on">>>;
808
+ readonly ignore: Once<Spoken, "ignore", Globs<OverrideChain<Kind, Spoken | "ignore">>>;
603
809
  readonly message: Kind extends "guardrail" ? Once<Spoken, "message", (message: string) => OverrideChain<Kind, Spoken | "message">> : never;
604
- readonly text: Kind extends "breadcrumb" ? Once<Spoken, "text", (text: string) => OverrideChain<Kind, Spoken | "text">> : never;
605
- readonly file: Kind extends "breadcrumb" ? Once<Spoken, "file", (path: string) => OverrideChain<Kind, Spoken | "file">> : never;
810
+ readonly text: Kind extends "breadcrumb" ? Once<Spoken, "text", Prose<Spoken, "file", (text: string) => OverrideChain<Kind, Spoken | "text">>> : never;
811
+ readonly file: Kind extends "breadcrumb" ? Once<Spoken, "file", Prose<Spoken, "text", (path: string) => OverrideChain<Kind, Spoken | "file">>> : never;
606
812
  readonly disabled: Once<Spoken, "disabled", (reason?: string) => OverrideChain<Kind, Spoken | "disabled">>;
607
813
  readonly description: Once<Spoken, "description", (description: string) => OverrideChain<Kind, Spoken | "description">>;
608
814
  }
@@ -649,7 +855,7 @@ export interface Overlaid {
649
855
  */
650
856
  export declare function overlay(base: EntrySpec, spoken: Readonly<Record<string, unknown>>, prior?: Readonly<Record<string, "pack" | "override">>): Overlaid;
651
857
  /** The closed vocabulary of refusals. Every load failure is exactly one of these. */
652
- export declare const REFUSAL_CODES: readonly ["not-a-pack", "missing-parameter", "unknown-entry", "unknown-key", "unknown-moment", "missing-mandatory", "undeclared-category", "no-cases", "duplicate-id", "dead-scope", "bad-check-options"];
858
+ export declare const REFUSAL_CODES: readonly ["not-a-pack", "missing-parameter", "unknown-entry", "unknown-key", "unknown-moment", "missing-mandatory", "undeclared-category", "no-block-cases", "duplicate-id", "dead-scope", "bad-check-options", "empty-scope", "double-prose"];
653
859
  export type RefusalCode = (typeof REFUSAL_CODES)[number];
654
860
  /** One reason the config will not load. */
655
861
  export interface Refusal {
@@ -1,20 +1,9 @@
1
- /**
2
- * Two documentation audiences, one folder, nothing replicated.
3
- *
4
- * Every repo documents for exactly two readers who want opposite things: the end user wants the
5
- * least possible cognitive load, and the agent wants almost nothing in context. So `docs/` has
6
- * exactly two doors and everything inside follows its door's rules.
7
- *
8
- * @setup A `docs/` folder holding exactly `user/` and `agent/`, each with its own index —
9
- * `docs/user/index.html` for people and `docs/agent/README.md` as a list of pointers. The two
10
- * guardrails below refuse anything else at the top of the folder, so a repo with a different shape
11
- * either moves its pages or does not bind this pack.
12
- */
1
+ /** The two facts about a repo's docs folder the pack cannot know: where it is, and what may sit loose in it. */
13
2
  export interface Doors {
14
3
  /**
15
4
  * The documentation folder, as a path from the repo root. Defaults to `docs`.
16
5
  *
17
- * Both guardrails and both breadcrumbs are scoped to it, so a repo whose pages live in
6
+ * Its guardrail and both breadcrumbs are scoped to it, so a repo whose pages live in
18
7
  * `documentation/` names that here and keeps every rule.
19
8
  */
20
9
  readonly root?: string;
@@ -26,6 +15,21 @@ export interface Doors {
26
15
  */
27
16
  readonly allow?: readonly string[];
28
17
  }
18
+ /**
19
+ * Two documentation audiences, one folder, nothing replicated.
20
+ *
21
+ * Every repo documents for exactly two readers who want opposite things: the end user wants the
22
+ * least possible cognitive load, and the agent wants almost nothing in context. So `docs/` has
23
+ * exactly two doors and everything inside follows its door's rules.
24
+ *
25
+ * @setup A `docs/` folder holding exactly `user/` and `agent/`, each with its own index — an index
26
+ * file under `docs/user/` for people and `docs/agent/README.md` as a list of pointers. The user
27
+ * pages are markdown, and each one is one of the four kinds of the Diátaxis method
28
+ * (https://diataxis.fr): a tutorial, a how-to guide, a reference or an explanation. The
29
+ * breadcrumbs say so as guidance and refuse nothing, so how `docs/user/` is foldered inside stays
30
+ * the repo's choice. The guardrail below refuses anything else at the top of the folder, so a repo
31
+ * with a different shape either moves its pages or does not bind this pack.
32
+ */
29
33
  export declare const docs: import("../index.ts").Pack<{
30
34
  /**
31
35
  * Without it, an agent writing under `docs/` has no way to know there are two audiences. User
@@ -45,5 +49,4 @@ export declare const docs: import("../index.ts").Pack<{
45
49
  * drawer, and neither audience's rules can be applied to what is in it.
46
50
  */
47
51
  docsShape: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
48
- noMarkdownInUserDocs: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
49
52
  }, Doors | undefined>;
@@ -8,9 +8,10 @@ export interface Home {
8
8
  * list nobody reviews, and here it decides what two rules watch: the nudge when you edit the
9
9
  * guard, and the refusal to delete it. Both name it and neither defaults.
10
10
  *
11
- * The engine derives the REPAIR surface from the config's imports directly (`configSurface`),
12
- * because it is asked in the broken state and can only read text. This is the same fact asked
13
- * at a moment where the config loads, so it is stated once, here, and typed.
11
+ * The engine reads the same folders out of the config's imports (`guardPaths`), for the
12
+ * weaken-after-block report, because that report must answer while the config will not load
13
+ * and text is all it can read then. This is the same fact asked at a moment where the config
14
+ * loads, so it is stated once, here, and typed.
14
15
  */
15
16
  readonly packs: readonly string[];
16
17
  /**
@@ -27,8 +28,9 @@ export interface Home {
27
28
  /**
28
29
  * flow's own orientation and self-protection, as a pack any repo binds.
29
30
  *
30
- * Every guarded repo wants these three: a note saying what is steering you, a nudge when you edit
31
- * the steering itself, and a refusal when a command would delete it.
31
+ * Every guarded repo wants these four: a note saying what is steering you, a nudge when you edit
32
+ * the steering itself, a refusal when a command would delete it, and a refusal when an agent runs
33
+ * `flow uninstall`, which removes the guard without deleting a file the delete refusal watches.
32
34
  *
33
35
  * @setup A `flow.config.ts` at the repo root and a folder for the packs the repo writes itself —
34
36
  * whatever it is called, named here as the `packs` parameter. `flow init` writes both.
@@ -47,4 +49,12 @@ export declare const flow: import("../index.ts").Pack<{
47
49
  */
48
50
  editingTheGuardrails: import("../index.ts").BreadcrumbSentence<"text" | "at" | "on" | "description", "touch">;
49
51
  noDeleteGuardrails: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "delete">;
52
+ /**
53
+ * Without it, an agent that hit a guardrail could take the whole guard off with one command.
54
+ * `flow uninstall` removes the commit gate, unsets `core.hooksPath` and removes flow's hook
55
+ * registrations from inside its own process, so no shell command deletes a file that
56
+ * `noDeleteGuardrails` watches, and that refusal never fires. A person in their own terminal
57
+ * has no hooks in front of them, so this refusal never reaches them.
58
+ */
59
+ noUninstall: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
50
60
  }, Home>;
@@ -1,30 +1,14 @@
1
- /**
2
- * The working-tree paths a command line would discard OUTRIGHT, or [] when it is safe to run.
3
- *
4
- * Deliberately narrow — false negatives beat blocking real work. Every path here is one the
5
- * command line itself declares: the `--` pathspec form of checkout, the restore forms that touch
6
- * the tree, and `reset --hard` / `clean -f`, whose target is the tree itself.
7
- *
8
- * The BARE form of checkout — `git checkout <thing>` with no `--` — is not here and cannot be:
9
- * whether `<thing>` is a path or a branch is a fact about the repo, not about the string, and this
10
- * function is pure. `checkoutBarePaths` below names the candidates and the check asks git.
11
- */
12
- export declare function discardPaths(command: string): string[];
13
- /**
14
- * The words a bare `git checkout` might be aiming at — candidates, not answers.
15
- *
16
- * `git checkout <path>` discards that path exactly as `git checkout -- <path>` does, and until F3
17
- * only the second was refused. The comment above this rule claimed the narrowing was deliberate
18
- * ("branch switching passes"), and it was — but it bought that pass by missing the form a person
19
- * actually types, which the F3 builder proved on itself by running `git checkout adapter/claude.ts`
20
- * over an hour of uncommitted work and watching the guard allow it.
21
- *
22
- * An invocation carrying `--` is skipped: `discardPaths` already has it, and everything before the
23
- * `--` there is a tree-ish by definition.
24
- */
25
- export declare function checkoutBarePaths(command: string): string[];
1
+ import { type Ctx } from "../index.ts";
26
2
  /** Of some porcelain lines, the paths with real uncommitted work. */
27
3
  export declare function dirtyIn(porcelain: string, includeUntracked: boolean): string[];
4
+ /**
5
+ * Each way a command line writes a commit or a ref the commit gate never runs on, or [] when it
6
+ * writes none: `git commit --no-verify` or `-n`, `-c core.hooksPath=…` before any subcommand, and
7
+ * git's plumbing that writes commits and refs directly (`commit-tree`, `update-ref`,
8
+ * `hash-object -w`). The git commands are read from the parse, so a `git commit -n` inside a quoted
9
+ * argument or a heredoc body is not one.
10
+ */
11
+ export declare function gateSkips(facts: Pick<Ctx, "command" | "commands">): string[];
28
12
  /** Does this header line speak Conventional Commits for the given type set? */
29
13
  export declare function isConventional(header: string, types: readonly string[]): boolean;
30
14
  /**
@@ -95,9 +79,20 @@ export declare const git: import("../index.ts").Pack<{
95
79
  * that has no undo.
96
80
  */
97
81
  orientation: import("../index.ts").BreadcrumbSentence<"text" | "at" | "description", "session">;
82
+ /**
83
+ * Claude Code tells every agent to end a commit or a pull request with a line crediting Claude,
84
+ * and an agent follows the harness's instruction over the repo's. Without this, the credit lands
85
+ * in history, where removing it means rewriting commits that may already be shared.
86
+ */
98
87
  noAiAttributionInCommits: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
99
88
  noGitDiscard: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
100
89
  noForcePush: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
90
+ /**
91
+ * The commit gate is a hook, and git has ways to commit without running it. A commit made that
92
+ * way holds content no rule judged, and flow treats a commit that already exists as judged: a
93
+ * later `git switch` to it puts its files in place unread.
94
+ */
95
+ noGateSkip: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
101
96
  conventionalCommitFormat: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
102
97
  noShellSubstitutionInProse: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
103
98
  node: {
@@ -25,13 +25,13 @@ export interface Catalogue {
25
25
  /** The folder holding the code behind the recipes. Defaults to `tools`. */
26
26
  readonly tools?: string;
27
27
  /**
28
- * Commands this repo has a recipe for, as a map from the command's pattern to the recipe that
28
+ * Commands this repo has a recipe for, as a map from the command, in words, to the recipe that
29
29
  * replaces it — `{ "npm run build": "just build" }`.
30
30
  *
31
31
  * OPTIONAL, and with no map there is no entry at all: which raw command a repo has wrapped is a
32
32
  * fact only that repo has, and a rule with an empty list would be one more armed entry matching
33
- * nothing. The patterns are regular expressions over the command line, so `npx vitest\\b` is a
34
- * pattern and `npx vitest` is one too.
33
+ * nothing. Each key is a command as `banCommands` names one: a program and its arguments, so
34
+ * `"npx vitest"` refuses `npx vitest run` and `npx --yes vitest`, and passes `npx vitest-preview`.
35
35
  *
36
36
  * The catalogue's whole promise is that the recipe is the thing to reach for; a breadcrumb says
37
37
  * so once a session, and this refuses the raw command by name for the ones that matter.
@@ -45,9 +45,25 @@ export interface Seam {
45
45
  readonly dx?: string;
46
46
  /**
47
47
  * Seals every env file in place, leaving the public prefixes plaintext. Defaults to dotenvx's
48
- * own `dotenvx encrypt`, excluding the prefixes above by name.
48
+ * own `dotenvx encrypt`, excluding the prefixes above by name, and passing `--no-native` when
49
+ * `custody` is `"file"`.
49
50
  */
50
51
  readonly encrypt?: string;
52
+ /**
53
+ * Where the private key that decrypts the env files is kept. Defaults to `"native"`.
54
+ *
55
+ * `"native"` is the OS secret store: the macOS Keychain, Windows Credential Manager or Linux
56
+ * Secret Service. dotenvx 2.25 and later write the key there by default, and no file in the repo
57
+ * holds it. A machine without the key in its store decrypts nothing.
58
+ *
59
+ * `"file"` is `.env.keys`, beside the env files and never committed. You copy it by hand to every
60
+ * machine that runs the code. dotenvx writes it only when `dotenvx encrypt` is passed
61
+ * `--no-native`.
62
+ *
63
+ * The orientation tells the agent which of the two this repo uses, and where to look when a
64
+ * decrypt comes back empty.
65
+ */
66
+ readonly custody?: "native" | "file";
51
67
  }
52
68
  /**
53
69
  * Env files committed encrypted, decrypted through one seam, and the local-only files kept out of
@@ -63,9 +79,14 @@ export interface Seam {
63
79
  * ephemera that must never be committed (`.env.keys`, `.dev.vars`, `.env.*.local`), and runtime
64
80
  * secrets set per environment on the platform.
65
81
  *
66
- * @install dotenvx — https://dotenvx.com. It is what encrypts the committed files and what runs a
67
- * command with them decrypted, and with nothing else in place every sentence below names it
68
- * directly. The two command parameters are for a repo that has wrapped it in recipes of its own.
82
+ * The private key that decrypts the committed files is kept in one of two places, named by
83
+ * `custody`: the OS secret store, which is dotenvx's default, or a `.env.keys` file copied by hand
84
+ * to every machine.
85
+ *
86
+ * @install dotenvx 2.25 or later — https://dotenvx.com. It is what encrypts the committed files
87
+ * and what runs a command with them decrypted, and with nothing else in place every sentence below
88
+ * names it directly. 2.25 is the first release that keeps the key in the OS secret store by
89
+ * default. The two command parameters are for a repo that has wrapped it in recipes of its own.
69
90
  * @setup `.env.keys`, `.dev.vars` and `.env.*.local` in `.gitignore` — the rules below refuse them
70
91
  * in a commit, and an ignore entry is what stops anyone reaching that refusal.
71
92
  *
@@ -96,6 +117,13 @@ export interface Seam {
96
117
  * Bound, that repo hands over `dx: "just dx"` and `encrypt: "just env-encrypt"`. The third job is
97
118
  * nobody's parameter — no sentence in this pack names it — and it is here because an env seam
98
119
  * wants it.
120
+ *
121
+ * These recipes leave the key's place to dotenvx. Run with no terminal attached, as an agent runs
122
+ * them, it writes a new key to the OS secret store; in a terminal it asks. A repo bound with
123
+ * `custody: "file"` adds `--no-native` to the `dotenvx encrypt` line, so dotenvx writes the key to
124
+ * `.env.keys`. The switch matters only the first time a file is sealed, when its key is created.
125
+ * `dotenvx run` needs no switch in either case: it reads `.env.keys` before it looks in the OS
126
+ * secret store.
99
127
  * @adopt A repo with no `.env*` at all binds this for `noSecretsInCommits` alone, which is worth
100
128
  * having in a repo that has never held a secret, and turns `envEncrypted` off BY NAME in its
101
129
  * config with the reason on the record — an entry sitting dead is not the same as one opted out of.