@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.
- package/README.md +52 -271
- package/dist/flow.mjs +12293 -106
- package/dist/index.mjs +7945 -18
- package/dist/packs.mjs +9477 -115
- package/dist/types/adapter/archive.d.ts +8 -1
- package/dist/types/adapter/claude.d.ts +67 -18
- package/dist/types/adapter/domain.d.ts +991 -154
- package/dist/types/adapter/product.d.ts +5 -0
- package/dist/types/checks/domain.d.ts +134 -72
- package/dist/types/engine/domain.d.ts +75 -17
- package/dist/types/engine/state.d.ts +7 -4
- package/dist/types/index.d.ts +4 -3
- package/dist/types/language/domain.d.ts +227 -21
- package/dist/types/packs/docs.d.ts +17 -14
- package/dist/types/packs/flow.d.ts +15 -5
- package/dist/types/packs/git.d.ts +20 -25
- package/dist/types/packs/justfile.d.ts +3 -3
- package/dist/types/packs/secrets.d.ts +32 -4
- package/dist/types/packs/work.d.ts +22 -6
- package/docs/user/migrations/0.1.0.md +142 -0
- package/docs/user/migrations/0.1.1.md +44 -0
- package/docs/user/migrations/0.1.2.md +58 -0
- package/docs/user/migrations/0.2.0.md +113 -0
- package/docs/user/migrations/0.2.1.md +41 -0
- package/docs/user/migrations/0.2.2.md +104 -0
- package/docs/user/migrations/0.2.3.md +42 -0
- package/docs/user/migrations/0.3.0.md +289 -0
- package/docs/user/migrations/index.md +45 -0
- package/package.json +5 -3
|
@@ -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
|
|
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
|
|
69
|
+
/** The agent type the host started this spawn as, from its hook message. Absent on a parent. */
|
|
70
70
|
agentType?: string | undefined;
|
|
71
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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,
|
|
406
|
-
readonly ignore: Once<Spoken, "ignore", Scoping<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,
|
|
417
|
-
readonly ignore: Once<Spoken, "ignore", Scoping<At,
|
|
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",
|
|
602
|
-
readonly ignore: Once<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"
|
|
605
|
-
readonly file: Kind extends "breadcrumb" ? Once<Spoken, "file", (path: string) => OverrideChain<Kind, Spoken | "file"
|
|
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
|
-
*
|
|
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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
31
|
-
* the steering itself,
|
|
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
|
|
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.
|
|
34
|
-
*
|
|
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
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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.
|