fyflow-orchestration 0.7.0 → 0.8.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 CHANGED
@@ -14,12 +14,10 @@ worker returned, routing can **fan out** into parallel branches, and a **join**
14
14
  +--> [review] (low confidence routes away entirely)
15
15
  ```
16
16
 
17
- The scheduler runs tasks. This library decides **which task runs next, and why** — because
18
- `fyflow-scheduler` has no dependency graph of its own, so sequence, decision, fan-out and join are
19
- built here rather than wrapped from below. See [doc/concept.md](doc/concept.md) for the reasoning.
17
+ The scheduler runs tasks; this library decides **which task runs next**. `fyflow-scheduler` has no
18
+ dependency graph of its own, so sequence, decision, fan-out and join live here.
20
19
 
21
- **0.7.0 is the first release**, on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`.
22
- The manifests read `0.7.0`.
20
+ Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.8.0`.
23
21
 
24
22
  ## Install
25
23
 
@@ -28,33 +26,27 @@ npm install fyflow-orchestration fyflow-scheduler # Node, and a browser bundl
28
26
  deno add jsr:@fyflow/orchestration # Deno
29
27
  ```
30
28
 
31
- npm carries an esbuild bundle per platform (`dist/node`, `dist/browser`) and the type declarations;
32
- JSR carries the TypeScript source. Both carry this README, [AGENTS.md](AGENTS.md) — the complete
33
- reference — and the `LICENSE` file.
34
-
35
- The scheduler is a **peer dependency**, so your installer resolves it rather than this library
36
- bundling it: `fyflow-scheduler` is pinned to **0.5.2** and installed from npm, and under Deno it comes
37
- from JSR at the same version. **Node 24 or a current Deno.** The library's own suites also run in the
38
- browser, but a flow does not: a worker there has to be a bundled `.js` asset, because nothing in a
39
- browser executes TypeScript.
29
+ - **Runtimes:** Node 24 or a current Deno. A flow does not run in a browser: a worker there would have
30
+ to be a bundled `.js` asset.
31
+ - **The scheduler is a peer dependency**, pinned to the exact version in this package's
32
+ `peerDependencies`. Deno takes it from JSR at the same version.
33
+ - **What ships:** npm has bundles for Node and browser bundlers plus type declarations, and JSR has
34
+ the TypeScript source. Both include [AGENTS.md](AGENTS.md), the complete reference.
40
35
 
41
36
  ## Getting started
42
37
 
43
- **Two JSON documents, and one file of your own.** A **registry** says what exists — a worker module,
44
- and a node type that runs on it. A **flow** says what happens — nodes naming those types, and the
45
- edges between them. `createRuntime` builds everything the registry names, and each run hands back
46
- `{ driver, done, header }` — the driver, a promise for the run's end, and the run's checkpoint header
47
- (the example below needs only `done`, so that is all it names). The one thing you write as code is the
48
- worker — [examples/getting-started/worker.ts](examples/getting-started/worker.ts), a class with a
49
- `run` method, and nothing else.
38
+ **Two JSON documents, and one file of your own.** A **registry** says what exists: worker modules,
39
+ and the node types that run on them. A **flow** says what happens: nodes naming those types, and the
40
+ edges between them. `createRuntime` builds what the registry names, and `start` runs a flow on it.
41
+ The only code you write is the worker,
42
+ [examples/getting-started/worker.ts](examples/getting-started/worker.ts): a class with a `run` method.
50
43
 
51
44
  ```ts
52
45
  import { createRuntime } from 'fyflow-orchestration/scheduler';
53
46
  import { type FlowDefinition, type RegistryDocument } from 'fyflow-orchestration';
54
47
 
55
48
  // THE REGISTRY — what exists. One worker module, and one node type that runs on it. This is data:
56
- // in your program it is a JSON file you import. The worker url is relative, and `base` below says
57
- // to what; the file itself is `examples/getting-started/worker.ts` — a class with a `run` method.
49
+ // in your program it is a JSON file you import. The worker url is relative to `base` below.
58
50
  const registry: RegistryDocument = {
59
51
  workers: {
60
52
  greeter: {
@@ -66,9 +58,7 @@ const registry: RegistryDocument = {
66
58
  };
67
59
 
68
60
  // THE FLOW — what happens. Two nodes of that type, each with its own `settings`, then an end. A flow
69
- // names registry KEYS and nothing else — no file, no url, no class — so it is portable across
70
- // runtimes that agree on the keys. The end node's `outcome` is a LABEL of yours that the driver does
71
- // not interpret; the run's own outcome is the fold's — `state.outcome` below.
61
+ // names registry keys only (no files, urls or classes), so it is portable between runtimes.
72
62
  const flow: FlowDefinition = {
73
63
  flowId: 'hello', version: 1, entry: 'greet',
74
64
  nodes: [
@@ -78,29 +68,22 @@ const flow: FlowDefinition = {
78
68
  ],
79
69
  };
80
70
 
81
- // ONE call builds the pool, the scheduler and everything between, and refuses every defect in the
82
- // REGISTRY at construction, by name. It never sees the flow: a defect THERE is refused where the
83
- // flow arrives, at `start`, which is what builds the `Driver` — so a `settings` value the worker's
84
- // schema rejects names its node from the call three lines below, not from this one. `base` is what
85
- // relative worker urls resolve against — `import.meta.url` when the registry sits beside this file.
71
+ // ONE call builds the pools and the scheduler, and refuses a defective registry here, by name. A
72
+ // defect in the FLOW is refused by `start`, below.
86
73
  const runtime = createRuntime(registry, {}, { base: import.meta.url });
87
74
 
88
- // `start` hands back `{ driver, done }` — the driver already running, and `done` a promise that
89
- // resolves with the run's terminal `RunState` (ADR 0058). **It never rejects**: `failed`, `stuck`
90
- // and `stopped` are terminal OUTCOMES, so a run that goes wrong arrives as `state.outcome` and not
91
- // as a `catch`. This snippet wants only the end, so it names only `done`. *A run with a person in
92
- // it does not end by itself — section 7 is where that is taught, and section 8 is what `done` waits
93
- // for.*
75
+ // `start` runs the flow. `done` resolves with the run's final state and never rejects: a run that
76
+ // goes wrong arrives as `state.outcome` ('failed', 'stuck', 'halted'), not as a `catch`.
94
77
  const { done } = runtime.start(flow, { text: 'hello' });
95
78
  const state = await done;
96
79
  await runtime.shutdown();
97
80
 
98
- // What the run did, read off its state: `greet` turned `hello` into `hello world`, and `shout` was
99
- // handed THAT as its input — the previous node's result is the next node's `input`.
81
+ // The previous node's result is the next node's input: `greet` made `hello world`, `shout` added `!`.
100
82
  const shout = Object.values(state.nodes).find((n) => n.nodeId === 'shout');
101
83
  console.log(state.outcome); // 'completed'
102
84
  console.log(shout?.result); // { text: 'hello world !' }
103
85
 
86
+ // Verified by the docs gate:
104
87
  if (state.outcome !== 'completed') throw new Error(`expected completed, got ${state.outcome}`);
105
88
  if (JSON.stringify(shout?.result) !== '{"text":"hello world !"}') throw new Error(`shout returned ${JSON.stringify(shout?.result)}`);
106
89
  ```
@@ -108,37 +91,32 @@ if (JSON.stringify(shout?.result) !== '{"text":"hello world !"}') throw new Erro
108
91
 
109
92
  ## A flow that decides
110
93
 
111
- **The same call, with the node kinds that make it a flow.** A **decision** is an edge with a `when`
112
- naming a predicate — code you supply, under a name the registry declares. A **fan-out** opens
113
- branches that run concurrently; a **join** waits for them and hands the next node every branch's
114
- result keyed by branch path. Two runs on one runtime, at high and low confidence, take different
115
- roads — and which nodes ran is read off each run's state, not asserted from a diagram. The worker is
116
- [examples/getting-started/tag.ts](examples/getting-started/tag.ts), five lines that tag what they see.
94
+ A **decision** is an edge with a `when` naming a predicate: code you supply, under a name the
95
+ registry declares. A **fan-out** opens branches that run concurrently; a **join** waits for them and
96
+ hands the next node every branch's result, keyed by branch path. Below, two runs at high and low
97
+ confidence take different roads. The worker is
98
+ [examples/getting-started/tag.ts](examples/getting-started/tag.ts).
117
99
 
118
100
  ```ts
119
101
  import { createRuntime } from 'fyflow-orchestration/scheduler';
120
102
  import { type FlowDefinition, type Predicate, type RegistryDocument, type RunState } from 'fyflow-orchestration';
121
103
 
122
- // THE REGISTRY — one worker, one type, and the NAME of a predicate the flow may use. A predicate is
123
- // code (ADR 0002), supplied beside the document under that name; a flow using a name nobody supplied
124
- // is refused at construction, not at the decision. The worker tags its input with the node's label.
104
+ // THE REGISTRY — one worker, one type, and the NAME of a predicate the flow may use. The predicate
105
+ // itself is code, passed to `createRuntime` under that name. The worker tags its input with a label.
125
106
  const registry: RegistryDocument = {
126
107
  workers: { tag: { url: '../../examples/getting-started/tag.ts' } }, // in your program: './tag.ts'
127
108
  nodeTypes: { Tag: { worker: 'tag', inline: true } },
128
109
  predicates: ['isLowConfidence'],
129
110
  };
130
- // A predicate reads the EFFECTIVE outcome, and `result` is `null` for an optional task that
131
- // failed (`OR-R9`) — so it is guarded, as section 4's is. Unreachable here, because `ingest` is
132
- // not optional; guarded anyway, because this is the form a consumer copies.
111
+ // `result` is `null` when an optional task failed, so a predicate guards it.
133
112
  const isLowConfidence: Predicate = (ctx) => {
134
113
  const r = ctx.outcome.result as { confidence?: number } | null;
135
114
  return typeof r?.confidence === 'number' && r.confidence < 0.5;
136
115
  };
137
116
 
138
- // THE FLOW — a DECISION, a FAN-OUT, a JOIN. `ingest`'s first edge is taken when its predicate says so,
139
- // otherwise the last, unconditional one. `split` opens two branches that run concurrently; `collect`
140
- // waits for both and hands `publish` every branch's result keyed by branch path (JP-R12). `review` is
141
- // the road the run takes when confidence is low — it never reaches the join, and the run completes.
117
+ // THE FLOW — a DECISION, a FAN-OUT, a JOIN. `ingest` goes to `review` when the predicate holds,
118
+ // otherwise to `split`. `split` runs two branches concurrently; `collect` waits for both and hands
119
+ // `publish` every branch's result, keyed by branch path.
142
120
  const flow: FlowDefinition = {
143
121
  flowId: 'decide', version: 1, entry: 'ingest',
144
122
  nodes: [
@@ -154,44 +132,42 @@ const flow: FlowDefinition = {
154
132
  ],
155
133
  };
156
134
 
157
- // ONE runtime, TWO runs. Pools belong to the runtime and are shared across runs; each run is its own
158
- // `Driver` with its own fold, and the join policy defaults to `live-token` (ADR 0003).
135
+ // ONE runtime, TWO runs. Pools belong to the runtime and are shared; each run has its own state.
159
136
  const runtime = createRuntime(registry, { predicates: { isLowConfidence } }, { base: import.meta.url });
160
- // Each run has its OWN `done`, so two runs are two promises — `start` is called twice and the whole
161
- // of the wait is `await`. Nothing here folds anything: the runtime keeps the fold (ADR 0058).
162
137
  const run = (payload: unknown) => runtime.start(flow, payload).done;
163
138
  const high = await run({ confidence: 0.9 });
164
139
  const low = await run({ confidence: 0.2 });
165
140
  await runtime.shutdown();
166
141
 
167
- // WHICH nodes ran, read off each run's own state — the decision took a different edge each time.
142
+ // Which nodes ran: the decision took a different edge each time.
168
143
  const ran = (s: RunState) => Object.values(s.nodes).map((n) => n.nodeId).sort();
169
144
  const resultOf = (s: RunState, id: string) => Object.values(s.nodes).find((n) => n.nodeId === id)?.result;
170
145
  console.log(ran(high)); // [ 'classify', 'enrich', 'ingest', 'publish' ]
171
146
  console.log(ran(low)); // [ 'ingest', 'review' ]
172
147
  console.log(resultOf(high, 'publish')); // { '0': { confidence: 0.9, ingest: true, enrich: true }, '1': { …, classify: true }, publish: true }
173
148
 
149
+ // Verified by the docs gate:
174
150
  if (high.outcome !== 'completed' || low.outcome !== 'completed') throw new Error(`outcomes ${high.outcome} / ${low.outcome}`);
175
151
  if (ran(high).join() !== 'classify,enrich,ingest,publish') throw new Error(`high ran ${ran(high).join()}`);
176
152
  if (ran(low).join() !== 'ingest,review') throw new Error(`low ran ${ran(low).join()}`);
177
- // `publish` received BOTH branches' results, keyed by branch path (JP-R12), and each branch's value
178
- // carries the whole road it came by. What this string pins is the branch-to-KEY MAPPING: the ASCENDING
179
- // order that rule also fixes is not observable at depth 1, because "0" and "1" are integer-like keys
180
- // and the language orders those ascending whatever the driver did. A nested region would show it.
181
153
  const want = '{"0":{"confidence":0.9,"ingest":true,"enrich":true},"1":{"confidence":0.9,"ingest":true,"classify":true},"publish":true}';
182
154
  if (JSON.stringify(resultOf(high, 'publish')) !== want) throw new Error(`publish received ${JSON.stringify(resultOf(high, 'publish'))}`);
183
155
  ```
184
156
 
185
157
  ## Checking a flow before it runs
186
158
 
187
- **A schema is for the AUTHOR, and this is what it buys.** Every worker declares what it expects and
188
- what it returns; `checkWiring` gives every arc of a flow one of five verdicts from those declarations
189
- BEFORE anything runs, `pathsOf` says what there is to reach for, and `tryTransform` runs a candidate
190
- JSONata expression on the producer's own declared example and holds the result to the consumer's
191
- schema. Below: a flow with a real miswire, the transform that repairs it found in two tries, a
192
- TypeScript signature generated from the same schemas, and then the run. The two workers are
159
+ Every worker declares JSON schemas for what it expects and what it returns, so a flow can be checked
160
+ before anything runs:
161
+
162
+ - `checkWiring` gives every arc of a flow a verdict from those declarations.
163
+ - `pathsOf` lists what a producer's output offers.
164
+ - `tryTransform` runs a candidate JSONata expression on the producer's declared example and checks
165
+ the result against the consumer's schema.
166
+ - `signatureOf` generates the TypeScript signature a worker is written against.
167
+
168
+ Below, a flow with a miswire is repaired in two tries, then run. The two workers are
193
169
  [examples/getting-started/reader.ts](examples/getting-started/reader.ts) and
194
- [summarise.ts](examples/getting-started/summarise.ts), whose schemas oppose on purpose.
170
+ [summarise.ts](examples/getting-started/summarise.ts), whose schemas deliberately do not match.
195
171
 
196
172
  ```ts
197
173
  import { checkWiring, describePaths, pathsOf, signatureOf, tryTransform, type FlowDefinition } from 'fyflow-orchestration';
@@ -203,8 +179,7 @@ import { SESSION_BASE, SESSION_REGISTRY, SESSION_WORKERS } from '../../examples/
203
179
  // and the languages a transform may be written in. `createRuntime` takes the whole document below.
204
180
  const registry = { workers: SESSION_WORKERS, nodeTypes: SESSION_REGISTRY.nodeTypes, languages: { jsonata } };
205
181
 
206
- // THE FLOW, as an author first writes it: read a batch, summarise it. It is a VALID document —
207
- // `validateDefinition` has nothing to say about it, because nothing here is about the grammar.
182
+ // THE FLOW, as an author first writes it: read a batch, summarise it. Its grammar is valid.
208
183
  const flow: FlowDefinition = {
209
184
  flowId: 'session', version: 1, entry: 'read',
210
185
  nodes: [
@@ -229,40 +204,39 @@ console.log(describePaths(pathsOf(SESSION_WORKERS.reader.outputSchema)));
229
204
  // ? $.lines[] string
230
205
  // $.source string
231
206
 
232
- // STEP 3 — a first candidate. It is run on the producer's declared EXAMPLE, and the result is held
233
- // to the consumer's input schema, so the answer is about a value rather than about two declarations.
207
+ // STEP 3 — a first candidate, run on the producer's declared example and checked against the
208
+ // consumer's input schema.
234
209
  const attempt = await tryTransform(registry, flow, 'sum', { lang: 'jsonata', expr: '{ "text": lines }' });
235
210
  console.log(`${attempt[0]!.kind}: ${attempt[0]!.why}`);
236
211
  // transform-fails: the transform's result on the example fails the consumer's inputSchema — $.text expected string, got array
237
212
  if (attempt[0]!.kind !== 'transform-fails') throw new Error('an array is not a string, and the loop says which');
238
213
 
239
- // STEP 4 — ONE expression edit, and the same loop. `transform-passes` is never `fits`: it is a fact
240
- // about the sample the verdict names, which is why `sample.source` is on it (a declared example
241
- // here; where a schema declares none, a generated WITNESS, and the verdict says so).
214
+ // STEP 4 — one edit, and the same loop. `transform-passes` is about the sample it names, which is
215
+ // the declared example here, or a generated one where a schema declares none.
242
216
  const good = { lang: 'jsonata', expr: '{ "text": $join(lines, " ") }' };
243
217
  const passed = await tryTransform(registry, flow, 'sum', good);
244
218
  console.log(`${passed[0]!.kind} on the ${passed[0]!.sample!.source}: ${JSON.stringify(passed[0]!.sample!.result)}`);
245
219
  if (passed[0]!.kind !== 'transform-passes') throw new Error(`expected the sample to pass, got ${passed[0]!.kind}`);
246
220
 
247
- // STEP 5 — the signature the worker is written against, generated from the same three schemas.
248
- // `examples/getting-started/session-types.ts` is this text, checked in, and `summarise.ts` imports
249
- // it. A type holds the SHAPE: `words` is `number` here and its schema says `minimum: 0`.
221
+ // STEP 5 — the signature the worker is written against, generated from the same schemas.
222
+ // `examples/getting-started/session-types.ts` is this text, and `summarise.ts` imports it.
250
223
  const signature = signatureOf('Summarise', SESSION_WORKERS.summarise);
251
224
  console.log(signature.split('\n').filter((l) => l.startsWith('export')).join('\n'));
252
225
  if (!signature.includes('"text": string;')) throw new Error('the required key is required in the type too');
253
226
 
254
- // STEP 6 — and only now the runtime. Pools are built ONCE, so a flow whose transform was decided
255
- // during the session has to reach `createRuntime` as `flows`, or `start` throws `LanguageNotBuilt`
256
- // naming the type. Check, then build.
227
+ // STEP 6 — only now the runtime. Check the wired flow against it, then start it; `start` refuses
228
+ // from that same check.
257
229
  const wired: FlowDefinition = { ...flow, nodes: flow.nodes.map((n) => (n.id === 'sum' ? { ...n, transform: good } : n)) };
258
- const runtime = createRuntime(SESSION_REGISTRY, { languages: { jsonata } }, { base: SESSION_BASE, flows: [wired] });
230
+ const runtime = createRuntime(SESSION_REGISTRY, { languages: { jsonata } }, { base: SESSION_BASE });
231
+ const refused = runtime.check(wired);
232
+ if (refused.length !== 0) throw new Error(`the runtime refuses the wired flow: ${JSON.stringify(refused)}`);
233
+ const unknownLang: FlowDefinition = { ...flow, nodes: flow.nodes.map((n) => (n.id === 'sum' ? { ...n, transform: { lang: 'nope', expr: '$' } } : n)) };
234
+ if (!runtime.check(unknownLang).some((f) => f.rule === 'FD-V35' && f.at === 'sum')) throw new Error('a language the registry does not name is FD-V35, at check');
259
235
  const state = await runtime.start(wired, { source: 'inbox' }).done;
260
236
  await runtime.shutdown();
261
237
  console.log(state.outcome, Object.values(state.nodes).find((n) => n.nodeId === 'sum')?.result); // completed { words: 3 }
262
238
 
263
- // The expected values are the DOCUMENT's, not literals beside the run: the consumer's own declared
264
- // output example, and the producer's declared line count. A registry whose examples stop describing
265
- // its workers is red here and in `test/definition/assist.test.ts`, which runs the same chain.
239
+ // Verified by the docs gate:
266
240
  if (state.outcome !== 'completed') throw new Error(`expected completed, got ${state.outcome}`);
267
241
  const got = Object.values(state.nodes).find((n) => n.nodeId === 'sum')?.result;
268
242
  if (JSON.stringify(got) !== JSON.stringify(SESSION_WORKERS.summarise.outputSchema.examples[0])) {
@@ -277,21 +251,34 @@ if (after.counts.no !== 0) throw new Error('and the flow that ran has no unwired
277
251
 
278
252
  ## More examples
279
253
 
280
- The three above are the shape of it. [AGENTS.md](AGENTS.md) is the full reference — every section's
281
- snippet is extracted and executed on every `npm run gate`, so none of it can drift from the code:
254
+ [AGENTS.md](AGENTS.md) is the full reference, and every snippet in it is tested:
282
255
 
283
256
  | | |
284
257
  |---|---|
285
- | Holding a **result** to the schema its worker declares | [AGENTS.md](AGENTS.md) section 16 |
258
+ | A task **cancelled** by its id — the run ends `cancelled` and refuses a resume | [AGENTS.md](AGENTS.md) section 6 |
259
+ | **Cancelling a run** — `driver.cancel`, the run `cancelling` until its last task has stopped | section 6 |
260
+ | **Pausing** a run and unpausing it — every dispatch held, then handed on in order | section 8 |
261
+ | A node that **waits for a person** by design — an `ask`, which keeps what reached it and holds the answer to a schema | section 7 |
262
+ | A task's **messages and progress**, each with its run, node and attempt as fields — `onTaskMessage`, `onTaskProgress` | section 8 |
263
+ | A **worker's own messages**, outside any task — `worker.message` on its pool, reached through `createScheduler` | section 8 |
264
+ | Reaching what reached the **run** from any node or edge — `needs: { invoice: "$payload" }` | section 3 |
265
+ | Reaching the item that opened a **map instance** from any node in it — `needs: { invoice: "$item" }` | section 3 |
266
+ | **Stopping** a run to resume it on another runtime — `handle.stop` | section 18 |
267
+ | Writing a run's **header first**, before its first event — `onHeader` | section 18 |
268
+ | A resumed task **keeps its retry budget** — the attempts it already made, handed on as `attemptsSpent` | section 18 |
269
+ | **Checking** a flow against a running runtime before starting it — `runtime.check` | section 12 |
270
+ | **Trying** a transform on a value you give, the entry node included | section 15 |
271
+ | Holding a **result** to the schema its worker declares | section 16 |
286
272
  | Fanning out **per item** — a region inside a `map` region | section 17 |
287
273
  | **Saving** a run and **resuming** it from the stored events | section 18 |
288
274
  | What a node may **remember** across branches — a `gate` | section 19 |
275
+ | Routing with **no code** — the built-in `if` and `switch` gates | section 19 |
289
276
  | Wiring a gate **before it runs** | section 20 |
290
277
 
291
- Runnable programs live in [examples/](examples/) — including
292
- [examples/flows/triage.flow.json](examples/flows/triage.flow.json) with
293
- [examples/run-flow.ts](examples/run-flow.ts), which puts a custom resource group, a node-type class,
294
- a named pool instance, a decision, a fan-out, a join and a declared transform on real workers.
278
+ Runnable programs are in [examples/](examples/). For a larger one, see
279
+ [examples/run-flow.ts](examples/run-flow.ts) running
280
+ [examples/flows/triage.flow.json](examples/flows/triage.flow.json): a custom resource group, a
281
+ node-type class, a named pool, a decision, a fan-out, a join and a transform.
295
282
 
296
283
  ## Running the tests
297
284
 
@@ -299,22 +286,19 @@ a named pool instance, a decision, a fan-out, a join and a declared transform on
299
286
  npm install
300
287
  npm test # the unit and routing suites
301
288
  npm run test:l6 # real runs on a real scheduler
302
- npm run gate # everything above, in order
289
+ npm run gate # lint, typecheck, checks, all suites, Node and Deno runs
303
290
  ```
304
291
 
305
- `npm run gate` is the full ladder — lint, typecheck, the static checks, the suites in three runtimes,
306
- and the real-scheduler runs in Node and Deno.
307
-
308
292
  ## Documentation
309
293
 
310
294
  | | |
311
295
  |---|---|
312
- | [AGENTS.md](AGENTS.md) | the complete reference: every snippet in it is typechecked by the gate, each one tagged `run` is executed, and a snippet tagged `check` is typechecked only |
296
+ | [AGENTS.md](AGENTS.md) | the complete API reference, with tested snippets |
313
297
  | [doc/concept.md](doc/concept.md) | what this is and why it is shaped this way |
314
- | [spec/](spec/) | the normative rules — the flow grammar, run state, join policies, task ids |
315
- | [doc/README.md](doc/README.md) | the development index: plans, work items, decisions, open questions |
316
- | [CLAUDE.md](CLAUDE.md) | guidance for an agent working ON this library |
298
+ | [spec/](spec/) | the normative rules: flow grammar, run state, join policies, task ids |
299
+ | [doc/README.md](doc/README.md) | for contributors: plans, work items, decisions, open questions |
300
+ | [CLAUDE.md](CLAUDE.md) | for an agent working on this library |
317
301
 
318
302
  ## Licence
319
303
 
320
- MIT — the `LICENSE` file at the repository root, which both registries carry beside the code.
304
+ MIT. See `LICENSE`.